/*
 * Copyright 2014 Google LLC
 *
 * 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.
 */
package com.google.auto.value.processor;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;

import com.google.auto.common.AnnotationMirrors;
import com.google.auto.value.processor.AutoValueishProcessor.GetterProperty;
import com.google.auto.value.processor.PropertyBuilderClassifier.PropertyBuilder;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.escapevelocity.Template;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Optional;
import java.util.zip.CRC32;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

/**
 * Generates GWT serialization code for {@code @AutoValue} classes also marked
 * {@code @GwtCompatible(serializable = true)}.
 *
 * @author Éamonn McManus
 */
class GwtSerialization {
  private final GwtCompatibility gwtCompatibility;
  private final ProcessingEnvironment processingEnv;
  private final TypeElement type;

  GwtSerialization(
      GwtCompatibility gwtCompatibility, ProcessingEnvironment processingEnv, TypeElement type) {
    this.gwtCompatibility = gwtCompatibility;
    this.processingEnv = processingEnv;
    this.type = type;
  }

  private boolean shouldWriteGwtSerializer() {
    Optional<AnnotationMirror> optionalGwtCompatible = gwtCompatibility.gwtCompatibleAnnotation();
    if (optionalGwtCompatible.isPresent()) {
      AnnotationMirror gwtCompatible = optionalGwtCompatible.get();
      return AnnotationMirrors.getAnnotationValuesWithDefaults(gwtCompatible).entrySet().stream()
          .anyMatch(
              e ->
                  e.getKey().getSimpleName().contentEquals("serializable")
                      && e.getValue().getValue().equals(true));
    }
    return false;
  }

  /**
   * Writes the GWT serializer for the given type, if appropriate. An {@code @AutoValue} class gets
   * a GWT serializer if it is annotated with {@code @GwtCompatible(serializable = true)}, where the
   * {@code @GwtCompatible} annotation can come from any package.
   *
   * <p>If the type is com.example.Foo then the generated AutoValue subclass is
   * com.example.AutoValue_Foo and the GWT serializer is
   * com.example.AutoValue_Foo_CustomFieldSerializer.
   *
   * @param autoVars the template variables defined for this type.
   * @param finalSubclass the simple name of the AutoValue class being generated, AutoValue_Foo
   *     in the example.
   */
  void maybeWriteGwtSerializer(AutoValueTemplateVars autoVars, String finalSubclass) {
    if (shouldWriteGwtSerializer()) {
      GwtTemplateVars vars = new GwtTemplateVars();
      vars.pkg = autoVars.pkg;
      vars.subclass = finalSubclass;
      vars.formalTypes = autoVars.formalTypes;
      vars.actualTypes = autoVars.actualTypes;
      vars.useBuilder = !autoVars.builderTypeName.isEmpty();
      vars.builderSetters = autoVars.builderSetters;
      vars.builderPropertyBuilders = autoVars.builderPropertyBuilders;
      vars.generated = autoVars.generated;
      String className =
          (vars.pkg.isEmpty() ? "" : vars.pkg + ".") + vars.subclass + "_CustomFieldSerializer";
      vars.serializerClass = TypeSimplifier.simpleNameOf(className);
      vars.props =
          autoVars.props.stream().map(p -> new Property((GetterProperty) p)).collect(toList());
      vars.classHashString = computeClassHash(autoVars.props, vars.pkg);
      String text = vars.toText();
      text = TypeEncoder.decode(text, processingEnv, vars.pkg, type.asType());
      writeSourceFile(className, text, type);
    }
  }

  public static class Property {
    private final GetterProperty property;
    private final boolean isCastingUnchecked;

    Property(GetterProperty property) {
      this.property = property;
      this.isCastingUnchecked = TypeSimplifier.isCastingUnchecked(property.getTypeMirror());
    }

    @Override
    public String toString() {
      return property.toString();
    }

    public String getGetter() {
      return property.getGetter();
    }

    public String getType() {
      return property.getType();
    }

    public String getName() {
      return property.getName();
    }

    /**
     * Returns the suffix in serializer method names for values of the given type. For example, if
     * the type is "int" then the returned value will be "Int" because the serializer methods are
     * called readInt and writeInt. There are methods for all primitive types and String; every
     * other type uses readObject and writeObject.
     */
    public String getGwtType() {
      TypeMirror typeMirror = property.getTypeMirror();
      String type = typeMirror.toString();
      if (property.getKind().isPrimitive()) {
        return Character.toUpperCase(type.charAt(0)) + type.substring(1);
      } else if (type.equals("java.lang.String")) {
        return "String";
      } else {
        return "Object";
      }
    }

    /**
     * Returns a string to be inserted before the call to the readFoo() call so that the expression
     * can be assigned to the given type. For primitive types and String, the readInt() etc methods
     * already return the right type so the string is empty. For other types, the string is a cast
     * like "(Foo) ".
     */
    public String getGwtCast() {
      if (property.getKind().isPrimitive() || getType().equals("String")) {
        return "";
      } else {
        return "(" + getType() + ") ";
      }
    }

    public boolean isCastingUnchecked() {
      return isCastingUnchecked;
    }
  }

  @SuppressWarnings("unused") // some fields are only read through reflection
  static class GwtTemplateVars extends TemplateVars {
    /** The properties defined by the parent class's abstract methods. */
    List<Property> props;

    /**
     * The package of the class with the {@code @AutoValue} annotation and its generated subclass.
     */
    String pkg;

    /** The simple name of the generated subclass. */
    String subclass;

    /**
     * The formal generic signature of the class with the {@code @AutoValue} annotation and its
     * generated subclass. This is empty, or contains type variables with optional bounds, for
     * example {@code <K, V extends K>}.
     */
    String formalTypes;
    /**
     * The generic signature used by the generated subclass for its superclass reference. This is
     * empty, or contains only type variables with no bounds, for example {@code <K, V>}.
     */
    String actualTypes;

    /** True if the {@code @AutoValue} class is constructed using a generated builder. */
    Boolean useBuilder;

    /**
     * A multimap from property names (like foo) to the corresponding setter methods (foo or
     * setFoo).
     */
    Multimap<String, BuilderSpec.PropertySetter> builderSetters;

    /**
     * A map from property names to information about the associated property builder. A property
     * called foo (defined by a method foo() or getFoo()) can have a property builder called
     * fooBuilder(). The type of foo must be a type that has an associated builder following certain
     * conventions. Guava immutable types such as ImmutableList follow those conventions, as do many
     * {@code @AutoValue} types.
     */
    ImmutableMap<String, PropertyBuilder> builderPropertyBuilders = ImmutableMap.of();

    /** The simple name of the generated GWT serializer class. */
    String serializerClass;

    /**
     * The encoding of the {@code Generated} class. Empty if no {@code Generated} class is
     * available.
     */
    String generated;

    /** A string that should change if any salient details of the serialized class change. */
    String classHashString;

    private static final Template TEMPLATE = parsedTemplateForResource("gwtserializer.vm");

    @Override
    Template parsedTemplate() {
      return TEMPLATE;
    }
  }

  private void writeSourceFile(String className, String text, TypeElement originatingType) {
    try {
      JavaFileObject sourceFile =
          processingEnv.getFiler().createSourceFile(className, originatingType);
      try (Writer writer = sourceFile.openWriter()) {
        writer.write(text);
      }
    } catch (IOException e) {
      processingEnv
          .getMessager()
          .printMessage(
              Diagnostic.Kind.WARNING, "Could not write generated class " + className + ": " + e);
      // A warning rather than an error for the reason explained in
      // AutoValueishProcessor.writeSourceFile.
    }
  }

  // Compute a hash that is guaranteed to change if the names, types, or order of the fields
  // change. We use TypeEncoder so that we can get a defined string for types, since
  // TypeMirror.toString() isn't guaranteed to remain the same.
  private String computeClassHash(Iterable<AutoValueishProcessor.Property> props, String pkg) {
    CRC32 crc = new CRC32();
    String encodedType = TypeEncoder.encode(type.asType()) + ":";
    String decodedType = TypeEncoder.decode(encodedType, processingEnv, "", null);
    if (!decodedType.startsWith(pkg)) {
      // This is for compatibility with the way an earlier version did things. Preserving hash
      // codes probably isn't vital, since client and server should be in sync.
      decodedType = pkg + "." + decodedType;
    }
    crc.update(decodedType.getBytes(UTF_8));
    for (AutoValueishProcessor.Property prop : props) {
      String encodedProp = prop + ":" + TypeEncoder.encode(prop.getTypeMirror()) + ";";
      String decodedProp = TypeEncoder.decode(encodedProp, processingEnv, pkg, null);
      crc.update(decodedProp.getBytes(UTF_8));
    }
    return String.format("%08x", crc.getValue());
  }
}
