package com.jparkie.hawleyretainer;

import android.app.Activity;

import com.jparkie.hawleyretainer.internal.Retainer;
import com.jparkie.hawleyretainer.internal.RetainerFragmentMap;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import javax.annotation.processing.Filer;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeMirror;

/*
 * Example:
 *
 * package com.jparkie.example;
 *
 * import android.app.Activity;
 * import com.jparkie.hawleyretainer.internal.Retainer;
 * import com.jparkie.hawleyretainer.internal.RetainerFragmentMap;
 * import java.io.File;
 * import java.lang.Override;
 * import rx.Observable;
 *
 * public class ExampleActivity$$HawleyRetainer<T extends com.jparkie.example.ExampleActivity> extends Retainer.Object<T> {
 *  @Override
 *  public void restoreRetainedObjectMap(T target, Activity activity) {
 *      final RetainerFragmentMap retainedMap = RetainerFragmentMap.<T>findOrCreateRetainerFragmentMap(target, activity);
 *      if (target.mObservable == null) {
 *          if (retainedMap.containsKey("mObservable")) {
 *              target.mObservable = (Observable<File>)retainedMap.get("mObservable");
 *          }
 *      }
 *  }
 *
 *  @Override
 *  public void saveRetainedObjectMap(T target, Activity activity) {
 *      final RetainerFragmentMap retainedMap = RetainerFragmentMap.<T>findOrCreateRetainerFragmentMap(target, activity);
 *      if (target.mObservable != null) {
 *          retainedMap.put("mObservable", target.mObservable);
 *      }
 *  }
 * }
 */
public final class HawleyRetainerDentist {
    public static final String TAG = HawleyRetainerDentist.class.getSimpleName();

    private final String mClassPackage;
    private final String mClassName;
    private final String mClassTarget;

    private String mClassParent;

    private final List<FieldBinding> mFieldBindings;

    public HawleyRetainerDentist(String classPackage, String className, String classTarget) {
        mClassPackage = classPackage;
        mClassName = className;
        mClassTarget = classTarget;

        mFieldBindings = new LinkedList<>();
    }

    public void setClassParent(String classParent) {
        mClassParent = classParent;
    }

    public void addFieldBinding(Element element) {
        final String name = element.getSimpleName().toString();
        final TypeMirror type = element.asType();

        final FieldBinding fieldBinding = new FieldBinding(name, type);

        mFieldBindings.add(fieldBinding);
    }

    public void writeToFiler(Filer filer) throws IOException {
        final ClassName targetClassName = ClassName.get(mClassPackage, mClassTarget);

        final TypeSpec.Builder builder = TypeSpec.classBuilder(mClassName)
                .addModifiers(Modifier.PUBLIC)
                .addTypeVariable(TypeVariableName.get("T", targetClassName))
                .addMethod(generateRestoreRetainedObjectMapMethod())
                .addMethod(generateSaveRetainedObjectMapMethod());

        if (mClassParent != null) {
            builder.superclass(ParameterizedTypeName.get(ClassName.bestGuess(mClassParent), TypeVariableName.get("T")));
        } else {
            builder.superclass(ParameterizedTypeName.get(ClassName.get(Retainer.Object.class), TypeVariableName.get("T")));
        }

        final JavaFile retainerFile = JavaFile.builder(mClassPackage, builder.build()).build();

        retainerFile.writeTo(filer);
    }

    private MethodSpec generateRestoreRetainedObjectMapMethod() {
        final MethodSpec.Builder builder = MethodSpec.methodBuilder("restoreRetainedObjectMap")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .returns(void.class)
                .addParameter(TypeVariableName.get("T"), "target")
                .addParameter(Activity.class, "activity");

        if (mClassParent != null) {
            builder.addStatement("super.restoreRetainedObjectMap(target, activity)");
        }

        builder.addStatement("final $T retainedMap = RetainerFragmentMap.<$T>findOrCreateRetainerFragmentMap(target, activity)", ClassName.get(RetainerFragmentMap.class), TypeVariableName.get("T"));

        for (FieldBinding fieldBinding : mFieldBindings) {
            builder.beginControlFlow("if (retainedMap.containsKey($S))", fieldBinding.mName)
                    .addStatement("target.$N = ($T)retainedMap.get($S)", fieldBinding.mName, fieldBinding.mType, fieldBinding.mName)
                    .endControlFlow();
        }

        return builder.build();
    }

    private MethodSpec generateSaveRetainedObjectMapMethod() {
        final MethodSpec.Builder builder = MethodSpec.methodBuilder("saveRetainedObjectMap")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .returns(void.class)
                .addParameter(TypeVariableName.get("T"), "target")
                .addParameter(Activity.class, "activity");

        if (mClassParent != null) {
            builder.addStatement("super.saveRetainedObjectMap(target, activity)");
        }

        builder.addStatement("final $T retainedMap = RetainerFragmentMap.<$T>findOrCreateRetainerFragmentMap(target, activity)", ClassName.get(RetainerFragmentMap.class), TypeVariableName.get("T"));

        for (FieldBinding fieldBinding : mFieldBindings) {
            builder.beginControlFlow("if (target.$N != null)", fieldBinding.mName)
                    .addStatement("retainedMap.put($S, target.$N)", fieldBinding.mName, fieldBinding.mName)
                    .endControlFlow();
        }

        return builder.build();
    }

    public static final class FieldBinding {
        private final String mName;
        private final TypeMirror mType;

        public FieldBinding(String name, TypeMirror type) {
            mName = name;
            mType = type;
        }
    }
}