content: package com.aitorvs.autoparcel.internal.codegen;
  
  /*
   * Copyright (C) 13/07/16 aitorvs
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  import com.aitorvs.autoparcel.AutoParcel;
  import com.aitorvs.autoparcel.ParcelAdapter;
  import com.aitorvs.autoparcel.ParcelVersion;
  import com.aitorvs.autoparcel.internal.common.MoreElements;
  import com.google.common.base.CaseFormat;
  import com.google.common.base.Strings;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableMap;
  import com.google.common.collect.ImmutableSet;
  import com.google.common.collect.Lists;
  import com.squareup.javapoet.AnnotationSpec;
  import com.squareup.javapoet.ArrayTypeName;
  import com.squareup.javapoet.ClassName;
  import com.squareup.javapoet.CodeBlock;
  import com.squareup.javapoet.FieldSpec;
  import com.squareup.javapoet.JavaFile;
  import com.squareup.javapoet.MethodSpec;
  import com.squareup.javapoet.NameAllocator;
  import com.squareup.javapoet.ParameterSpec;
  import com.squareup.javapoet.ParameterizedTypeName;
  import com.squareup.javapoet.TypeName;
  import com.squareup.javapoet.TypeSpec;
  
  import java.io.IOException;
  import java.io.Writer;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import javax.annotation.processing.AbstractProcessor;
  import javax.annotation.processing.ProcessingEnvironment;
  import javax.annotation.processing.RoundEnvironment;
  import javax.annotation.processing.SupportedAnnotationTypes;
  import javax.lang.model.SourceVersion;
  import javax.lang.model.element.AnnotationMirror;
  import javax.lang.model.element.Element;
  import javax.lang.model.element.ElementKind;
  import javax.lang.model.element.TypeElement;
  import javax.lang.model.element.VariableElement;
  import javax.lang.model.type.MirroredTypeException;
  import javax.lang.model.type.TypeKind;
  import javax.lang.model.type.TypeMirror;
  import javax.lang.model.util.ElementFilter;
  import javax.lang.model.util.Types;
  import javax.tools.Diagnostic;
  import javax.tools.JavaFileObject;
  
  import static javax.lang.model.element.Modifier.FINAL;
  import static javax.lang.model.element.Modifier.PRIVATE;
  import static javax.lang.model.element.Modifier.PUBLIC;
  import static javax.lang.model.element.Modifier.STATIC;
  
  @SupportedAnnotationTypes("com.aitorvs.autoparcel.AutoParcel")
  public final class AutoParcelProcessor extends AbstractProcessor {
      private ErrorReporter mErrorReporter;
      private Types mTypeUtils;
  
  
      static final class Property {
          final String fieldName;
          final VariableElement element;
          final TypeName typeName;
          final ImmutableSet<String> annotations;
          final int version;
          TypeMirror typeAdapter;
  
          Property(String fieldName, VariableElement element) {
              this.fieldName = fieldName;
              this.element = element;
              this.typeName = TypeName.get(element.asType());
              this.annotations = getAnnotations(element);
  
              // get the parcel adapter if any
              ParcelAdapter parcelAdapter = element.getAnnotation(ParcelAdapter.class);
              if (parcelAdapter != null) {
                  try {
                      parcelAdapter.value();
                  } catch (MirroredTypeException e) {
                      this.typeAdapter = e.getTypeMirror();
                  }
              }
  
              // get the element version, default 0
              ParcelVersion parcelVersion = element.getAnnotation(ParcelVersion.class);
              this.version = parcelVersion == null ? 0 : parcelVersion.from();
          }
  
          public boolean isNullable() {
              return this.annotations.contains("Nullable");
          }
  
          public int version() {
              return this.version;
          }
  
          private ImmutableSet<String> getAnnotations(VariableElement element) {
              ImmutableSet.Builder<String> builder = ImmutableSet.builder();
              for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
                  builder.add(annotation.getAnnotationType().asElement().getSimpleName().toString());
              }
  
              return builder.build();
          }
      }
  
      @Override
      public synchronized void init(ProcessingEnvironment processingEnv) {
          super.init(processingEnv);
          mErrorReporter = new ErrorReporter(processingEnv);
          mTypeUtils = processingEnv.getTypeUtils();
      }
  
      @Override
      public SourceVersion getSupportedSourceVersion() {
          return SourceVersion.latestSupported();
      }
  
      @Override
      public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
          Collection<? extends Element> annotatedElements =
                  env.getElementsAnnotatedWith(AutoParcel.class);
          List<TypeElement> types = new ImmutableList.Builder<TypeElement>()
                  .addAll(ElementFilter.typesIn(annotatedElements))
                  .build();
  
          for (TypeElement type : types) {
              processType(type);
          }
  
          // We are the only ones handling AutoParcel annotations
          return true;
      }
  
      private void processType(TypeElement type) {
          AutoParcel autoParcel = type.getAnnotation(AutoParcel.class);
          if (autoParcel == null) {
              mErrorReporter.abortWithError("annotation processor for @AutoParcel was invoked with a" +
                      "type annotated differently; compiler bug? O_o", type);
          }
          if (type.getKind() != ElementKind.CLASS) {
              mErrorReporter.abortWithError("@" + AutoParcel.class.getName() + " only applies to classes", type);
          }
          if (ancestorIsAutoParcel(type)) {
              mErrorReporter.abortWithError("One @AutoParcel class shall not extend another", type);
          }
  
          checkModifiersIfNested(type);
  
          // get the fully-qualified class name
          String fqClassName = generatedSubclassName(type, 0);
          // class name
          String className = TypeUtil.simpleNameOf(fqClassName);
          String source = generateClass(type, className, type.getSimpleName().toString(), false);
          source = Reformatter.fixup(source);
          writeSourceFile(fqClassName, source, type);
  
      }
  
      private void writeSourceFile(String className, String text, TypeElement originatingType) {
          try {
              JavaFileObject sourceFile =
                      processingEnv.getFiler().createSourceFile(className, originatingType);
              Writer writer = sourceFile.openWriter();
              try {
                  writer.write(text);
              } finally {
                  writer.close();
              }
          } catch (IOException e) {
              // This should really be an error, but we make it a warning in the hope of resisting Eclipse
              // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=367599. If that bug manifests, we may get
              // invoked more than once for the same file, so ignoring the ability to overwrite it is the
              // right thing to do. If we are unable to write for some other reason, we should get a compile
              // error later because user code will have a reference to the code we were supposed to
              // generate (new AutoValue_Foo() or whatever) and that reference will be undefined.
              processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                      "Could not write generated class " + className + ": " + e);
          }
      }
  
      private String generateClass(TypeElement type, String className, String classToExtend, boolean isFinal) {
          if (type == null) {
              mErrorReporter.abortWithError("generateClass was invoked with null type", type);
          }
          if (className == null) {
              mErrorReporter.abortWithError("generateClass was invoked with null class name", type);
          }
          if (classToExtend == null) {
              mErrorReporter.abortWithError("generateClass was invoked with null parent class", type);
          }
          List<VariableElement> nonPrivateFields = getParcelableFieldsOrError(type);
          if (nonPrivateFields.isEmpty()) {
              mErrorReporter.abortWithError("generateClass error, all fields are declared PRIVATE", type);
          }
  
          // get the properties
          ImmutableList<Property> properties = buildProperties(nonPrivateFields);
  
          // get the type adapters
          ImmutableMap<TypeMirror, FieldSpec> typeAdapters = getTypeAdapters(properties);
  
          // get the parcel version
          //noinspection ConstantConditions
          int version = type.getAnnotation(AutoParcel.class).version();
  
          // Generate the AutoParcel_??? class
          String pkg = TypeUtil.packageNameOf(type);
          TypeName classTypeName = ClassName.get(pkg, className);
          TypeSpec.Builder subClass = TypeSpec.classBuilder(className)
                  // Add the version
                  .addField(TypeName.INT, "version", PRIVATE)
                  // Class must be always final
                  .addModifiers(FINAL)
                  // extends from original abstract class
                  .superclass(ClassName.get(pkg, classToExtend))
                  // Add the DEFAULT constructor
                  .addMethod(generateConstructor(properties))
                  // Add the private constructor
                  .addMethod(generateConstructorFromParcel(processingEnv, properties, typeAdapters))
                  // overrides describeContents()
                  .addMethod(generateDescribeContents())
                  // static final CREATOR
                  .addField(generateCreator(processingEnv, properties, classTypeName, typeAdapters))
                  // overrides writeToParcel()
                  .addMethod(generateWriteToParcel(version, processingEnv, properties, typeAdapters)); // generate writeToParcel()
  
          if (!ancestoIsParcelable(processingEnv, type)) {
              // Implement android.os.Parcelable if the ancestor does not do it.
              subClass.addSuperinterface(ClassName.get("android.os", "Parcelable"));
          }
  
          if (!typeAdapters.isEmpty()) {
              typeAdapters.values().forEach(subClass::addField);
          }
  
  
          JavaFile javaFile = JavaFile.builder(pkg, subClass.build()).build();
          return javaFile.toString();
      }
  
      private ImmutableMap<TypeMirror, FieldSpec> getTypeAdapters(ImmutableList<Property> properties) {
          Map<TypeMirror, FieldSpec> typeAdapters = new LinkedHashMap<>();
          NameAllocator nameAllocator = new NameAllocator();
          nameAllocator.newName("CREATOR");
          for (Property property : properties) {
              if (property.typeAdapter != null && !typeAdapters.containsKey(property.typeAdapter)) {
                  ClassName typeName = (ClassName) TypeName.get(property.typeAdapter);
                  String name = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, typeName.simpleName());
                  name = nameAllocator.newName(name, typeName);
  
                  typeAdapters.put(property.typeAdapter, FieldSpec.builder(
                          typeName, NameAllocator.toJavaIdentifier(name), PRIVATE, STATIC, FINAL)
                          .initializer("new $T()", typeName).build());
              }
          }
          return ImmutableMap.copyOf(typeAdapters);
      }
  
      private ImmutableList<Property> buildProperties(List<VariableElement> elements) {
          ImmutableList.Builder<Property> builder = ImmutableList.builder();
          for (VariableElement element : elements) {
              builder.add(new Property(element.getSimpleName().toString(), element));
          }
  
          return builder.build();
      }
  
      /**
       * This method returns a list of all non private fields. If any <code>private</code> fields is
       * found, the method errors out
       *
       * @param type element
       * @return list of all non-<code>private</code> fields
       */
      private List<VariableElement> getParcelableFieldsOrError(TypeElement type) {
          List<VariableElement> allFields = ElementFilter.fieldsIn(type.getEnclosedElements());
          List<VariableElement> nonPrivateFields = new ArrayList<>();
  
          for (VariableElement field : allFields) {
              if (!field.getModifiers().contains(PRIVATE)) {
                  nonPrivateFields.add(field);
              } else {
                  // return error, PRIVATE fields are not allowed
                  mErrorReporter.abortWithError("getFieldsError error, PRIVATE fields not allowed", type);
              }
          }
  
          return nonPrivateFields;
      }
  
      private MethodSpec generateConstructor(ImmutableList<Property> properties) {
  
          List<ParameterSpec> params = Lists.newArrayListWithCapacity(properties.size());
          for (Property property : properties) {
              params.add(ParameterSpec.builder(property.typeName, property.fieldName).build());
          }
  
          MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                  .addParameters(params);
  
          for (ParameterSpec param : params) {
              builder.addStatement("this.$N = $N", param.name, param.name);
          }
  
          return builder.build();
      }
  
      private MethodSpec generateConstructorFromParcel(
              ProcessingEnvironment env,
              ImmutableList<Property> properties,
              ImmutableMap<TypeMirror, FieldSpec> typeAdapters) {
  
          // Create the PRIVATE constructor from Parcel
          MethodSpec.Builder builder = MethodSpec.constructorBuilder()
                  .addModifiers(PRIVATE)      // private
                  .addParameter(ClassName.bestGuess("android.os.Parcel"), "in"); // input param
  
          // get a code block builder
          CodeBlock.Builder block = CodeBlock.builder();
  
          // First thing is reading the Parcelable object version
          block.add("this.version = in.readInt();\n");
  
          // FIXME: 31/07/16 remove if not used
          boolean requiresSuppressWarnings = false;
  
          // Now, iterate all properties, check the version initialize them
          for (Property p : properties) {
  
              // get the property version
              int pVersion = p.version();
              if (pVersion > 0) {
                  block.beginControlFlow("if (this.version >= $L)", pVersion);
              }
  
              block.add("this.$N = ", p.fieldName);
  
              if (p.typeAdapter != null && typeAdapters.containsKey(p.typeAdapter)) {
                  Parcelables.readValueWithTypeAdapter(block, p, typeAdapters.get(p.typeAdapter));
              } else {
                  requiresSuppressWarnings |= Parcelables.isTypeRequiresSuppressWarnings(p.typeName);
                  TypeName parcelableType = Parcelables.getTypeNameFromProperty(p, env.getTypeUtils());
                  Parcelables.readValue(block, p, parcelableType);
              }
  
              block.add(";\n");
  
              if (pVersion > 0) {
                  block.endControlFlow();
              }
          }
  
          builder.addCode(block.build());
  
          return builder.build();
      }
  
      private String generatedSubclassName(TypeElement type, int depth) {
          return generatedClassName(type, Strings.repeat("$", depth) + "AutoParcel_");
      }
  
      private String generatedClassName(TypeElement type, String prefix) {
          String name = type.getSimpleName().toString();
          while (type.getEnclosingElement() instanceof TypeElement) {
              type = (TypeElement) type.getEnclosingElement();
              name = type.getSimpleName() + "_" + name;
          }
          String pkg = TypeUtil.packageNameOf(type);
          String dot = pkg.isEmpty() ? "" : ".";
          return pkg + dot + prefix + name;
      }
  
      private MethodSpec generateWriteToParcel(
              int version,
              ProcessingEnvironment env,
              ImmutableList<Property> properties,
              ImmutableMap<TypeMirror, FieldSpec> typeAdapters) {
          ParameterSpec dest = ParameterSpec
                  .builder(ClassName.get("android.os", "Parcel"), "dest")
                  .build();
          ParameterSpec flags = ParameterSpec.builder(int.class, "flags").build();
          MethodSpec.Builder builder = MethodSpec.methodBuilder("writeToParcel")
                  .addAnnotation(Override.class)
                  .addModifiers(PUBLIC)
                  .addParameter(dest)
                  .addParameter(flags);
  
          // write first the parcelable object version...
          builder.addCode(Parcelables.writeVersion(version, dest));
  
          // ...then write all the properties
          for (Property p : properties) {
              if (p.typeAdapter != null && typeAdapters.containsKey(p.typeAdapter)) {
                  FieldSpec typeAdapter = typeAdapters.get(p.typeAdapter);
                  builder.addCode(Parcelables.writeValueWithTypeAdapter(typeAdapter, p, dest));
              } else {
                  builder.addCode(Parcelables.writeValue(p, dest, flags, env.getTypeUtils()));
              }
          }
  
          return builder.build();
      }
  
      private MethodSpec generateDescribeContents() {
          return MethodSpec.methodBuilder("describeContents")
                  .addAnnotation(Override.class)
                  .addModifiers(PUBLIC)
                  .returns(int.class)
                  .addStatement("return 0")
                  .build();
      }
  
      private FieldSpec generateCreator(
              ProcessingEnvironment env,
              ImmutableList<Property> properties,
              TypeName type,
              ImmutableMap<TypeMirror, FieldSpec> typeAdapters) {
          ClassName creator = ClassName.bestGuess("android.os.Parcelable.Creator");
          TypeName creatorOfClass = ParameterizedTypeName.get(creator, type);
  
          Types typeUtils = env.getTypeUtils();
          CodeBlock.Builder ctorCall = CodeBlock.builder();
          boolean requiresSuppressWarnings = false;
          ctorCall.add("return new $T(in);\n", type);
  
          // Method createFromParcel()
          MethodSpec.Builder createFromParcel = MethodSpec.methodBuilder("createFromParcel")
                  .addAnnotation(Override.class);
          if (requiresSuppressWarnings) {
              createFromParcel.addAnnotation(createSuppressUncheckedWarningAnnotation());
          }
          createFromParcel
                  .addModifiers(PUBLIC)
                  .returns(type)
                  .addParameter(ClassName.bestGuess("android.os.Parcel"), "in");
          createFromParcel.addCode(ctorCall.build());
  
          TypeSpec creatorImpl = TypeSpec.anonymousClassBuilder("")
                  .superclass(creatorOfClass)
                  .addMethod(createFromParcel
                          .build())
                  .addMethod(MethodSpec.methodBuilder("newArray")
                          .addAnnotation(Override.class)
                          .addModifiers(PUBLIC)
                          .returns(ArrayTypeName.of(type))
                          .addParameter(int.class, "size")
                          .addStatement("return new $T[size]", type)
                          .build())
                  .build();
  
          return FieldSpec
                  .builder(creatorOfClass, "CREATOR", PUBLIC, FINAL, STATIC)
                  .initializer("$L", creatorImpl)
                  .build();
      }
  
      private void checkModifiersIfNested(TypeElement type) {
          ElementKind enclosingKind = type.getEnclosingElement().getKind();
          if (enclosingKind.isClass() || enclosingKind.isInterface()) {
              if (type.getModifiers().contains(PRIVATE)) {
                  mErrorReporter.abortWithError("@AutoParcel class must not be private", type);
              }
              if (!type.getModifiers().contains(STATIC)) {
                  mErrorReporter.abortWithError("Nested @AutoParcel class must be static", type);
              }
          }
          // In principle type.getEnclosingElement() could be an ExecutableElement (for a class
          // declared inside a method), but since RoundEnvironment.getElementsAnnotatedWith doesn't
          // return such classes we won't see them here.
      }
  
      private boolean ancestorIsAutoParcel(TypeElement type) {
          while (true) {
              TypeMirror parentMirror = type.getSuperclass();
              if (parentMirror.getKind() == TypeKind.NONE) {
                  return false;
              }
              TypeElement parentElement = (TypeElement) mTypeUtils.asElement(parentMirror);
              if (MoreElements.isAnnotationPresent(parentElement, AutoParcel.class)) {
                  return true;
              }
              type = parentElement;
          }
      }
  
      private boolean ancestoIsParcelable(ProcessingEnvironment env, TypeElement type) {
          // TODO: 15/07/16 check recursively
          TypeMirror classType = type.asType();
          TypeMirror parcelable = env.getElementUtils().getTypeElement("android.os.Parcelable").asType();
          return TypeUtil.isClassOfType(env.getTypeUtils(), parcelable, classType);
      }
  
      private static AnnotationSpec createSuppressUncheckedWarningAnnotation() {
          return AnnotationSpec.builder(SuppressWarnings.class)
                  .addMember("value", "\"unchecked\"")
                  .build();
      }
  }
  