package org.firezenk.naviganto.processor;

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

import org.firezenk.naviganto.annotations.RoutableActivity;
import org.firezenk.naviganto.annotations.RoutableView;
import org.firezenk.naviganto.processor.exceptions.NotEnoughParametersException;
import org.firezenk.naviganto.processor.exceptions.ParameterNotFoundException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import javax.annotation.Generated;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
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.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.MirroredTypesException;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;

/**
 * Project: Naviganto
 *
 * Created by Jorge Garrido Oval on 04/11/2016.
 * Copyright © Jorge Garrido Oval 2016
 */
@AutoService(Processor.class)
public class RouteProcessor extends AbstractProcessor {

    private Filer filer;
    private Messager messager;

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

    @Override public Set<String> getSupportedAnnotationTypes() {
        final Set<String> set = new HashSet<>();
        set.add(RoutableActivity.class.getCanonicalName());
        set.add(RoutableView.class.getCanonicalName());
        return Collections.unmodifiableSet(set);
    }

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

    @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        for (Element element :  env.getElementsAnnotatedWith(RoutableActivity.class)) {
            JavaFile javaFile = this.generateRoute((TypeElement) element);
            try {
                javaFile.writeTo(filer);
            } catch (Exception e) {
                messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
                e.printStackTrace();
            }
        }

        for (Element element :  env.getElementsAnnotatedWith(RoutableView.class)) {
            JavaFile javaFile = this.generateRoute((TypeElement) element);
            try {
                javaFile.writeTo(filer);
            } catch (Exception e) {
                messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
                e.printStackTrace();
            }
        }

        return true;
    }

    private JavaFile generateRoute(TypeElement typeElement) {
        messager.printMessage(Diagnostic.Kind.NOTE, "Creating route...");

        final ArrayList<MethodSpec> methods = new ArrayList<>();

        methods.add(this.addRouteMethod(typeElement));

        final TypeSpec myClass = this.createRoute(typeElement, methods);

        messager.printMessage(Diagnostic.Kind.NOTE, "Save location: " +
                typeElement.getQualifiedName().toString().replace("."+typeElement.getSimpleName(), ""));

        return JavaFile.builder(
                typeElement.getQualifiedName().toString().replace("."+typeElement.getSimpleName(), ""),
                myClass).build();
    }

    private MethodSpec addRouteMethod(TypeElement typeElement) {
        messager.printMessage(Diagnostic.Kind.NOTE, "Generating route method");

        boolean isActivity;
        int requestCode;
        List<TypeMirror> params;

        if (isActivity = (typeElement.getAnnotation(RoutableActivity.class) != null)) {
            requestCode = this.getForResult(typeElement.getAnnotation(RoutableActivity.class));
            params = this.getParameters(typeElement.getAnnotation(RoutableActivity.class));
        } else {
            requestCode = this.getForResult(typeElement.getAnnotation(RoutableView.class));
            params = this.getParameters(typeElement.getAnnotation(RoutableView.class));
        }

        final StringBuilder sb = new StringBuilder();

        sb.append("" +
                "  if (parameters.length < " + params.size() + ") {\n" +
                "      throw new NotEnoughParametersException(\"Need " + params.size() + " params\");\n" +
                "  }\n");

        int i = 0;
        for (TypeMirror tm : params) {
            sb.append("" +
                    "  if (parameters[" + i + "] == null || !(parameters[" + i + "] instanceof " + tm.toString() + ")) {\n" +
                    "      throw new ParameterNotFoundException(\"Need " + tm.toString() + "\");\n" +
                    "  }\n");
            ++i;
        }

        sb.append("\n");

        if (isActivity) {
            sb.append("  final android.content.Intent intent = new android.content.Intent((android.content.Context) context, " + typeElement.getSimpleName() + ".class);\n");

            sb.append("  android.os.Bundle bundle = new android.os.Bundle();\n\n");
            sb.append("  bundle.putString(\"uuid\", uuid.toString());\n");

            if (params.size() > 0) {
                sb.append(this.parametersToBundle(params));
            }

            sb.append("  intent.putExtras(bundle);\n");

            sb.append("" +
                    "  if (context instanceof android.app.Activity) {\n" +
                    "      ((android.app.Activity) context).startActivityForResult(intent, "+ requestCode +");\n" +
                    "  } else if (context instanceof android.content.Context) {\n" +
                    "      ((android.content.Context) context).startActivity(intent);\n" +
                    "  }\n");
        } else {
            sb.append("" +
                    "  if (viewParent == null || !(viewParent instanceof android.view.ViewGroup)) {\n" +
                    "      throw new ParameterNotFoundException(\"Need a view parent or is not a ViewGroup\");\n" +
                    "  }\n");

            if (params.size() > 0) {
                sb.append("" +
                        "  ((android.view.ViewGroup) viewParent).removeAllViews();\n" +
                        "  ((android.view.ViewGroup) viewParent).addView(" + typeElement.getSimpleName() + ".newInstance((android.content.Context) context, uuid" + this.parametersToString(params) + "));\n");
            } else {
                sb.append("" +
                        "  ((android.view.ViewGroup) viewParent).removeAllViews();\n" +
                        "  ((android.view.ViewGroup) viewParent).addView(" + typeElement.getSimpleName() + ".newInstance((android.content.Context) context, uuid));\n");
            }
        }

        messager.printMessage(Diagnostic.Kind.NOTE, sb.toString());

        return MethodSpec.methodBuilder("route")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addException(ParameterNotFoundException.class)
                .addException(NotEnoughParametersException.class)
                .returns(void.class)
                .addParameter(Object.class, "context")
                .addParameter(UUID.class, "uuid")
                .addParameter(Object[].class, "parameters")
                .addParameter(Object.class, "viewParent")
                .addCode(sb.toString())
                .build();
    }

    private String parametersToString(List<TypeMirror> params) {
        final StringBuilder sb = new StringBuilder();

        int i = 0;
        for (TypeMirror tm : params) {
            sb.append(", (" + tm.toString() + ") parameters[" + i + "]");
            ++i;
        }

        return sb.toString();
    }

    private String parametersToBundle(List<TypeMirror> params) {
        final StringBuilder sb = new StringBuilder();

        int i = 0;
        for (TypeMirror tm : params) {

            final String extra = "parameters[" + i + "]);\n";
            final String casting = "(" + tm.toString() + ") ";

            switch (tm.toString()) {
                case "java.lang.Boolean":
                    sb.append("  bundle.putBoolean(\"bool" + i + "\", ");
                    sb.append(casting);
                    sb.append(extra);
                    break;
                case "java.lang.Integer":
                    sb.append("  bundle.putInt(\"int" + i + "\", ");
                    sb.append(casting);
                    sb.append(extra);
                    break;
                case "java.lang.Character":
                    sb.append("  bundle.putChar(\"char" + i + "\", ");
                    sb.append(casting);
                    sb.append(extra);
                    break;
                case "java.lang.Float":
                    sb.append("  bundle.putFloat(\"float" + i + "\", ");
                    sb.append(casting);
                    sb.append(extra);
                    break;
                case "java.lang.Double":
                    sb.append("  bundle.putDouble(\"double" + i + "\", ");
                    sb.append(casting);
                    sb.append(extra);
                    break;
                case "java.lang.String":
                    sb.append("  bundle.putString(\"string" + i + "\", ");
                    sb.append(casting);
                    sb.append(extra);
                    break;
                default:
                    sb.append("  bundle.putParcelable(\"parcelable" + i + "\", (android.os.Parcelable) " + extra);
                    break;
            }

            sb.append("\n");
            ++i;
        }

        return sb.toString();
    }

    private TypeSpec createRoute(TypeElement typeElement, ArrayList<MethodSpec> methods) {
        messager.printMessage(Diagnostic.Kind.NOTE, "Saving route file...");
        return TypeSpec.classBuilder(typeElement.getSimpleName() + "Route")
                .addAnnotation(AnnotationSpec.builder(Generated.class)
                        .addMember("value", "$S", this.getClass().getName())
                        .build())
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(org.firezenk.naviganto.processor.interfaces.Routable.class)
                .addMethods(methods)
                .build();
    }

    @SuppressWarnings("unchecked") private List<TypeMirror> getParameters(RoutableActivity annotation) {
        try {
            annotation.params(); // TODO get forResult
        } catch (MirroredTypesException e) {
            return (List<TypeMirror>) e.getTypeMirrors();
        }
        return null;
    }

    @SuppressWarnings("unchecked") private List<TypeMirror> getParameters(RoutableView annotation) {
        try {
            annotation.params(); // TODO get forResult
        } catch (MirroredTypesException e) {
            return (List<TypeMirror>) e.getTypeMirrors();
        }
        return null;
    }

    private int getForResult(RoutableActivity annotation) {
        return annotation.requestCode();
    }

    private int getForResult(RoutableView annotation) {
        return annotation.requestCode();
    }
}