package net.minestom.codegen;

import com.squareup.javapoet.*;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.lang.model.element.Modifier;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;

/**
 * Helper class to generate a .java enum
 */
public class EnumGenerator implements CodeGenerator {

    private final static Logger LOGGER = LoggerFactory.getLogger(EnumGenerator.class);
    private static final String COMMENT = "//==============================\n//  AUTOGENERATED BY " + EnumGenerator.class.getSimpleName() + "\n//==============================";

    private final String enumName;
    private ParameterSpec[] parameters;
    private List<TypeName> superinterfaces = new LinkedList<>();
    private List<Method> methods = new LinkedList<>();
    private List<Field> staticFields = new LinkedList<>();
    private List<Instance> instances = new LinkedList<>();
    private List<Pair<Field, Boolean>> fields = new LinkedList<>();
    private List<Field> hardcodedFields = new LinkedList<>();
    private List<AnnotationSpec> annotations = new LinkedList<>();
    private String enumPackage;
    private CodeBlock staticBlock;
    private List<Consumer<CodeBlock.Builder>> constructorEnds = new LinkedList<>();

    public EnumGenerator(String packageName, String enumName) {
        this.enumPackage = packageName;
        parameters = new ParameterSpec[0];
        this.enumName = enumName;
    }

    public void addSuperinterface(TypeName typeNames) {
        superinterfaces.add(typeNames);
    }

    public void setParams(ParameterSpec... parameters) {
        this.parameters = parameters;
    }

    public void addMethod(String name, ParameterSpec[] signature, TypeName returnType, Consumer<CodeBlock.Builder> code) {
        methods.add(new Method(false, name, signature, returnType, code, false));
    }

    public void addVarargMethod(String name, ParameterSpec[] signature, TypeName returnType, Consumer<CodeBlock.Builder> code) {
        methods.add(new Method(false, name, signature, returnType, code, true));
    }

    public void addStaticMethod(String name, ParameterSpec[] signature, TypeName returnType, Consumer<CodeBlock.Builder> code) {
        methods.add(new Method(true, name, signature, returnType, code, false));
    }

    public void addStaticField(TypeName type, String name, String value) {
        staticFields.add(new Field(type, name, value));
    }

    public void addInstance(String name, Object... parameters) {
        instances.add(new Instance(name, parameters));
    }

    public List<JavaFile> generate() {
        TypeSpec.Builder enumClass = TypeSpec.enumBuilder(ClassName.get(enumPackage, enumName)).addModifiers(Modifier.PUBLIC);

        enumClass.addJavadoc(COMMENT);
        for (AnnotationSpec annotation : annotations) {
            enumClass.addAnnotation(annotation);
        }

        for (Instance instance : instances) {
            StringBuilder format = new StringBuilder();
            for (int i = 0; i < instance.parameters.length; i++) {
                if (i != 0) {
                    format.append(", ");
                }
                if (instance.parameters[i] instanceof ConstructorLambda) {
                    instance.parameters[i] = ((ConstructorLambda) instance.parameters[i]).getClassName();
                    format.append("$T::new");
                } else {
                    format.append("$L");
                }
            }

            // generate instances
            TypeSpec arguments = TypeSpec.anonymousClassBuilder(format.toString(), instance.parameters).build();
            enumClass.addEnumConstant(instance.name, arguments);
        }

        // add superinterfaces
        enumClass.addSuperinterfaces(superinterfaces);

        if (staticBlock != null) {
            enumClass.addStaticBlock(staticBlock);
        }

        // generate properties & constructor
        if (parameters.length != 0) {
            // properties
            for (ParameterSpec property : parameters) {
                enumClass.addField(FieldSpec.builder(property.type, property.name)
                        .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
                        .addAnnotations(property.annotations)
                        .build());
            }

            for (Field field : staticFields) {
                enumClass.addField(FieldSpec.builder(field.type, field.name)
                        .initializer("$L", field.value)
                        .addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC)
                        .build());
            }

            // hard coded fields
            for (Field hardcoded : hardcodedFields) {
                enumClass.addField(FieldSpec.builder(hardcoded.type, hardcoded.name)
                        .initializer("$L", hardcoded.value)
                        .addModifiers(Modifier.PRIVATE)
                        .build());
            }

            // normal fields
            for (Pair<Field, Boolean> field : fields) {
                FieldSpec.Builder builder = FieldSpec.builder(field.getLeft().type, field.getLeft().name)
                        .addModifiers(Modifier.PRIVATE);

                if (field.getRight()) {
                    builder.addModifiers(Modifier.FINAL);
                }

                enumClass.addField(builder.build());
            }

            // constructor
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder();
            for (int i = 0; i < parameters.length; i++) {
                ParameterSpec param = parameters[i];
                constructorBuilder.addParameter(param);

                // property assignment
                constructorBuilder.addStatement("this.$N = $N", param.name, param.name);
            }
            CodeBlock.Builder generatorEnd = CodeBlock.builder();
            constructorEnds.forEach(b -> b.accept(generatorEnd));
            constructorBuilder.addCode(generatorEnd.build());
            enumClass.addMethod(constructorBuilder.build());
        }

        // generate methods
        for (Method m : methods) {
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(m.name);
            if (m.isStatic) {
                methodBuilder.addModifiers(Modifier.STATIC);
            }
            methodBuilder.addModifiers(Modifier.PUBLIC);
            methodBuilder.returns(m.returnType);
            if (m.vararg) {
                methodBuilder.varargs(true);
            }
            for (ParameterSpec parameter : m.signature) {
                methodBuilder.addParameter(parameter);
            }

            CodeBlock.Builder builder = CodeBlock.builder();
            m.code.accept(builder);
            methodBuilder.addCode(builder.build());

            enumClass.addMethod(methodBuilder.build());
        }
        JavaFile file = JavaFile.builder(enumPackage, enumClass.build())
                .skipJavaLangImports(true)
                .indent("    ")
                .build();
        return Collections.singletonList(file);
    }

    public void setStaticInitBlock(CodeBlock staticBlock) {
        this.staticBlock = staticBlock;
    }

    public void appendToConstructor(Consumer<CodeBlock.Builder> constructorEnding) {
        constructorEnds.add(constructorEnding);
    }

    public void addField(TypeName type, String name, boolean isFinal) {
        fields.add(Pair.of(new Field(type, name), isFinal));
    }

    public void addHardcodedField(TypeName type, String name, String value) {
        hardcodedFields.add(new Field(type, name, value));
    }

    public void addClassAnnotation(AnnotationSpec annotation) {
        annotations.add(annotation);
    }

    public String getPackage() {
        return enumPackage;
    }

    public String getEnumName() {
        return enumName;
    }

    @Override
    public Logger getLogger() {
        return LOGGER;
    }

    private static class Method {
        private final boolean isStatic;
        private String name;
        private ParameterSpec[] signature;
        private TypeName returnType;
        private Consumer<CodeBlock.Builder> code;
        private boolean vararg;

        private Method(boolean isStatic, String name, ParameterSpec[] signature, TypeName returnType, Consumer<CodeBlock.Builder> code, boolean vararg) {
            this.isStatic = isStatic;
            this.name = name;
            this.signature = signature;
            this.returnType = returnType;
            this.code = code;
            this.vararg = vararg;
        }
    }

    private static class Field {
        private TypeName type;
        private String name;
        private String value;

        public Field(TypeName type, String name) {
            this(type, name, null);
        }

        public Field(TypeName type, String name, String value) {
            this.type = type;
            this.name = name;
            this.value = value;
        }
    }

    private static class Instance {
        private String name;
        private Object[] parameters;

        private Instance(String name, Object[] parameters) {
            this.name = name;
            this.parameters = parameters;
        }
    }
}
