package eu.jonahbauer.wizard.common.messages.util;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.*;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import eu.jonahbauer.wizard.common.messages.ParseException;
import eu.jonahbauer.wizard.common.util.StringUtil;
import lombok.experimental.UtilityClass;

import java.lang.reflect.Modifier;

@UtilityClass
public class SerializationUtil {
    public static ObjectMapper newObjectMapper(Class<?>...classes) {
        var mapper = new ObjectMapper()
                .registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
                .registerModule(new SimpleModule() {
                    @Override
                    public void setupModule(SetupContext context) {
                        super.setupModule(context);
                        for (Class<?> clazz : classes) {
                            context.setMixInAnnotations(clazz, Mixin.class);
                        }
                        context.insertAnnotationIntrospector(new NopAnnotationIntrospector() {
                            @Override
                            public JsonCreator.Mode findCreatorAnnotation(MapperConfig<?> config, Annotated ann) {
                                if (ann instanceof AnnotatedConstructor con) {
                                    var declaringClass = con.getDeclaringClass();
                                    for (Class<?> clazz : classes) {
                                        if (clazz.isAssignableFrom(declaringClass)) {
                                            return JsonCreator.Mode.PROPERTIES;
                                        }
                                    }
                                }
                                return super.findCreatorAnnotation(config, ann);
                            }
                        });
                    }
                });

        for (Class<?> clazz : classes) {
            registerSubtypes(mapper, clazz);
        }

        return mapper;
    }

    private static void registerSubtypes(ObjectMapper objectMapper, Class<?> clazz) {
        if (!clazz.isSealed()) throw new IllegalArgumentException();

        var suffix = "Message";
        for (Class<?> subclass : clazz.getPermittedSubclasses()) {
            int modifiers = subclass.getModifiers();

            if (Modifier.isFinal(modifiers) || subclass.isSealed() && !Modifier.isAbstract(modifiers)) {
                var name = subclass.getSimpleName();
                if (name.endsWith(suffix)) name = name.substring(0, name.length() - suffix.length());
                name = StringUtil.toSnakeCase(name);

                objectMapper.registerSubtypes(new NamedType(subclass, name));
            }

            if (subclass.isSealed()) {
                registerSubtypes(objectMapper, subclass);
            } else if (!Modifier.isFinal(modifiers)) {
                throw new IllegalArgumentException("Non-sealed classes are not supported.");
            }
        }
    }

    public static <T> T parse(String json, ObjectMapper objectMapper, Class<T> clazz) throws ParseException {
        try {
            return objectMapper.readValue(json, clazz);
        } catch (JsonProcessingException e) {
            throw new ParseException(e);
        }
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
    private static class Mixin {}
}
