package org.dominokit.jacksonapt.processor;

import static org.dominokit.jacksonapt.processor.AbstractMapperProcessor.elementUtils;
import static org.dominokit.jacksonapt.processor.AbstractMapperProcessor.filer;
import static org.dominokit.jacksonapt.processor.AbstractMapperProcessor.typeUtils;
import static org.dominokit.jacksonapt.processor.ObjectMapperProcessor.DEFAULT_WILDCARD;

import java.io.IOException;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

import org.dominokit.jacksonapt.JsonDeserializer;
import org.dominokit.jacksonapt.JsonSerializer;
import org.dominokit.jacksonapt.ObjectMapper;
import org.dominokit.jacksonapt.ObjectReader;
import org.dominokit.jacksonapt.ObjectWriter;
import org.dominokit.jacksonapt.processor.deserialization.FieldDeserializersChainBuilder;
import org.dominokit.jacksonapt.processor.serialization.FieldSerializerChainBuilder;

import com.google.auto.common.MoreTypes;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

public abstract class AbstractMapperGenerator implements MapperGenerator {

    private String packageName;

    @Override
    public void generate(Element element) throws IOException {
        String className = enclosingName(element, "_") + (useInterface(element) ? element.getSimpleName() : "Mapper") + "Impl";
        packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();
        TypeMirror beanType = getElementType(element);
        Name beanName = typeUtils.asElement(beanType).getSimpleName();

        generateJsonMappers(beanType, packageName, beanName);

        TypeSpec.Builder builder = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .superclass(abstractObjectMapper(element))
                .addField(FieldSpec.builder(ClassName.bestGuess(className), "INSTANCE")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                        .initializer(CodeBlock.builder().add("new $T()", ClassName.bestGuess(className)).build()).
                                build())
                .addMethod(makeConstructor(beanName))
                .addMethods(getMapperMethods(element, beanType));

        if (useInterface(element)) {
            builder.addSuperinterface(TypeName.get(element.asType()));
        }

        TypeSpec classSpec = builder
                .build();

        JavaFile.builder(packageName, classSpec).build().writeTo(filer);
    }

    protected static TypeMirror getElementType(Element element) {
        if (useInterface(element)) {
            TypeMirror objectReader = ((TypeElement) typeUtils.asElement(element.asType())).getInterfaces().get(0);
            return MoreTypes.asDeclared(objectReader).getTypeArguments().get(0);
        } else {
            return element.asType();
        }

    }

    protected static boolean useInterface(Element element) {
        return
                Type.isAssignableFrom(element.asType(), ObjectMapper.class)
                        || Type.isAssignableFrom(element.asType(), ObjectReader.class)
                        || Type.isAssignableFrom(element.asType(), ObjectWriter.class);
    }

    protected String enclosingName(Element element, String postfix) {
        Element enclosingElement = element.getEnclosingElement();
        if (useInterface(element) && !ElementKind.PACKAGE.equals(enclosingElement.getKind())) {
            return enclosingElement.getSimpleName().toString() + postfix;
        }else if(useInterface(element) && ElementKind.PACKAGE.equals(enclosingElement.getKind())){
            return "";
        }else{
            return element.getSimpleName().toString() + postfix;
        }
    }

    protected TypeName abstractObjectMapper(Element element) {
        TypeMirror beanType = getElementType(element);
        return ParameterizedTypeName.get(ClassName.get(getSuperClass()),
                ClassName.get(beanType));
    }

    protected MethodSpec makeConstructor(Name beanName) {
        return MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addStatement("super(\"" + beanName + "\")").build();
    }

    /**
     * <p>makeNewDeserializerMethod.</p>
     * <p>
     * Creates method for build corresponding deserializer for  given beanType. If beanType is
     * basic type, generated code utilize existing deserializers. Otherwise, it creates instances
     * of newly generated ones.
     *
     * @param element
     * @param beanType
     * @return
     */
    protected MethodSpec makeNewDeserializerMethod(Element element, TypeMirror beanType) {
        CodeBlock.Builder builder = CodeBlock.builder();
        if (Type.isBasicType(typeUtils.erasure(beanType))) {
            builder.addStatement("return $L", new FieldDeserializersChainBuilder(packageName, getElementType(element)).getInstance(getElementType(element)));
        } else {
            builder.addStatement("return new " + deserializerName(beanType));
        }

        return MethodSpec.methodBuilder("newDeserializer")
                .addModifiers(Modifier.PROTECTED)
                .addAnnotation(Override.class)
                .returns(ParameterizedTypeName.get(ClassName.get(JsonDeserializer.class),
                        ClassName.get(getElementType(element))))
                .addCode(builder.build())
                .build();

    }

    /**
     * <p>makeNewSerializerMethod.</p>
     * <p>
     * Creates method for build corresponding serializer for  given beanType. If beanType is
     * basic type, generated code utilize existing serializers. Otherwise, it creates instances
     * of newly generated ones.
     *
     * @param beanType
     * @return
     */
    protected MethodSpec makeNewSerializerMethod(TypeMirror beanType) {
        CodeBlock.Builder builder = CodeBlock.builder();
        if (Type.isBasicType(typeUtils.erasure(beanType))) {
            builder.addStatement("return $L", new FieldSerializerChainBuilder(packageName, beanType).getInstance(beanType));
        } else {
            builder.addStatement("return new " + serializerName(beanType));
        }

        return MethodSpec.methodBuilder("newSerializer")
                .addModifiers(Modifier.PROTECTED)
                .addAnnotation(Override.class)
                .returns(ParameterizedTypeName.get(ClassName.get(JsonSerializer.class), DEFAULT_WILDCARD))
                .addCode(builder.build())
                .build();
    }

    /**
     * Create deserializer name based on given TypeMirror.
     * <p>
     * The package, containing the deserializer is NOT returned as part of the result.
     *
     * @param type TypeMirror of the bean, deserializerr corresponds to
     * @return deserializer name as String
     */
    private String deserializerName(TypeMirror type) {
        return Type.stringifyType(type) + "BeanJsonDeserializerImpl()";
    }

    /**
     * Create serializer name based on given TypeMirror.
     * <p>
     * The package, containing the serializer is NOT returned as part of the result.
     *
     * @param type TypeMirror of the bean, serializerr corresponds to
     * @return serializer name as String
     */
    private String serializerName(TypeMirror type) {
        return Type.stringifyType(type) + "BeanJsonSerializerImpl()";
    }

    /**
     * <p>getSuperClass.</p>
     *
     * @return a {@link java.lang.Class} object.
     */
    protected abstract Class<?> getSuperClass();

    /**
     * <p>getMapperMethods.</p>
     *
     * @param element a {@link javax.lang.model.element.Element} object.
     * @param type    a {@link javax.lang.model.type.TypeMirror} object.
     * @return a {@link java.lang.Iterable} object.
     */
    protected abstract Iterable<MethodSpec> getMapperMethods(Element element, TypeMirror type);

    /**
     * <p>generateJsonMappers.</p>
     * <p>
     * Creates  mapper implementation for given beanType. If beanType is generic type, create
     * corresponding mappers for type arguments incl. their type arguments as well. All type
     * parameters will be included as part of the name of the mapper implementation.
     * <p>
     * Note that mappers are not generated for "simple" data types, since their implementation
     * already exist.
     *
     * @param beanType    a {@link javax.lang.model.type.TypeMirror} object.
     * @param packageName a {@link java.lang.String} object.
     */

    private void generateJsonMappers(TypeMirror beanType, String packageName, Name beanName) {
        // Root object for ObjectMapper can not have wildcards and/or type parameter.
        // TypeToken (and JsonRegistry) works only with declared types.
        if (Type.hasWildcards(beanType) || Type.hasTypeParameter(beanType)) {
            throw new IllegalArgumentException("Can not create mapper for type with wildcards or type parameters");
        }

        if (beanType.getKind() == TypeKind.DECLARED
                && !Type.isBasicType(typeUtils.erasure(beanType))) {
            generateDeserializer(beanType, packageName);
            generateSerializer(beanType, packageName);

        }
    }

    /**
     * Generate serializer for given beanType and packageName
     *
     * @param beanType
     * @param packageName
     */
    protected void generateSerializer(TypeMirror beanType, String packageName) {
    }

    /**
     * Generate deserializer for given beanType and packageName
     *
     * @param beanType
     * @param packageName
     */
    protected void generateDeserializer(TypeMirror beanType, String packageName) {
    }
}