package com.github.florent37.autoasync.processor;

import com.github.florent37.autoasync.processor.holders.ActivityHolder;
import com.github.florent37.autoasync.processor.holders.ApplicationHolder;
import com.github.florent37.autoasync.processor.holders.FragmentHolder;
import com.github.florent37.daggerautoinject.InjectActivity;
import com.github.florent37.daggerautoinject.InjectApplication;
import com.github.florent37.daggerautoinject.InjectFragment;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.HashMap;
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.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;

@SupportedAnnotationTypes({
        "com.github.florent37.daggerautoinject.InjectActivity",
        "com.github.florent37.daggerautoinject.InjectFragment",
        "com.github.florent37.daggerautoinject.InjectApplication"
})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@AutoService(javax.annotation.processing.Processor.class)
public class DaggerAutoInjectProcessor extends AbstractProcessor {

    private Map<ClassName, ActivityHolder> activityHolders = new HashMap<>();
    private Map<ClassName, FragmentHolder> fragmentHolders = new HashMap<>();
    private ApplicationHolder applicationHolder;
    private Filer filer;

    private static TypeMirror getComponent(InjectApplication annotation) {
        try {
            annotation.component(); // this should throw
        } catch (MirroredTypeException mte) {
            return mte.getTypeMirror();
        }
        return null; // can this ever happen ??
    }

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        filer = env.getFiler();
    }

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

        if(applicationHolder != null) {
            writeHoldersOnJavaFile();
        }
        return true;
    }

    protected void processAnnotations(RoundEnvironment env) {
        for (Element element : env.getElementsAnnotatedWith(InjectActivity.class)) {
            final ClassName classFullName = ClassName.get((TypeElement) element); //com.github.florent37.sample.TutoAndroidFrance
            final String className = element.getSimpleName().toString(); //TutoAndroidFrance
            activityHolders.put(classFullName, new ActivityHolder(element, classFullName, className));
        }

        for (Element element : env.getElementsAnnotatedWith(InjectFragment.class)) {
            final ClassName classFullName = ClassName.get((TypeElement) element); //com.github.florent37.sample.TutoAndroidFrance
            final String className = element.getSimpleName().toString(); //TutoAndroidFrance
            fragmentHolders.put(classFullName, new FragmentHolder(element, classFullName, className));
        }

        for (Element element : env.getElementsAnnotatedWith(InjectApplication.class)) {
            final ClassName classFullName = ClassName.get((TypeElement) element); //com.github.florent37.sample.TutoAndroidFrance
            final String className = element.getSimpleName().toString(); //TutoAndroidFrance

            final TypeMirror componentClass = getComponent(element.getAnnotation(InjectApplication.class));

            applicationHolder = new ApplicationHolder(element, classFullName, className);
            applicationHolder.setComponentClass(componentClass);
        }
    }

    protected void writeHoldersOnJavaFile() {
        constructActivityModule();
        constructFragmentModule();
        construct();

        fragmentHolders.clear();
        activityHolders.clear();
        applicationHolder = null;
    }

    private void constructActivityModule() {
        final TypeSpec.Builder builder = TypeSpec.classBuilder(Constants.ACTIVITY_MODULE)
                .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
                .addAnnotation(Constants.DAGGER_MODULE);

        for (ActivityHolder activityHolder : activityHolders.values()) {
            builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_CONTRIBUTE + activityHolder.className)
                    .addAnnotation(Constants.DAGGER_ANDROID_ANNOTATION)
                    .addModifiers(Modifier.ABSTRACT)
                    .returns(activityHolder.classNameComplete)
                    .build()
            );
        }

        final TypeSpec newClass = builder.build();
        final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();

        try {
            javaFile.writeTo(System.out);
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void constructFragmentModule() {
        final TypeSpec.Builder builder = TypeSpec.classBuilder(Constants.FRAGMENT_MODULE)
                .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
                .addAnnotation(Constants.DAGGER_MODULE);

        for (FragmentHolder fragmentHolder : fragmentHolders.values()) {
            builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_CONTRIBUTE + fragmentHolder.className)
                    .addAnnotation(Constants.DAGGER_ANDROID_ANNOTATION)
                    .addModifiers(Modifier.ABSTRACT)
                    .returns(fragmentHolder.classNameComplete)
                    .build()
            );
        }

        final TypeSpec newClass = builder.build();
        final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();

        try {
            javaFile.writeTo(System.out);
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void construct() {

        final TypeSpec.Builder builder = TypeSpec.classBuilder(Constants.MAIN_CLASS_NAME)
                .addModifiers(Modifier.PUBLIC);

        builder.addField(FieldSpec.builder(ClassName.get(String.class), "TAG", Modifier.FINAL, Modifier.PUBLIC, Modifier.STATIC)
                .initializer("\"" + Constants.MAIN_CLASS_NAME + "\"").build());

        builder.addMethod(
                MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PRIVATE)
                        .build()
        );

        //final ClassName daggerComponent = findDaggerComponent(applicationHolder.componentClass);
        final ClassName component = findComponent(applicationHolder.componentClass);

        {
            final MethodSpec.Builder methodInit = MethodSpec.methodBuilder(Constants.METHOD_INIT)
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC);
            if (applicationHolder != null) {

                methodInit
                        .addParameter(applicationHolder.classNameComplete, Constants.PARAM_APPLICATION)
                        .addParameter(component, Constants.PARAM_COMPONENT);

                    methodInit.addStatement("$L.inject($L)", Constants.PARAM_COMPONENT, Constants.PARAM_APPLICATION);
            }


            methodInit.addStatement("application.registerActivityLifecycleCallbacks(new $T.ActivityLifecycleCallbacks() {\n" +
                            "            @Override\n" +
                            "            public void onActivityCreated($T activity, $T savedInstanceState) {\n" +
                            "                " + Constants.METHOD_HANDLE_ACTIVITY + "(activity);\n" +
                            "            }\n" +
                            "\n" +
                            "            @Override\n" +
                            "            public void onActivityStarted(Activity activity) {\n" +
                            "\n" +
                            "            }\n" +
                            "\n" +
                            "            @Override\n" +
                            "            public void onActivityResumed(Activity activity) {\n" +
                            "\n" +
                            "            }\n" +
                            "\n" +
                            "            @Override\n" +
                            "            public void onActivityPaused(Activity activity) {\n" +
                            "\n" +
                            "            }\n" +
                            "\n" +
                            "            @Override\n" +
                            "            public void onActivityStopped(Activity activity) {\n" +
                            "\n" +
                            "            }\n" +
                            "\n" +
                            "            @Override\n" +
                            "            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {\n" +
                            "\n" +
                            "            }\n" +
                            "\n" +
                            "            @Override\n" +
                            "            public void onActivityDestroyed(Activity activity) {\n" +
                            "\n" +
                            "            }\n" +
                            "        });",

                    Constants.APPLICATION,
                    Constants.ACTIVITY,
                    Constants.BUNDLE
            );

            builder.addMethod(methodInit.build());
        }

        /*
        {
            final MethodSpec.Builder methodInit = MethodSpec.methodBuilder(Constants.METHOD_INIT)
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC);
            if (applicationHolder != null) {
                methodInit
                        .addParameter(applicationHolder.classNameComplete, Constants.PARAM_APPLICATION);

                if (applicationHolder.componentClass != null) {
                    methodInit.addCode("$T component = $T.builder()\n" +
                                    "                .application(" + Constants.PARAM_APPLICATION + ")\n" +
                                    "                .build();\n",
                            component, daggerComponent);
                    methodInit.addStatement("$L($L, $L)", Constants.METHOD_INIT, Constants.PARAM_APPLICATION, Constants.PARAM_COMPONENT);
                }
            }
            builder.addMethod(methodInit.build());
        }
        */

        {
            final MethodSpec.Builder methodHandleActivity = MethodSpec.methodBuilder(Constants.METHOD_HANDLE_ACTIVITY)
                    .addModifiers(Modifier.PROTECTED, Modifier.STATIC)
                    .addParameter(Constants.ACTIVITY, "activity");

            methodHandleActivity.addCode("try {\n" +
                            "            $T.inject(activity);\n" +
                            "        } catch (Exception e){\n" +
                            "            $T.d(TAG, activity.getClass().toString()+\" non injected\");\n" +
                            "        }\n" +
                            "        if (activity instanceof $T) {\n" +
                            "            final $T supportFragmentManager = ((FragmentActivity) activity).getSupportFragmentManager();\n" +
                            "            supportFragmentManager.registerFragmentLifecycleCallbacks(\n" +
                            "                    new FragmentManager.FragmentLifecycleCallbacks() {\n" +
                            "                        @Override\n" +
                            "                        public void onFragmentCreated(FragmentManager fm, $T f, $T savedInstanceState) {\n" +
                            "                            try {\n" +
                            "                                $T.inject(f);\n" +
                            "                            } catch (Exception e){\n" +
                            "                                Log.d(TAG, f.getClass().toString()+\" non injected\");\n" +
                            "                            }\n" +
                            "                        }\n" +
                            "                    }, true);\n" +
                            "        }",

                    Constants.ANDROID_INJECTION,
                    Constants.ANDROID_LOG,
                    Constants.FRAGMENT_ACTIVITY,
                    Constants.FRAGMENT_MANAGER,
                    Constants.FRAGMENT,
                    Constants.BUNDLE,
                    Constants.ANDROID_SUPPORT_INJECTION
            );

            builder.addMethod(methodHandleActivity.build());
        }

        final TypeSpec newClass = builder.build();

        final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();

        try {
            javaFile.writeTo(System.out);
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ClassName findDaggerComponent(TypeMirror typeMirror) {
        final ClassName typeName = (ClassName) TypeName.get(typeMirror);
        final String packageName = typeName.packageName();
        final String className = typeName.simpleName();
        return ClassName.bestGuess(packageName + "." + Constants.DAGGER + className);
    }

    private ClassName findComponent(TypeMirror typeMirror) {
        final ClassName typeName = (ClassName) TypeName.get(typeMirror);
        final String packageName = typeName.packageName();
        final String className = typeName.simpleName();
        return ClassName.bestGuess(packageName + "." + className);
    }

}