package fit.compiler;

import com.google.auto.common.SuperficialValidation;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
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;
import fit.PreferenceIgnore;
import fit.SharedPreferenceAble;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashSet;
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.Name;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;

import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PUBLIC;

@AutoService(Processor.class) public class FitProcessor extends AbstractProcessor {
  private static final ClassName MM = ClassName.get("fit", "MM");
  private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
  private static final ClassName SHARED_PREFERENCES =
      ClassName.get("android.content", "SharedPreferences");
  private static final ClassName SHARED_PREFERENCES_EDITOR =
      ClassName.get("android.content.SharedPreferences", "Editor");
  private static final ClassName UTILS = ClassName.get("fit.internal", "Utils");
  private static final ClassName FILE_OBJECT_UTIL = ClassName.get("fit.internal", "FileObjectUtil");
  private static final ClassName STRING = ClassName.get("java.lang", "String");

  //Set<String>
  TypeName stringTypeName = TypeName.get(String.class);
  ClassName set = ClassName.get("java.util", "Set");
  ClassName hashSet = ClassName.get("java.util", "HashSet");
  TypeName setOfHoverboards = ParameterizedTypeName.get(set, stringTypeName);
  TypeName hashSetOfHoverboards = ParameterizedTypeName.get(hashSet, stringTypeName);

  private static final String METHOD_GET_STRING = "getString";
  private static final String METHOD_GET_Int = "getInt";

  private Elements elementUtils;
  private Filer filer;

  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    elementUtils = env.getElementUtils();
    //typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    //try {
    //  trees = Trees.instance(processingEnv);
    //} catch (IllegalArgumentException ignored) {
    //}
  }

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

    // Process each @SharedPreferenceAble element.
    for (Element element : roundEnv.getElementsAnnotatedWith(SharedPreferenceAble.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds

      TypeElement enclosingElement = (TypeElement) element;

      if (enclosingElement.getKind() != ElementKind.CLASS) {
        throw new RuntimeException("Fit only use class");
      }
      //remove inner class
      if (enclosingElement.getNestingKind() != NestingKind.TOP_LEVEL) {
        throw new RuntimeException("Fit can't use Inner class");
      }
      // Assemble information on the field.

      TypeName targetType = TypeName.get(enclosingElement.asType());

      if (targetType instanceof ParameterizedTypeName) {
        targetType = ((ParameterizedTypeName) targetType).rawType;
      }
      String packageName = getPackageName(enclosingElement);

      try {
        String className = getClassName(enclosingElement, packageName);
        ClassName preferenceClassName = ClassName.get(packageName, className + "_Preference");

        boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
        boolean hasNonParaConstructor = false;
        Set<Element> fieldElements = new HashSet<>();
        Set<Element> privateFieldElements = new HashSet<>();
        Set<Element> suspectedGetterElements = new HashSet<>();
        Set<Element> suspectedSetterElements = new HashSet<>();

        for (Element memberElement : elementUtils.getAllMembers(enclosingElement)) {

          Set<Modifier> modifiers = memberElement.getModifiers();
          //add not static /private field
          final ElementKind kind = memberElement.getKind();
          if (modifiers.contains(Modifier.STATIC)) {
            continue;
          }

          if (kind == ElementKind.FIELD && !modifiers.contains(Modifier.TRANSIENT)) {

            //ignore field
            if (null != memberElement.getAnnotation(PreferenceIgnore.class)) {
              continue;
            }

            if (modifiers.contains(Modifier.PRIVATE)) {
              privateFieldElements.add(memberElement);
            } else {
              fieldElements.add(memberElement);
            }
            continue;
          } else if (kind == ElementKind.METHOD && !modifiers.contains(Modifier.PRIVATE)) {
            Name methodName = memberElement.getSimpleName();
            if ((methodName.contentEquals("getMetaClass") && memberElement.asType()
                .toString()
                .equals("()groovy.lang.MetaClass")) || methodName.contentEquals("getClass")) {
              continue;
            }
            if (isGetter(memberElement)) {
              suspectedGetterElements.add(memberElement);
              continue;
            }
            if (isSetter(memberElement)) {
              suspectedSetterElements.add(memberElement);
              continue;
            }
          } else if (memberElement.getKind() == ElementKind.CONSTRUCTOR && memberElement.toString()
              .equals(className + "()") && !modifiers.contains(Modifier.PRIVATE)) {
            hasNonParaConstructor = true;
          }
        }

        //移除重复属性
        Set<Element> rep = new HashSet<>();
        for (Element field : fieldElements) {
          if (privateFieldElements.isEmpty()) {
            break;
          }
          for (Element privateField : privateFieldElements) {
            if (privateField.getSimpleName().equals(field.getSimpleName())) {
              rep.add(field);
              break;
            }
          }
        }
        fieldElements.removeAll(rep);
        if (!hasNonParaConstructor) {
          throw new RuntimeException("Fit can't use no non-parameter constructor");
        }

        Set<fit.compiler.PropertyDescriptor> getterPropertyDescriptors = new HashSet<>();
        Set<fit.compiler.PropertyDescriptor> setterPropertyDescriptors = new HashSet<>();

        //过滤getter
        Set<Element> getterElements = new HashSet<>();
        for (Element method : suspectedGetterElements) {
          String methodName = method.getSimpleName().toString();
          final String propertyName =
              methodName.startsWith("is") ? methodName.substring(2).toLowerCase()
                  : methodName.substring(3).toLowerCase();
          for (Element field : privateFieldElements) {
            if (field.getSimpleName().toString().equalsIgnoreCase(propertyName)) {
              getterElements.add(method);
              fit.compiler.PropertyDescriptor propertyDescriptor =
                  new fit.compiler.PropertyDescriptor();
              propertyDescriptor.setField(field);
              propertyDescriptor.setGetter(method);
              getterPropertyDescriptors.add(propertyDescriptor);
              break;
            }
          }
        }

        //过滤setter
        Set<Element> setterElements = new HashSet<>();
        for (Element method : suspectedSetterElements) {
          String methodName = method.getSimpleName().toString();
          final String propertyName = methodName.substring(3).toLowerCase();
          for (Element field : privateFieldElements) {
            if (field.getSimpleName().toString().equalsIgnoreCase(propertyName)) {
              setterElements.add(method);
              fit.compiler.PropertyDescriptor propertyDescriptor =
                  new fit.compiler.PropertyDescriptor();
              propertyDescriptor.setField(field);
              propertyDescriptor.setSetter(method);
              setterPropertyDescriptors.add(propertyDescriptor);
              break;
            }
          }
        }

        JavaFile javaFile = JavaFile.builder(preferenceClassName.packageName(),
            createPreferenceClass(preferenceClassName, isFinal, targetType, fieldElements,
                getterPropertyDescriptors, setterPropertyDescriptors))
            .addFileComment("Generated code from Fit. Do not modify!")
            .build();
        javaFile.writeTo(filer);
      } catch (Exception e) {
        logParsingError(element, SharedPreferenceAble.class, e);
      }
    }
    return false;
  }

  private boolean isGetter(Element method) {
    Name methodName = method.getSimpleName();
    if ((!methodName.toString().startsWith("get")) && !methodName.toString().startsWith("is")) {
      return false;
    }
    ExecutableType type = (ExecutableType) method.asType();
    //返回值为void
    if (TypeKind.VOID.equals(type.getReturnType().getKind())) {
      return false;
    }
    //有参数
    if (type.getParameterTypes().size() > 0) {
      return false;
    }

    if (methodName.length() < 4) {
      return false;
    }
    return true;
  }

  private boolean isSetter(Element method) {
    Name methodName = method.getSimpleName();
    if (!methodName.toString().startsWith("set")) {
      return false;
    }
    ExecutableType type = (ExecutableType) method.asType();
    //返回值不为void
    if (!TypeKind.VOID.equals(type.getReturnType().getKind())) {
      return false;
    }
    //有1个参数
    if (type.getParameterTypes().size() != 1) {
      return false;
    }

    if (methodName.length() < 4) {
      return false;
    }
    return true;
  }

  private String getPackageName(TypeElement type) {
    return elementUtils.getPackageOf(type).getQualifiedName().toString();
  }

  private static String getClassName(TypeElement type, String packageName) {
    int packageLen = packageName.length() + 1;
    return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
  }

  private TypeSpec createPreferenceClass(ClassName preferenceClassName, boolean isFinal,
      TypeName targetTypeName, Set<Element> fieldElements,
      Set<fit.compiler.PropertyDescriptor> getterPropertyDescriptors,
      Set<fit.compiler.PropertyDescriptor> setterPropertyDescriptors) {
    TypeSpec.Builder result =
        TypeSpec.classBuilder(preferenceClassName.simpleName()).addModifiers(PUBLIC);

    if (isFinal) {
      result.addModifiers(FINAL);
    }

    ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(MM, targetTypeName);
    result.addSuperinterface(parameterizedTypeName);

    result.addMethod(
        createPreferenceSaveMethod(targetTypeName, fieldElements, getterPropertyDescriptors));

    result.addMethod(
        createPreferenceGetMethod(targetTypeName, fieldElements, setterPropertyDescriptors));

    result.addMethod(createPreferenceClearFieldsMethod(fieldElements, getterPropertyDescriptors));

    return result.build();
  }

  private MethodSpec createPreferenceSaveMethod(TypeName targetType, Set<Element> fieldElements,
      Set<fit.compiler.PropertyDescriptor> getterPropertyDescriptors) {
    MethodSpec.Builder result = MethodSpec.methodBuilder("save")
        .returns(SHARED_PREFERENCES_EDITOR)
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC)
        .addParameter(CONTEXT, "context")
        .addParameter(STRING, "name")
        .addParameter(targetType, "obj")
        .addStatement(
            "SharedPreferences.Editor editor = $T.getSharedPreferenceEditor(context, name)", UTILS);
    //属性
    for (Element element : fieldElements) {
      TypeName typeName = TypeName.get(element.asType());
      String putMethod = genPutMethod(typeName);
      String valueL = element.getSimpleName().toString();
      result = genSaveCode(result, typeName, putMethod, valueL, element.getSimpleName().toString());
    }

    //getter
    for (fit.compiler.PropertyDescriptor propertyDescriptor : getterPropertyDescriptors) {
      Element method = propertyDescriptor.getGetter();
      TypeMirror typeMirror = ((ExecutableType) method.asType()).getReturnType();
      TypeName typeName = TypeName.get(typeMirror);
      String putMethod = genPutMethod(typeName);
      String valueL = method.toString();

      result = genSaveCode(result, typeName, putMethod, valueL,
          propertyDescriptor.getField().getSimpleName().toString());
    }

    result = result.addStatement("return editor");

    return result.build();
  }

  private String genPutMethod(TypeName fieldTypeName) {
    TypeName unboxFieldTypeName = unbox(fieldTypeName);
    String putMethod = "";

    if (stringTypeName.equals(unboxFieldTypeName)) {
      putMethod = "putString";
    } else if (TypeName.BOOLEAN.equals(unboxFieldTypeName)) {
      putMethod = "putBoolean";
    } else if (TypeName.FLOAT.equals(unboxFieldTypeName)) {
      putMethod = "putFloat";
    } else if (TypeName.INT.equals(unboxFieldTypeName)
        || TypeName.BYTE.equals(unboxFieldTypeName)
        || TypeName.SHORT.equals(unboxFieldTypeName)
        || TypeName.CHAR.equals(unboxFieldTypeName)) {
      putMethod = "putInt";
    } else if (TypeName.LONG.equals(unboxFieldTypeName)) {
      putMethod = "putLong";
    } else if (TypeName.DOUBLE.equals(unboxFieldTypeName)) {
      putMethod = "putLong";
    } else if (setOfHoverboards.equals(unboxFieldTypeName) || hashSetOfHoverboards.equals(
        unboxFieldTypeName)) {
      putMethod = "putStringSet";
    }
    return putMethod;
  }

  private MethodSpec.Builder genSaveCode(MethodSpec.Builder builder, TypeName typeName,
      String putMethod, String valueL, String propertyName) {
    TypeName unboxFieldTypeName = unbox(typeName);
    if (TypeName.DOUBLE.equals(typeName)) {
      valueL = "Double.doubleToLongBits( obj." + valueL + ")";
      builder.addStatement("editor.$L($S, " + valueL + ")", putMethod, propertyName);
      return builder;
    } else if (setOfHoverboards.equals(unboxFieldTypeName) || hashSetOfHoverboards.equals(
        unboxFieldTypeName)) {
      builder.addStatement("$T.$L($L, $S, obj." + valueL + ")", UTILS, putMethod, "editor",
          propertyName);
      return builder;
    }

    if (typeName.isBoxedPrimitive()) {
      if (TypeName.DOUBLE.equals(unboxFieldTypeName)) {
        builder.addStatement(
            "editor.$L($S, Double.doubleToLongBits($T.checkNonNull(obj.$L) ?  obj.$L : 0))",
            putMethod, propertyName, UTILS, propertyName, propertyName);
      } else if (TypeName.CHAR.equals(unboxFieldTypeName) || TypeName.BYTE.equals(
          unboxFieldTypeName) || TypeName.SHORT.equals(unboxFieldTypeName) || TypeName.INT.equals(
          unboxFieldTypeName) || TypeName.LONG.equals(unboxFieldTypeName) || TypeName.FLOAT.equals(
          unboxFieldTypeName)) {
        builder.addStatement(
            "editor.$L($S, $T.checkNonNull( obj." + valueL + ") ? obj." + valueL + " : 0)",
            putMethod, propertyName, UTILS);
      } else if (TypeName.BOOLEAN.equals(unboxFieldTypeName)) {
        builder.addStatement("editor.$L($S, $T.checkNonNull(obj.$L) ? obj.$L : false)", putMethod,
            propertyName, UTILS, propertyName, propertyName);
      }
    } else {
      if (stringTypeName.equals(unboxFieldTypeName) || typeName.isPrimitive()) {
        builder.addStatement("editor.$L($S, obj." + valueL + ")", putMethod, propertyName);
      } else {
        builder.addStatement("$T.writeObject(context, name + $S, obj.$L)", FILE_OBJECT_UTIL,
            "." + propertyName, valueL);
      }
    }
    return builder;
  }

  private MethodSpec createPreferenceGetMethod(TypeName targetType, Set<Element> fieldElements,
      Set<fit.compiler.PropertyDescriptor> setterPropertyDescriptors) {
    MethodSpec.Builder result = MethodSpec.methodBuilder("get")
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC)
        .addParameter(CONTEXT, "context")
        .addParameter(STRING, "name");

    result.addStatement(
        "$T sharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE)",
        SHARED_PREFERENCES);
    result.addStatement("$T obj = new $T()", targetType, targetType);

    for (Element element : fieldElements) {
      genGetCode(false, result, element.asType(), element.getSimpleName().toString(), "obj.$N");
    }

    //setter
    for (fit.compiler.PropertyDescriptor propertyDescriptor : setterPropertyDescriptors) {
      Element method = propertyDescriptor.getSetter();
      TypeMirror typeMirror = ((ExecutableType) method.asType()).getParameterTypes().get(0);
      genGetCode(true, result, typeMirror, propertyDescriptor.getField().getSimpleName().toString(),
          "obj." + method.getSimpleName() + "(");
    }
    result.addStatement("return obj").returns(targetType);
    return result.build();
  }

  private MethodSpec.Builder genGetCode(boolean isSetter, MethodSpec.Builder builder,
      TypeMirror typeMirror, String propertyName, String assignment) {
    TypeName fieldTypeName = unbox(TypeName.get(typeMirror));
    String method;
    String defaultValue = "0";
    String cast = "";
    String value = "$L sharedPreferences.$L($S, $L)";
    if (stringTypeName.equals(fieldTypeName)) {
      method = METHOD_GET_STRING;
      defaultValue = null;
    } else if (TypeName.BOOLEAN.equals(fieldTypeName)) {
      method = "getBoolean";
      defaultValue = "false";
    } else if (TypeName.FLOAT.equals(fieldTypeName)) {
      method = "getFloat";
    } else if (TypeName.INT.equals(fieldTypeName)) {
      method = METHOD_GET_Int;
    } else if (TypeName.BYTE.equals(fieldTypeName)) {
      method = METHOD_GET_Int;
      value = "($L) sharedPreferences.$L($S, $L)";
      cast = "byte";
    } else if (TypeName.SHORT.equals(fieldTypeName)) {
      method = METHOD_GET_Int;
      value = "($L) sharedPreferences.$L($S, $L)";
      cast = "short";
    } else if (TypeName.CHAR.equals(fieldTypeName)) {
      method = METHOD_GET_Int;
      value = "($L) sharedPreferences.$L($S, $L)";
      cast = "char";
    } else if (TypeName.LONG.equals(fieldTypeName)) {
      method = "getLong";
    } else if (TypeName.DOUBLE.equals(fieldTypeName)) {
      method = "getLong";
      value = "$L Double.longBitsToDouble(sharedPreferences.$L($S, $L))";
    } else if (setOfHoverboards.equals(fieldTypeName) || hashSetOfHoverboards.equals(
        fieldTypeName)) {
      defaultValue = null;
      value = "($T) $T.getStringSet($L, $S, $L)";
      if (isSetter) {
        return builder.addStatement(assignment + value + ")", hashSetOfHoverboards, UTILS,
            "sharedPreferences", propertyName, defaultValue);
      }
      return builder.addStatement(assignment + " = " + value, propertyName, hashSetOfHoverboards,
          UTILS, "sharedPreferences", propertyName, defaultValue);
    } else {
      if (isSetter) {
        builder.addStatement(assignment + "($T) $T.readObject(context, name + $S))", fieldTypeName,
            FILE_OBJECT_UTIL, "." + propertyName);
      } else {
        builder.addStatement(assignment + " = ($T) $T.readObject(context, name + $S)", propertyName,
            fieldTypeName, FILE_OBJECT_UTIL, "." + propertyName);
      }
      return builder;
    }
    if (isSetter) {
      return builder.addStatement(assignment + value + ")", cast, method, propertyName,
          defaultValue);
    }
    return builder.addStatement(assignment + " = " + value, propertyName, cast, method,
        propertyName, defaultValue);
  }

  private MethodSpec createPreferenceClearFieldsMethod(Set<Element> fieldElements,
      Set<fit.compiler.PropertyDescriptor> getterPropertyDescriptors) {
    MethodSpec.Builder result = MethodSpec.methodBuilder("clearFields")
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC)
        .addParameter(CONTEXT, "context")
        .addParameter(STRING, "name");

    //属性
    for (Element element : fieldElements) {
      TypeName typeName = TypeName.get(element.asType());
      if (isObject(typeName)) {
        result.addStatement("$T.deleteFile(context, name + $S)", FILE_OBJECT_UTIL,
            "." + element.getSimpleName().toString());
      }
    }

    //getter
    for (fit.compiler.PropertyDescriptor propertyDescriptor : getterPropertyDescriptors) {

      Element element = propertyDescriptor.getGetter();
      TypeMirror typeMirror = ((ExecutableType) element.asType()).getReturnType();
      TypeName typeName = TypeName.get(typeMirror);
      String valueL = element.toString();

      if (isObject(typeName)) {
        result.addStatement("$T.deleteFile(context, name + $S)", FILE_OBJECT_UTIL,
            "." + propertyDescriptor.getField().getSimpleName().toString());
      }
    }
    return result.build();
  }

  /**
   * @param typeName {@link Type}
   * @return object is true,otherwise false
   * @since 1.0.1
   */
  private boolean isObject(TypeName typeName) {
    typeName = typeName.box();
    return !(typeName.isBoxedPrimitive()
        || stringTypeName.equals(typeName)
        || setOfHoverboards.equals(typeName)
        || hashSetOfHoverboards.equals(typeName));
  }

  private TypeName unbox(TypeName typeName) {
    if (typeName.isBoxedPrimitive()) {
      return typeName.unbox();
    }
    return typeName;
  }

  private void logParsingError(Element element, Class<? extends Annotation> annotation,
      Exception e) {
    StringWriter stackTrace = new StringWriter();
    e.printStackTrace(new PrintWriter(stackTrace));
    error(element, "Unable to parse @%s shared.\n\n%s", annotation.getSimpleName(), stackTrace);
  }

  private void error(Element element, String message, Object... args) {
    printMessage(Diagnostic.Kind.ERROR, element, message, args);
  }

  private void note(Element element, String message, Object... args) {
    printMessage(Diagnostic.Kind.NOTE, element, message, args);
  }

  private void printMessage(Diagnostic.Kind kind, Element element, String message, Object[] args) {
    if (args.length > 0) {
      message = String.format(message, args);
    }

    processingEnv.getMessager().printMessage(kind, message, element);
  }

  @Override public Set<String> getSupportedAnnotationTypes() {
    return Collections.singleton(SharedPreferenceAble.class.getCanonicalName());
  }

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