package okdeeplink;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import okdeeplink.util.ElementUtils;
import okdeeplink.util.Logger;

import static com.squareup.javapoet.MethodSpec.methodBuilder;
import static javax.lang.model.element.ElementKind.CLASS;
import static javax.lang.model.element.Modifier.PUBLIC;

@AutoService(Processor.class)
public class DeepLinkInjectProcessor extends AbstractProcessor {

    private static final String PACKAGE_NAME = DeepLinkInjectProcessor.class.getPackage().getName();

    private static final ClassName BUNDLE = ClassName.get("android.os", "Bundle");
    private static final ClassName INTENT = ClassName.get("android.content", "Intent");
    private static final ClassName BUNDLE_COMPACT = ClassName.get(PACKAGE_NAME, "BundleCompact");

    private static final String ACTIVITY = "android.app.Activity";


    private static final String INJECTOR_SUFFIX = "$$Injector";

    private static final List<String> SUPPORT_INJECT = new ArrayList<>();

    static {
        SUPPORT_INJECT.add(ACTIVITY);
        SUPPORT_INJECT.add("android.app.Fragment");
        SUPPORT_INJECT.add("android.support.v4.app.Fragment");
    }


    private Filer filer;
    private Logger logger;

    private Map<TypeElement, List<Element>> targetInjectElements;
    private Elements elements;
    private Types types;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        logger = new Logger(processingEnv.getMessager());
        filer = processingEnv.getFiler();
        elements = processingEnv.getElementUtils();
        types = processingEnv.getTypeUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> ret = new HashSet<>();
        ret.add(Query.class.getCanonicalName());
        ret.add(Service.class.getCanonicalName());
        return ret;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        List<Element> injectElements = new ArrayList<>();
        List<Element> queryElements = generateQueryElements(roundEnv);
        List<Element> serviceElements = generateServiceElements(roundEnv);
        injectElements.addAll(queryElements);
        injectElements.addAll(serviceElements);
        if (ElementUtils.isEmpty(injectElements)) {
            return false;
        }
        targetInjectElements = findInjectElements(injectElements);
        if (ElementUtils.isEmpty(targetInjectElements)) {
            return false;
        }
        for (Map.Entry<TypeElement, List<Element>> injectElementEntrySet : targetInjectElements.entrySet()) {
            TypeElement targetElement = injectElementEntrySet.getKey();
            List<Element> fieldElements = injectElementEntrySet.getValue();

            MethodSpec injectQueryMethod = geneOnCreateQueryMethod(targetElement, fieldElements);

            MethodSpec injectServiceMethod = geneInjectServiceMethod(targetElement, fieldElements);

            MethodSpec saveInstanceMethod = geneSaveInstanceMethod(targetElement, fieldElements);


            MethodSpec newIntentMethod = geneOnNewIntentQueryMethod(targetElement, fieldElements);

            String fileName = targetElement.getSimpleName() + INJECTOR_SUFFIX;
            TypeSpec.Builder helper = TypeSpec.classBuilder(fileName)
                    .addModifiers(PUBLIC)
                    .addAnnotation(AnnotationSpec.builder(Aspect.class).build())
                    .addMethod(injectQueryMethod)
                    .addMethod(injectServiceMethod)
                    .addMethod(saveInstanceMethod);

            TypeMirror typeMirror = elements.getTypeElement(ACTIVITY).asType();
            if (types.isSubtype(targetElement.asType(), typeMirror)) {
                helper.addMethod(newIntentMethod);
            }

            try {
                PackageElement packageElement = (PackageElement) targetElement.getEnclosingElement();
                JavaFile.builder(packageElement.getQualifiedName().toString(), helper.build()).build().writeTo(filer);
            } catch (IOException e) {
                logger.error("Error creating inject file", targetElement);
            }

        }
        return true;
    }

    private MethodSpec geneOnCreateQueryMethod(TypeElement targetElement, List<Element> fieldElements) {

        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("onCreate")
                .addModifiers(PUBLIC)
                .addException(Throwable.class)
                .addAnnotation(AnnotationSpec.builder(Around.class).addMember("value", "$S", "execution(* " + targetElement.getQualifiedName() + ".onCreate(..))").build())
                .addParameter(ProceedingJoinPoint.class, "joinPoint");

        CodeBlock.Builder injectQueryCodeBuilder = geneOnCreateCodeBuilder(targetElement);

        List<String> queryKeys = new ArrayList<>();

        for (Element queryElement : fieldElements) {
            if (queryElement.getAnnotation(Query.class) != null) {
                String queryKey = queryElement.getAnnotation(Query.class).value();
                if (queryKeys.contains(queryKey)) {
                    logger.error("The inject query key cannot be Duplicate" + Query.class.getCanonicalName(), queryElement);
                }
                queryKeys.add(queryKey);

                injectQueryCodeBuilder
                        .beginControlFlow("try")
                        .add("target.$L= $T.getValue(dataBundle,$S,$T.class);\n", queryElement, BUNDLE_COMPACT, queryKey, queryElement)
                        .nextControlFlow("catch ($T e)", Exception.class)
                        .addStatement("e.printStackTrace()")
                        .endControlFlow();

            }
        }
        injectQueryCodeBuilder.add("joinPoint.proceed();\n");
        injectMethodBuilder.addCode(injectQueryCodeBuilder.build());

        return injectMethodBuilder.build();
    }


    private MethodSpec geneOnNewIntentQueryMethod(TypeElement targetElement, List<Element> fieldElements) {

        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("onNewIntent")
                .addModifiers(PUBLIC)
                .addException(Throwable.class)
                .addAnnotation(AnnotationSpec.builder(Around.class).addMember("value", "$S", "execution(* " + targetElement.getQualifiedName() + ".onNewIntent(..))").build())
                .addParameter(ProceedingJoinPoint.class, "joinPoint");

        CodeBlock.Builder injectQueryCodeBuilder = geneOnNewIntentCodeBuilder(targetElement);

        List<String> queryKeys = new ArrayList<>();

        for (Element queryElement : fieldElements) {
            if (queryElement.getAnnotation(Query.class) != null) {
                String queryKey = queryElement.getAnnotation(Query.class).value();
                if (queryKeys.contains(queryKey)) {
                    logger.error("The inject query key cannot be Duplicate" + Query.class.getCanonicalName(), queryElement);
                }
                queryKeys.add(queryKey);

                injectQueryCodeBuilder
                        .beginControlFlow("try")
                        .add("target.$L= $T.getValue(dataBundle,$S,$T.class);\n", queryElement, BUNDLE_COMPACT, queryKey, queryElement)
                        .nextControlFlow("catch ($T e)", Exception.class)
                        .addStatement("e.printStackTrace()")
                        .endControlFlow();

            }
        }
        injectQueryCodeBuilder.add("joinPoint.proceed();\n");
        injectMethodBuilder.addCode(injectQueryCodeBuilder.build());

        return injectMethodBuilder.build();
    }


    private MethodSpec geneInjectServiceMethod(TypeElement targetElement, List<Element> fieldElements) {

        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("onCreateService")
                .addModifiers(PUBLIC)
                .addException(Throwable.class)
                .addAnnotation(AnnotationSpec.builder(Around.class).addMember("value", "$S", "execution(* " + targetElement.getQualifiedName() + ".onCreate(..))").build())
                .addParameter(ProceedingJoinPoint.class, "joinPoint");

        CodeBlock.Builder injectServiceCodeBuilder = CodeBlock.builder();
        injectServiceCodeBuilder.add("$T target = ($T)joinPoint.getTarget();\n", targetElement, targetElement);
        List<TypeName> serviceTypeNames = new ArrayList<>();

        for (Element queryElement : fieldElements) {
            if (queryElement.getAnnotation(Service.class) != null) {
                TypeName className = TypeName.get(queryElement.asType());
                if (serviceTypeNames.contains(className)) {
                    logger.error("The inject " + className + " cannot be Duplicate", queryElement);
                }
                serviceTypeNames.add(className);
                injectServiceCodeBuilder.add("target.$L=  new $T(target).build($T.class);\n ", queryElement, DeepLinkServiceProcessor.DEEP_LINK_CLIENT, queryElement);

            }
        }
        injectServiceCodeBuilder.add("joinPoint.proceed();\n");
        injectMethodBuilder.addCode(injectServiceCodeBuilder.build());

        return injectMethodBuilder.build();
    }


    private MethodSpec geneSaveInstanceMethod(TypeElement targetElement, List<Element> fieldElements) {

        MethodSpec.Builder saveInstanceMethodBuilder = methodBuilder("onSaveInstanceState")
                .addModifiers(PUBLIC)
                .addException(Throwable.class)
                .addAnnotation(AnnotationSpec
                        .builder(After.class)
                        .addMember("value", "$S", "execution(* " + targetElement.getQualifiedName() + ".onSaveInstanceState(..))")
                        .build())
                .addParameter(JoinPoint.class, "joinPoint");

        CodeBlock.Builder saveInstanceCodeBuilder = geneSaveInstanceCodeBuilder(targetElement);

        List<String> queryKeys = new ArrayList<>();

        for (Element queryElement : fieldElements) {
            if (queryElement.getAnnotation(Query.class) != null) {

                String queryKey = queryElement.getAnnotation(Query.class).value();
                if (queryKeys.contains(queryKey)) {
                    logger.error("The inject query key cannot be Duplicate" + Query.class.getCanonicalName(), queryElement);
                }
                queryKeys.add(queryKey);
                saveInstanceCodeBuilder.add("intent.putExtra($S,target.$L);\n", queryKey, queryElement);
            }
        }

        saveInstanceCodeBuilder.add("saveBundle.putAll(intent.getExtras());\n");

        saveInstanceMethodBuilder.addCode(saveInstanceCodeBuilder.build());

        return saveInstanceMethodBuilder.build();
    }

    private CodeBlock.Builder geneSaveInstanceCodeBuilder(TypeElement targetElement) {
        CodeBlock.Builder blockBuilderSave = CodeBlock.builder();
        blockBuilderSave.add("$T target = ($T)joinPoint.getTarget();\n", targetElement, targetElement);
        blockBuilderSave.add("$T saveBundle = ($T)joinPoint.getArgs()[0];\n", BUNDLE, BUNDLE);
        blockBuilderSave.add("$T intent = new $T();\n", INTENT, INTENT);
        return blockBuilderSave;
    }

    private CodeBlock.Builder geneOnCreateCodeBuilder(TypeElement targetElement) {
        CodeBlock.Builder injectQueryCodeBuilder = CodeBlock.builder();
        injectQueryCodeBuilder.add("$T target = ($T)joinPoint.getTarget();\n", targetElement, targetElement);
        injectQueryCodeBuilder.add("$T dataBundle = new $T();\n", BUNDLE, BUNDLE);
        injectQueryCodeBuilder.add("$T saveBundle = ($T)joinPoint.getArgs()[0];\n", BUNDLE, BUNDLE);
        injectQueryCodeBuilder.add("$T targetBundle = $T.getSupportBundle(target);\n", BUNDLE, BUNDLE_COMPACT);
        injectQueryCodeBuilder.beginControlFlow("if(targetBundle != null)");
        injectQueryCodeBuilder.add("dataBundle.putAll(targetBundle);\n");
        injectQueryCodeBuilder.endControlFlow();
        injectQueryCodeBuilder.beginControlFlow("if(saveBundle != null)");
        injectQueryCodeBuilder.add("dataBundle.putAll(saveBundle);\n");
        injectQueryCodeBuilder.endControlFlow();
        return injectQueryCodeBuilder;
    }

    private CodeBlock.Builder geneOnNewIntentCodeBuilder(TypeElement targetElement) {
        CodeBlock.Builder injectQueryCodeBuilder = CodeBlock.builder();
        injectQueryCodeBuilder.add("$T target = ($T)joinPoint.getTarget();\n", targetElement, targetElement);
        injectQueryCodeBuilder.add("$T targetIntent = ($T)joinPoint.getArgs()[0];\n", INTENT, INTENT);
        injectQueryCodeBuilder.add("$T dataBundle = targetIntent.getExtras();\n", BUNDLE);

        return injectQueryCodeBuilder;
    }


    private List<Element> generateQueryElements(RoundEnvironment roundEnv) {
        Set<? extends Element> deepLinkPathElements = roundEnv.getElementsAnnotatedWith(Query.class);
        List<Element> queryElements = new ArrayList<>();
        for (Element element : deepLinkPathElements) {
            Query deepLinkPathAnnotation = element.getAnnotation(Query.class);
            ElementKind kind = element.getKind();
            if (kind != ElementKind.PARAMETER && kind != ElementKind.FIELD) {
                logger.error("Only classes and methods can be annotated with @" + Query.class.getCanonicalName(), element);
            }
            String queryKey = deepLinkPathAnnotation.value();
            if (queryKey == null || queryKey.length() == 0) {
                logger.error("The inject query cannot be null @" + Query.class.getCanonicalName(), element);
            }

            if (kind == ElementKind.FIELD) {
                Element enclosingElement = element.getEnclosingElement();
                if (enclosingElement.getKind() != CLASS) {
                    logger.error("@" + Query.class.getCanonicalName() + "only be contained in classes", element);
                }
                TypeElement typeElement = (TypeElement) enclosingElement;
                boolean support = isSupportInject(typeElement);
                if (!support) {
                    logger.error("@" + Query.class.getCanonicalName() + "only support inject in activity or fragment", element);
                }
                if (element.getModifiers().contains(Modifier.PRIVATE)) {
                    logger.error("The inject query fields can not be private, please check field @" + Query.class.getCanonicalName() + "in class" + typeElement.getQualifiedName(), element);
                }
                queryElements.add(element);
            }
        }
        return queryElements;
    }


    private List<Element> generateServiceElements(RoundEnvironment roundEnv) {
        Set<? extends Element> deepLinkServiceElements = roundEnv.getElementsAnnotatedWith(Service.class);
        List<Element> serviceElements = new ArrayList<>();
        for (Element element : deepLinkServiceElements) {
            serviceElements.add(element);
        }
        return serviceElements;
    }

    private Map<TypeElement, List<Element>> findInjectElements(List<Element> queryInjectElements) {

        Map<TypeElement, List<Element>> map = new HashMap<>();

        for (Element queryInjectElement : queryInjectElements) {
            TypeElement enclosingElement = (TypeElement) queryInjectElement.getEnclosingElement();
            List<Element> builder = map.get(enclosingElement);
            if (builder == null) {
                builder = new ArrayList<>();
                map.put(enclosingElement, builder);
            }
            builder.add(queryInjectElement);
        }
        return map;
    }


    private boolean isSupportInject(TypeElement typeElement) {
        for (String supportClass : SUPPORT_INJECT) {
            TypeMirror typeMirror = elements.getTypeElement(supportClass).asType();
            if (types.isSubtype(typeElement.asType(), typeMirror)) {
                return true;
            }
        }
        return false;
    }


}