package org.immutables.value.processor.meta;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.Types;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;
import org.immutables.value.Value.Lazy;
import org.immutables.value.Value.Parameter;
import org.immutables.value.Value.Style;
import org.immutables.value.Value.Style.ImplementationVisibility;
import org.immutables.value.processor.meta.AttributeBuilderDescriptor.ValueToBuilderTarget;

/**
 * Reflects over the returnType and generates strings for the {@link AttributeBuilderDescriptor}
 */
@Immutable(builder = false)
@Style(visibility = ImplementationVisibility.PRIVATE)
public abstract class AttributeBuilderReflection {
  private static final Map<String, AttributeBuilderDescriptor> analyzedReturnTypes = new HashMap<>();

  // The discovery is based off of the class being investigated, AND the current attributeBuilder discovery pattern
  // The same class included in two parents, may or may not be nested builders based on that discovery pattern
  private static String cachingKey(ValueAttribute valueAttribute) {
    return String.format("%s-%s",
        valueAttribute.containedTypeElement.getQualifiedName(),
        Joiner.on(".").join(valueAttribute.containingType.constitution.style().attributeBuilder()));
  }

  public static AttributeBuilderReflection forValueType(ValueAttribute valueAttribute) {
    return ImmutableAttributeBuilderReflection.of(valueAttribute);
  }

  @Parameter
  abstract ValueAttribute valueAttribute();

  @Lazy
  protected List<Strategy> getStrategies() {
    // Order here matters. We want first party to be found first.
    return Arrays.asList(
        ImmutableFirstPartyStrategy.of(valueAttribute()),
        ThirdPartyAttributeBuilderStrategy.of(valueAttribute())
    );
  }

  @Lazy
  boolean isAttributeBuilder() {
    if (!valueAttribute().containingType.constitution.style().attributeBuilderDetection()) {
      return false;
    }

    if (valueAttribute().containedTypeElement == null) {
      return false;
    }

    String cacheKey = cachingKey(valueAttribute());

    if (analyzedReturnTypes.containsKey(cacheKey)) {
      return analyzedReturnTypes.get(cacheKey) != null;
    }

    for (Strategy strategy : getStrategies()) {
      if (strategy.isAttributeBuilder()) {
        return true;
      }
    }

    analyzedReturnTypes.put(cacheKey, null);

    return false;
  }

  @Lazy
  AttributeBuilderDescriptor getAttributeBuilderDescriptor() {
    if (!isAttributeBuilder()) {
      throw new IllegalStateException(
          "Should not call getReflectionStrategy unless isAttributeBuilder is true");
    }

    if (valueAttribute().containedTypeElement == null) {
      throw new AssertionError();
    }

    String cacheKey = cachingKey(valueAttribute());
    if (analyzedReturnTypes.containsKey(cacheKey)) {
      return Preconditions.checkNotNull(analyzedReturnTypes
          .get(cacheKey));
    }

    AttributeBuilderDescriptor descriptor = getReflectionStrategy().getAttributeBuilderDescriptor();
    analyzedReturnTypes.put(cacheKey, descriptor);

    return descriptor;
  }

  @Lazy
  protected Strategy getReflectionStrategy() {
    for (Strategy strategy : getStrategies()) {
      if (strategy.isAttributeBuilder()) {
        return strategy;
      }
    }

    throw new AssertionError("isAttributeBuilder flip-flopped from true to false.");
  }

  /**
   * Allows for different mechanisms of discovering nested builders.
   *
   * This is needed because Immutables value objects may not be available on the
   * class path during processing, which will be order dependent.
   */
  interface Strategy {

    boolean isAttributeBuilder();

    AttributeBuilderDescriptor getAttributeBuilderDescriptor();
  }

  /**
   * Strategy for processing first party immutables.  Honors both deepImmutableDiscovery, and
   * builder extension.
   */
  @Immutable(builder = false)
  abstract static class FirstPartyStrategy implements Strategy {

    @Parameter
    abstract ValueAttribute valueAttribute();

    @Override
    public boolean isAttributeBuilder() {
      return valueAttribute().attributeValueType != null &&
          valueAttribute().attributeValueType.isUseBuilder();

    }

    @Override
    public AttributeBuilderDescriptor getAttributeBuilderDescriptor() {
      return ImmutableAttributeBuilderDescriptor.builder()
          .valueToBuilderTarget(ValueToBuilderTarget.BUILDER_INSTANCE)
          .valueToBuilderMethod(attributeValueType().names().from)
          .buildMethod(attributeValueType().names().build)
          .qualifiedValueTypeName(attributeValueType().typeImmutable().toString())
          .qualifiedBuilderTypeName(attributeValueType().typeBuilderImpl().toString())
          .qualifiedBuilderConstructorMethod(attributeValueType().factoryBuilder().toString())
          .build();

    }

    ValueType attributeValueType() {
      return valueAttribute().attributeValueType;
    }
  }

  /**
   * Strategy for parsing third party immutables. for example: the protocol buffer API.
   *
   * Assumes that all third party classes are available on the class path at processing time.
   *
   * If this is not the case, we may need to use a processing method that allows deferring of
   * compilation. Example, moving from auto-value to immutables... though maybe you would just
   * implement a new strategy for that use case...
   */
  @Immutable(builder = false)
  abstract static class ThirdPartyAttributeBuilderStrategy implements Strategy {

    /**
     * Guaranteed not null if isAttributeBuilder is true.
     * @return model of how to generate attributeBuilder.
     */
    @Nullable @Parameter
    protected abstract AttributeBuilderThirdPartyModel builderModel();

    /**
     * Guaranteed not null if isAttributeBuilder is true.
     * @return containingType of the value attribute.
     */
    @Nullable @Parameter
    protected abstract TypeElement attributeValueType();

    @Override
    public boolean isAttributeBuilder() {
      return builderModel() != null;
    }

    @Nullable
    @Override
    @Derived
    public AttributeBuilderDescriptor getAttributeBuilderDescriptor() {
      if (!isAttributeBuilder()) {
        return null;
      }

      ValueToBuilderTarget target;
      ExecutableElement copyMethod = builderModel().copyMethod();
      ExecutableElement builderMethod = builderModel().builderMethod();
      ExecutableElement buildMethod = builderModel().buildMethod();
      TypeElement attributeBuilderType = builderModel().builderType();

      if (copyMethod.getKind() == ElementKind.CONSTRUCTOR) {
        target = ValueToBuilderTarget.BUILDER_CONSTRUCTOR;
      } else if (copyMethod.getModifiers().contains(Modifier.STATIC)) {
        if (copyMethod.getEnclosingElement().equals(attributeValueType())) {
          target = ValueToBuilderTarget.VALUE_TYPE;
        } else {
          target = ValueToBuilderTarget.BUILDER_TYPE;
        }
      } else {
        if (copyMethod.getEnclosingElement().equals(attributeValueType())) {
          target = ValueToBuilderTarget.VALUE_INSTANCE;
        } else {
          target = ValueToBuilderTarget.BUILDER_INSTANCE;
        }
      }

      String qualifiedBuilderConstructorMethod;
      if (builderMethod.getEnclosingElement().equals(attributeValueType())) {
        qualifiedBuilderConstructorMethod = String.format("%s.%s",
            attributeValueType().getQualifiedName(),
            builderMethod.getSimpleName());
      } else {
        if (builderMethod.getKind() == ElementKind.CONSTRUCTOR) {
          qualifiedBuilderConstructorMethod = String.format("new %s",
              attributeBuilderType.getQualifiedName());
        } else {
          qualifiedBuilderConstructorMethod = String.format("%s.%s",
              attributeBuilderType.getQualifiedName(),
              builderMethod.getSimpleName());
        }
      }

      return ImmutableAttributeBuilderDescriptor.builder()
          .valueToBuilderTarget(target)
          .valueToBuilderMethod(copyMethod.getSimpleName().toString())
          .buildMethod(buildMethod.getSimpleName().toString())
          .qualifiedValueTypeName(attributeValueType().getQualifiedName().toString())
          .qualifiedBuilderTypeName(attributeBuilderType.getQualifiedName().toString())
          .qualifiedBuilderConstructorMethod(qualifiedBuilderConstructorMethod)
          .build();
    }

    /**
     * @return strategy which has an AttributeBuilderDescriptor if attributeValue is an attributeBuilder.
     */
    static ThirdPartyAttributeBuilderStrategy of(ValueAttribute valueAttribute) {
      TypeElement attributeValueType = valueAttribute.containedTypeElement;
      if (attributeValueType == null) {
        return ImmutableThirdPartyAttributeBuilderStrategy.of(null, null);
      }

      // Map of possible builder class to needed methods.
      Map<TypeElement, AttributeBuilderThirdPartyModel.Creator> partiallyBuiltModels = new HashMap<>();
      for (Element possibleBuilderMethodCopyMethodOrClass
          : attributeValueType.getEnclosedElements()) {
        AttributeBuilderThirdPartyModel.Creator newBuilderModel = ModifiableCreator.create();

        if (isPossibleBuilderClass(possibleBuilderMethodCopyMethodOrClass, valueAttribute)) {
          newBuilderModel.builderType((TypeElement) possibleBuilderMethodCopyMethodOrClass);
        } else if (isPossibleBuilderMethod(possibleBuilderMethodCopyMethodOrClass, true, valueAttribute)) {
          newBuilderModel
              .builderMethod((ExecutableElement) possibleBuilderMethodCopyMethodOrClass);
        } else if (isPossibleCopyMethod(valueAttribute,
            possibleBuilderMethodCopyMethodOrClass, true)) {
          newBuilderModel
              .copyMethod((ExecutableElement) possibleBuilderMethodCopyMethodOrClass);
        }

        // We found something on the loop interesting
        if (newBuilderModel.findBuilderType() != null) {
          AttributeBuilderThirdPartyModel.Creator maybeCompleteModel;

          if (partiallyBuiltModels.containsKey(newBuilderModel.findBuilderType())) {
            AttributeBuilderThirdPartyModel.Creator partiallyBuiltModel = partiallyBuiltModels
                .get(newBuilderModel.findBuilderType());
            partiallyBuiltModel.mergeFrom(newBuilderModel);
            maybeCompleteModel = partiallyBuiltModel;
          } else {
            processPossibleBuilder(valueAttribute, newBuilderModel);
            partiallyBuiltModels.put(newBuilderModel.findBuilderType(), newBuilderModel);
            maybeCompleteModel = newBuilderModel;
          }

          if (maybeCompleteModel.complete()) {
            return ImmutableThirdPartyAttributeBuilderStrategy.of(
                maybeCompleteModel.toImmutable(),
                valueAttribute.containedTypeElement);
          }
        }
      }

      return ImmutableThirdPartyAttributeBuilderStrategy.of(null, null);
    }

    // NB: because of the null checks here, we will prefer using static initialization
    // from the value object, but eh, doesn't really work that well because we may
    // break out of the value loop if this call to processPossibleBuilder completes the model.
    private static void processPossibleBuilder(ValueAttribute attribute,
        AttributeBuilderThirdPartyModel.Creator builderModel) {
      for (Element possibleBuildMethodOrConstructor : builderModel.findBuilderType().getEnclosedElements()) {

        if (builderModel.buildMethod() == null
            && isPossibleBuildMethod(attribute, possibleBuildMethodOrConstructor)) {
          builderModel.buildMethod((ExecutableElement) possibleBuildMethodOrConstructor);
        }

        if (builderModel.builderMethod() == null
            && isPossibleBuilderMethod(possibleBuildMethodOrConstructor, false, attribute)) {
          builderModel.builderMethod((ExecutableElement) possibleBuildMethodOrConstructor);
        }

        if (builderModel.copyMethod() == null
            && isPossibleCopyMethod(attribute, possibleBuildMethodOrConstructor, false)) {
          builderModel.copyMethod((ExecutableElement) possibleBuildMethodOrConstructor);
        }
      }
    }

    /**
     * Returns true if there's a public way to build the value type with an instance no-arg method.
     *
     * @param attribute value attribute to check.
     * @param possibleBuildMethod method which matches {@link StyleMirror#attributeBuilder()}
     * @return true if this is the possibleBuildMethod can build the value type.
     */
    private static boolean isPossibleBuildMethod(ValueAttribute attribute, Element possibleBuildMethod) {
      if (possibleBuildMethod.getKind() != ElementKind.METHOD) {
        return false;
      }

      if (!attribute.containingType.names().possibleAttributeBuilder(possibleBuildMethod.getSimpleName())) {
        return false;
      }

      Types typeUtils = attribute.containingType.constitution.protoclass()
          .environment()
          .processing()
          .getTypeUtils();

      ExecutableElement candidateBuildMethod = (ExecutableElement) possibleBuildMethod;
      return !candidateBuildMethod.getModifiers().contains(Modifier.STATIC)
          && candidateBuildMethod.getModifiers().contains(Modifier.PUBLIC)
          && candidateBuildMethod.getTypeParameters().isEmpty()
          && candidateBuildMethod.getReturnType().getKind() == TypeKind.DECLARED
          && typeUtils.isSameType(candidateBuildMethod.getReturnType(), attribute.containedTypeElement.asType());
    }

    /**
     * Return true if the possibleBuilderMethod matches the
     * Style#attributeBuilder() and returns a class.
     *
     * TODO: may need to make this return true if the return type is an interface too...
     *
     * @param possibleBuilderMethod executableElement
     */
    private static boolean isPossibleBuilderMethod(Element possibleBuilderMethod, boolean onValueType, ValueAttribute valueAttribute) {
      if (possibleBuilderMethod.getKind() == ElementKind.METHOD) {
        if (!valueAttribute.containingType.names().possibleAttributeBuilder(possibleBuilderMethod.getSimpleName())) {
          return false;
        }

        ExecutableElement candidateMethod = (ExecutableElement) possibleBuilderMethod;

        TypeKind kind = candidateMethod.getReturnType().getKind();

        return possibleBuilderMethod.getModifiers().containsAll(
            Arrays.asList(Modifier.STATIC, Modifier.PUBLIC))
            && candidateMethod.getParameters().isEmpty()
            && candidateMethod.getReturnType().getKind() == TypeKind.DECLARED
            && !kind.isPrimitive() && kind != TypeKind.ARRAY;

      } else if (!onValueType && possibleBuilderMethod.getKind() == ElementKind.CONSTRUCTOR) {
        if (!valueAttribute.containingType.names().newTokenInAttributeBuilder()) {
          return false;
        }

        ExecutableElement candidateConstructor = (ExecutableElement) possibleBuilderMethod;

        return candidateConstructor.getModifiers().contains(Modifier.PUBLIC)
            && candidateConstructor.getTypeParameters().isEmpty();
      }
      return false;
    }

    /**
     * Determine if inner class could be a builder.
     *
     * @param possibleBuilderClass nested value element that could be builder class.
     * @return true if it's a static inner class.
     */
    private static boolean isPossibleBuilderClass(Element possibleBuilderClass, ValueAttribute valueAttribute) {
      if (possibleBuilderClass.getKind() == ElementKind.CLASS) {
        return possibleBuilderClass.getModifiers().contains(Modifier.STATIC)
            && possibleBuilderClass.getKind() == ElementKind.CLASS
            && valueAttribute.containingType.names().possibleAttributeBuilder(possibleBuilderClass.getSimpleName());
      }

      return false;
    }

    /**
     * Applies to both builder and value candidates.
     *
     * @param valueAttribute the valueAttribute.
     * @param possibleCopyMethod candidate to check.
     */
    protected static boolean isPossibleCopyMethod(
        ValueAttribute valueAttribute,
        Element possibleCopyMethod,
        boolean onValueType) {
      if (possibleCopyMethod.getKind() == ElementKind.METHOD) {
        if (!valueAttribute.containingType.names().possibleAttributeBuilder(possibleCopyMethod.getSimpleName())) {
          return false;
        }

        ExecutableElement candidateCopyMethod = (ExecutableElement) possibleCopyMethod;

        Types typeUtils = valueAttribute.containingType.constitution.protoclass()
            .environment()
            .processing()
            .getTypeUtils();

        if (candidateCopyMethod.getParameters().size() == 1
            && typeUtils.isSameType(
                candidateCopyMethod.getParameters().get(0).asType(),
                valueAttribute.containedTypeElement.asType())) {

          TypeKind kind = candidateCopyMethod.getReturnType().getKind();
          return !kind.isPrimitive() && kind != TypeKind.ARRAY;
          // handle proto style toBuilder() copy method... lots of BuilderModels created because of this
        } else if (onValueType
            && candidateCopyMethod.getParameters().size() == 0
            && !candidateCopyMethod.getModifiers().contains(Modifier.STATIC)) {

          TypeKind kind = candidateCopyMethod.getReturnType().getKind();
          return !kind.isPrimitive() && kind != TypeKind.ARRAY;
        }
      } else if (!onValueType && possibleCopyMethod.getKind() == ElementKind.CONSTRUCTOR) {

        if (!valueAttribute.containingType.names().newTokenInAttributeBuilder()) {
          return false;
        }

        ExecutableElement candidateConstructor = (ExecutableElement) possibleCopyMethod;
        return candidateConstructor.getParameters().size() == 1
            && candidateConstructor.getParameters().get(0).asType().equals(valueAttribute.containedTypeElement.asType());
      }

      return false;
    }
  }
}


/*
   Copyright 2013-2018 Immutables Authors and Contributors

   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 org.immutables.value.processor.meta;

import com.google.common.base.Ascii;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.lang.annotation.ElementType;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.immutables.generator.AnnotationMirrors;
import org.immutables.generator.StringLiterals;
import org.immutables.generator.TypeHierarchyCollector;
import org.immutables.value.processor.encode.Instantiation;
import org.immutables.value.processor.encode.Instantiator.InstantiationCreator;
import org.immutables.value.processor.meta.AnnotationInjections.AnnotationInjection;
import org.immutables.value.processor.meta.AnnotationInjections.InjectAnnotation.Where;
import org.immutables.value.processor.meta.AnnotationInjections.InjectionInfo;
import org.immutables.value.processor.meta.Generics.Parameter;
import org.immutables.value.processor.meta.Proto.DeclaringType;
import org.immutables.value.processor.meta.Proto.Environment;
import org.immutables.value.processor.meta.Proto.MetaAnnotated;
import org.immutables.value.processor.meta.Proto.Protoclass;
import org.immutables.value.processor.meta.Reporter.About;
import org.immutables.value.processor.meta.Styles.UsingName.AttributeNames;
import org.immutables.value.processor.meta.ValueMirrors.Style.ImplementationVisibility;
import org.immutables.value.processor.meta.ValueMirrors.Style.ValidationMethod;

/**
 * It's pointless to refactor this mess until
 * 1) Some sort of type calculus toolkit used/created
 * 2) Facets/Implicits in Generator toolkit with auto-memoising implemented
 */
public final class ValueAttribute extends TypeIntrospectionBase implements HasStyleInfo {
  private static final WholeTypeVariable NON_WHOLE_TYPE_VARIABLE = new WholeTypeVariable(-1);
  private static final int CONSTRUCTOR_PARAMETER_DEFAULT_ORDER = 0;
  private static final int CONSTRUCTOR_NOT_A_PARAMETER = -1;
  private static final String GUAVA_IMMUTABLE_PREFIX = UnshadeGuava.typeString("collect.Immutable");
  private static final String VALUE_ATTRIBUTE_NAME = "value";
  private static final String ID_ATTRIBUTE_NAME = "_id";
  private static final String[] EMPTY_SERIALIZED_NAMES = {};

  public AttributeNames names;
  public boolean isGenerateDefault;
  public boolean isGenerateDerived;
  public boolean isGenerateAbstract;
  public boolean isGenerateLazy;
  public boolean isAttributeBuilder;
  public ImmutableList<String> typeParameters = ImmutableList.of();
  public ImmutableList<AnnotationInjection> annotationInjections = ImmutableList.of();
  // Replace with delegation?
  public Reporter reporter;

  public ValueType containingType;

  @Nullable
  public ValueType attributeValueType;

  TypeMirror returnType;
  Element element;
  String returnTypeName;
  // Set only if isAttributeBuilder is true
  @Nullable
  private AttributeBuilderDescriptor attributeBuilderDescriptor;

  public boolean hasEnumFirstTypeParameter;

  @Nullable
  TypeElement containedTypeElement;

  @Nullable
  private TypeElement containedSecondaryTypeElement;

  private boolean generateOrdinalValueSet;
  private TypeMirror arrayComponent;

  @Nullable
  private NullabilityAnnotationInfo nullability;

  @Nullable
  private NullabilityAnnotationInfo nullabilityInSupertype;

  @Nullable
  private String rawTypeName;

  public String name() {
    return names.var;
  }

  public boolean isBoolean() {
    return returnType.getKind() == TypeKind.BOOLEAN;
  }

  public boolean isInt() {
    return returnType.getKind() == TypeKind.INT;
  }

  public boolean isShort() {
    return returnType.getKind() == TypeKind.SHORT;
  }

  public boolean isChar() {
    return returnType.getKind() == TypeKind.CHAR;
  }

  public boolean isByte() {
    return returnType.getKind() == TypeKind.BYTE;
  }

  public boolean isLong() {
    return returnType.getKind() == TypeKind.LONG;
  }

  public boolean isStringType() {
    return String.class.getName().equals(rawTypeName);
  }

  public boolean charType() {
    return returnType.getKind() == TypeKind.CHAR;
  }

  public String atNullability() {
    return nullability != null ? nullability.asPrefix() : "";
  }

  public String atNullabilityOriginal() {
    return nullability != null ? nullability.asPrefixOriginal() : "";
  }

  public String atNullabilityLocal() {
    return nullability != null ? nullability.asLocalPrefix() : "";
  }

  public boolean isSimpleLiteralType() {
    return isPrimitive()
        || isStringType()
        || isEnumType();
  }

  public boolean hasSimpleScalarElementType() {
    ensureTypeIntrospected();

    String type = getWrappedElementType();
    return isStringType()
        || String.class.getName().equals(type)
        || isPrimitiveWrappedType(type)
        || hasEnumContainedElementType()
        || isEnumType()
        || isJdkSpecializedOptional()
        || extendedClassesNames.contains(Number.class.getName());
  }

  public boolean requiresAlternativeStrictConstructor() {
    return typeKind.isCollectionKind()
        || (typeKind.isMappingKind()
            && !typeKind.isPlainMapKind()
            && !typeKind.isMultimap());
  }

  public boolean isMandatory() {
    return isGenerateAbstract
        && !isGenerateDefault // is the case for defaulted abstract annotation attribute
        && !isContainerType()
        && !isNullable()
        && !isEncoding()
        && !hasBuilderSwitcherDefault()
        && !(isPrimitive() && protoclass().styles().style().validationMethod() != ValidationMethod.SIMPLE);
  }

  /**
   * Checks if type defined by this attribute has associated criteria (see {@code @Criteria})
   */
  public boolean hasCriteria() {
    return attributeValueType != null && attributeValueType.isGenerateCriteria();
  }

  public boolean isNullable() {
    return nullability != null;
  }

  public ImmutableList<String> docComment = ImmutableList.of();

  public boolean deprecated;

  public boolean isMaybeComparableKey() {
    return isContainerType()
        && (super.isComparable() || this.containedTypeElement == null);
  }

  @Override
  public boolean isComparable() {
    return isNumberType()
        || isStringType()
        || (!(isCollectionType() || isMapType()) && super.isComparable());
  }

  private List<CharSequence> jsonQualifierAnnotations;

  public List<CharSequence> getJsonQualiferAnnotations() {
    if (jsonQualifierAnnotations == null) {
      List<CharSequence> annotationStrings = Collections.emptyList();
      for (AnnotationMirror m : element.getAnnotationMirrors()) {
        if (MetaAnnotated.from(m, protoclass().environment()).isJsonQualifier()) {
          if (annotationStrings.isEmpty()) {
            annotationStrings = Lists.newArrayList();
          }
          annotationStrings.add(AnnotationMirrors.toCharSequence(m));
        }
      }
      jsonQualifierAnnotations = annotationStrings;
    }
    return jsonQualifierAnnotations;
  }

  @Nullable
  private String serializedName;

  private String[] alternateSerializedNames = EMPTY_SERIALIZED_NAMES;

  public String[] getAlternateSerializedNames() {
    getSerializedName();// trigger lazy init
    return alternateSerializedNames;
  }

  /**
   * Serialized name, actully specified via annotation
   * @return name for JSON as overriden.
   */
  public String getSerializedName() {
    if (serializedName == null) {
      Optional<SerializedNameMirror> serializedNameAnnotation = SerializedNameMirror.find(element);
      if (serializedNameAnnotation.isPresent()) {
        SerializedNameMirror m = serializedNameAnnotation.get();
        serializedName = m.value();
        alternateSerializedNames = m.alternate();
        return serializedName;
      }
      Optional<NamedMirror> namedAnnotation = NamedMirror.find(element);
      if (namedAnnotation.isPresent()) {
        String value = namedAnnotation.get().value();
        if (!value.isEmpty()) {
          serializedName = value;
          return serializedName;
        }
      }
      Optional<OkNamedMirror> okNamedAnnotation = OkNamedMirror.find(element);
      if (okNamedAnnotation.isPresent()) {
        String value = okNamedAnnotation.get().name();
        if (!value.isEmpty()) {
          serializedName = value;
          return serializedName;
        }
      }
      if (isMarkedAsMongoId()) {
        serializedName = ID_ATTRIBUTE_NAME;
        return serializedName;
      }
      serializedName = "";
      return serializedName;
    }
    return serializedName;
  }

  /**
   * Marshaled name for compatibility with repository.
   * @return get JSON name either specified or default.
   */
  public String getMarshaledName() {
    String serializedName = getSerializedName();
    if (!serializedName.isEmpty()) {
      return serializedName;
    }
    return names.raw;
  }

  private @Nullable Boolean isGsonOther = null;

  public boolean isGsonOther() {
    if (isGsonOther == null) {
      if (GsonOtherMirror.isPresent(element)) {
        if (!isGenerateAbstract || !rawTypeName.equals(GsonMirrors.JSON_OBJECT_TYPE)) {
          report().error(
              "@Gson.Other attribute must be abstract accessor of type %s",
              GsonMirrors.JSON_OBJECT_TYPE);
          isGsonOther = false;
        } else {
          isGsonOther = true;
        }
      } else {
        isGsonOther = false;
      }
    }
    return isGsonOther;
  }

  public boolean isForcedEmpty() {
    return !containingType.gsonTypeAdapters().emptyAsNulls();
  }

  @Override
  protected TypeMirror internalTypeMirror() {
    return returnType;
  }

  public String getType() {
    return returnTypeName;
  }

  public List<CharSequence> getAnnotations() {
    if (containingType.isGenerateJacksonProperties()) {
      return extractAnnotationsForElement(
          ElementType.METHOD,
          protoclass().styles().style().passAnnotationsNames());

    }
    return Annotations.getAnnotationLines(element,
        protoclass().styles().style().passAnnotationsNames(),
        false,
        ElementType.METHOD,
        importsResolver,
        nullability);
  }

  public List<CharSequence> getFieldAnnotations() {
    return Annotations.getAnnotationLines(element,
        protoclass().styles().style().passAnnotationsNames(),
        false,
        ElementType.FIELD,
        importsResolver,
        null/* do not propagate nullable here */);
  }

  public CharSequence getConstructorParameterAnnotations() {
    List<CharSequence> annotations =
        Annotations.getAnnotationLines(element,
            protoclass().styles().style().passAnnotationsNames(),
            false,
            ElementType.PARAMETER,
            importsResolver,
            nullability);

    if (!annotations.isEmpty()) {
      return Joiner.on(' ').join(annotations).concat(" ");
    }

    return "";
  }

  public List<CharSequence> getJacksonFieldsAnnotations() {
    return extractAnnotationsForElement(ElementType.FIELD, Collections.<String>emptySet());
  }

  private CharSequence jacksonPropertyAnnotation() {
    StringBuilder propertyAnnotation = new StringBuilder("@").append(Annotations.JACKSON_PROPERTY);
    if (protoclass().styles().style().forceJacksonPropertyNames()) {
      propertyAnnotation.append('(').append(StringLiterals.toLiteral(names.raw)).append(')');
    }
    return propertyAnnotation;
  }

  public List<CharSequence> getBuilderAttributeAnnotation() {
    if (containingType.isGenerateJacksonProperties()
        && protoclass().isJacksonDeserialized()) {
      List<CharSequence> jacksonPropertyAnnotation = Annotations.getAnnotationLines(element,
          Collections.singleton(Annotations.JACKSON_PROPERTY),
          false,
          ElementType.METHOD,
          importsResolver,
          nullability);
      List<CharSequence> annotations = Lists.newArrayList();
      if (jacksonPropertyAnnotation.isEmpty()) {
        annotations.add(jacksonPropertyAnnotation());
      }
      annotations.addAll(Annotations.getAnnotationLines(element,
          Collections.<String>emptySet(),
          protoclass().environment().hasJacksonLib(),
          ElementType.METHOD,
          importsResolver,
          nullability));
      return annotations;
    }
    return ImmutableList.of();
  }

  private List<CharSequence> extractAnnotationsForElement(ElementType elementType, Set<String> additionalAnnotations) {
    List<CharSequence> allAnnotations = Lists.newArrayListWithCapacity(1);

    boolean dontHaveJsonPropetyAnnotationAlready = Annotations.getAnnotationLines(element,
        Collections.singleton(Annotations.JACKSON_PROPERTY),
        false,
        elementType,
        importsResolver,
        nullability).isEmpty();

    if (dontHaveJsonPropetyAnnotationAlready) {
      allAnnotations.add(jacksonPropertyAnnotation());
    }

    allAnnotations.addAll(
        Annotations.getAnnotationLines(element,
            Sets.union(additionalAnnotations,
                protoclass().styles().style().additionalJsonAnnotationsNames()),
            protoclass().environment().hasJacksonLib(),
            elementType,
            importsResolver,
            nullability));

    return allAnnotations;
  }

  public boolean isJsonIgnore() {
    return IgnoreMirror.isPresent(element)
        || OkIgnoreMirror.isPresent(element);
  }

  public List<String> typeParameters() {
    ensureTypeIntrospected();
    return arrayComponent != null ? ImmutableList.of(arrayComponent.toString()) : typeParameters;
  }

  public boolean isMapType() {
    return typeKind.isMappingKind();
  }

  public boolean isMultimapType() {
    return typeKind.isMultimapKind();
  }

  public boolean isListType() {
    return typeKind.isList();
  }

  private OrderKind orderKind = OrderKind.NONE;

  private enum OrderKind {
    NONE, NATURAL, REVERSE
  }

  public boolean isSetType() {
    return typeKind.isSet();
  }

  public boolean hasNaturalOrder() {
    return orderKind == OrderKind.NATURAL;
  }

  public boolean hasReverseOrder() {
    return orderKind == OrderKind.REVERSE;
  }

  public boolean isSortedSetType() {
    return typeKind.isSortedSet();
  }

  public boolean isSortedMapType() {
    return typeKind.isSortedMap();
  }

  public boolean isGenerateSortedSet() {
    return typeKind.isSortedSet();
  }

  public boolean isGenerateSortedMap() {
    return typeKind.isSortedMap();
  }

  public boolean isGenerateSortedMultiset() {
    return typeKind.isSortedMultiset();
  }

  private void checkOrderAnnotations() {
    Optional<NaturalOrderMirror> naturalOrderAnnotation = NaturalOrderMirror.find(element);
    Optional<ReverseOrderMirror> reverseOrderAnnotation = ReverseOrderMirror.find(element);

    if (naturalOrderAnnotation.isPresent() && reverseOrderAnnotation.isPresent()) {
      report()
          .error("@Value.Natural and @Value.Reverse annotations cannot be used on the same attribute");
    } else if (naturalOrderAnnotation.isPresent()) {
      configureOrdering(OrderKind.NATURAL, NaturalOrderMirror.simpleName());
    } else if (reverseOrderAnnotation.isPresent()) {
      configureOrdering(OrderKind.REVERSE, ReverseOrderMirror.simpleName());
    }
  }

  private void configureOrdering(OrderKind orderKind, String annotationName) {
    if (typeKind.isSortedKind()) {
      if (isMaybeComparableKey()) {
        this.orderKind = orderKind;
      } else {
        reportOrderingError(annotationName, "requires that a (multi)set's elements or a map's keys are Comparable");
      }
    } else {
      reportOrderingError(annotationName, "can be applied only to SortedSet, SortedMap and SortedMultiset attributes");
    }
  }

  private void reportOrderingError(String annotationName, String msg) {
    report().annotationNamed(annotationName).error(String.format("@Value.%s %s", annotationName, msg));
  }

  public boolean isJdkOptional() {
    return typeKind.isOptionalKind() && typeKind.isJdkOnlyContainerKind();
  }

  public boolean isFugueOptional() {
    return typeKind.isOptionFugue();
  }

  public boolean isGuavaOptional() {
    return typeKind.isOptionalGuava();
  }

  public boolean isJavaslangOptional() {
    return typeKind.isOptionJavaslang();
  }

  public boolean isJdkSpecializedOptional() {
    return typeKind.isOptionalSpecializedJdk();
  }

  public boolean isOptionalType() {
    return typeKind.isOptionalKind();
  }

  public boolean isCollectionType() {
    return typeKind.isCollectionKind();
  }

  public boolean isGenerateEnumSet() {
    return typeKind.isEnumSet();
  }

  public boolean isGuavaImmutableDeclared() {
    return typeKind.isContainerKind() && rawTypeName.startsWith(GUAVA_IMMUTABLE_PREFIX);
  }

  @Nullable
  private String defaultInterface;

  public String defaultInterface() {
    if (defaultInterface == null) {
      defaultInterface = inferDefaultInterface();
    }
    return defaultInterface;
  }

  private String inferDefaultInterface() {
    if (isInterfaceDefaultMethod()) {
      if (containingType.element.getKind() == ElementKind.INTERFACE) {
        return containingType.typeAbstract().relativeRaw();
      }
    }
    return "";
  }

  public boolean isInterfaceDefaultMethod() {
    return element.getEnclosingElement().getKind() == ElementKind.INTERFACE
        && !element.getModifiers().contains(Modifier.ABSTRACT);
  }

  public boolean isGenerateTransientDerived() {
    return isGenerateDerived
        && style().transientDerivedFields()
        && !containingType.serial.isSimple();
  }

  public boolean isGenerateEnumMap() {
    return typeKind.isEnumMap();
  }

  public boolean hasVirtualImpl() {
    return isEncoding() && instantiation.hasVirtualImpl();
  }

  public String getUnwrappedElementType() {
    return isContainerType() && nullElements.ban()
        ? unwrapType(firstTypeParameter())
        : getElementType();
  }

  public String getUnwrappedValueElementType() {
    return isMapType()
        ? getUnwrappedSecondaryElementType()
        : getUnwrappedElementType();
  }

  public String getWrappedElementType() {
    if (returnType.getKind().isPrimitive()) {
      return wrapType(Ascii.toLowerCase(returnType.getKind().name()));
    }
    return wrapType(hasContainedElementType()
        ? firstTypeParameter()
        : returnTypeName);
  }

  private boolean hasContainedElementType() {
    return isArrayType() || isContainerType();
  }

  public String getRawType() {
    return rawTypeName;
  }

  public String getConsumedElementType() {
    return (isUnwrappedElementPrimitiveType()
        || isStringType()
        || (hasContainedElementType() && firstTypeParameter().equals(String.class.getName()))
        || hasEnumFirstTypeParameter())
            ? getWrappedElementType()
            : "? extends " + getWrappedElementType();
  }

  public boolean hasEnumFirstTypeParameter() {
    return typeKind().isEnumKeyed()
        && containedTypeElement.getKind() == ElementKind.ENUM;
  }

  private String extractRawType(String className) {
    String rawType = className;
    int indexOfGenerics = rawType.indexOf('<');
    if (indexOfGenerics > 0) {
      rawType = rawType.substring(0, indexOfGenerics);
    }
    int endOfTypeAnnotations = rawType.lastIndexOf(' ');
    if (endOfTypeAnnotations > 0) {
      rawType = rawType.substring(endOfTypeAnnotations + 1);
    }
    return rawType;
  }

  public boolean isUnwrappedElementPrimitiveType() {
    return isPrimitiveType(getUnwrappedElementType());
  }

  public boolean isUnwrappedSecondaryElementPrimitiveType() {
    return isPrimitiveType(getUnwrappedSecondaryElementType());
  }

  public String firstTypeParameter() {
    return Iterables.getFirst(typeParameters(), "");
  }

  public String secondTypeParameter() {
    return Iterables.get(typeParameters(), 1);
  }

  public String getElementType() {
    return hasContainedElementType() ? firstTypeParameter() : returnTypeName;
  }

  @Nullable
  private List<String> expectedSubtypes;

  public List<String> getExpectedSubtypes() {
    if (expectedSubtypes == null) {
      ensureTypeIntrospected();
      if (containedTypeElement != null || containedSecondaryTypeElement != null) {
        TypeElement supertypeElement = MoreObjects.firstNonNull(containedSecondaryTypeElement, containedTypeElement);
        Optional<ExpectedSubtypesMirror> annotationOnAttribute = ExpectedSubtypesMirror.find(element);
        if (annotationOnAttribute.isPresent()) {
          expectedSubtypes = ImmutableList.copyOf(annotationOnAttribute.get().valueName());
          if (expectedSubtypes.isEmpty()) {
            expectedSubtypes = tryFindSubtypes(supertypeElement);
          }
        } else {
          Optional<ExpectedSubtypesMirror> annotationOnType = ExpectedSubtypesMirror.find(supertypeElement);
          if (annotationOnType.isPresent()) {
            expectedSubtypes = ImmutableList.copyOf(annotationOnType.get().valueName());
            if (expectedSubtypes.isEmpty()) {
              expectedSubtypes = tryFindSubtypes(supertypeElement);
            }
          }
        }
      }
      if (expectedSubtypes == null) {
        expectedSubtypes = ImmutableList.of();
      }
    }
    return expectedSubtypes;
  }

  private ImmutableList<String> tryFindSubtypes(TypeElement supertypeElement) {
    ValueType surroundingType =
        MoreObjects.firstNonNull(containingType.enclosingValue, containingType);
    Set<ValueType> subtypes =
        surroundingType.getCases().knownSubtypesOf(supertypeElement.getQualifiedName().toString());
    ImmutableList.Builder<String> builder = ImmutableList.builder();
    for (ValueType valueType : subtypes) {
      builder.add(valueType.typeAbstract().toString());
    }
    return builder.build();
  }

  public boolean isGenerateJdkOnly() {
    return containingType.isGenerateJdkOnly()
        && !typeKind.isGuavaContainerKind()
        && !isGuavaImmutableDeclared()
        && !isCustomCollectionType();
  }

  public boolean isGenerateOrdinalValueSet() {
    if (!isSetType()) {
      return false;
    }
    ensureTypeIntrospected();
    return generateOrdinalValueSet;
  }

  public boolean isArrayType() {
    return typeKind.isArray();
  }

  public boolean isReferenceArrayType() {
    return isArrayType() && !isPrimitiveType(firstTypeParameter());
  }

  public boolean isPrimitiveArrayType() {
    return isArrayType() && isPrimitiveType(firstTypeParameter());
  }

  public boolean isOptionalAcceptNullable() {
    return isOptionalType()
        && !typeKind.isOptionalSpecializedJdk()
        && containingType.isOptionalAcceptNullable();
  }

  @Override
  protected void introspectType() {
    TypeMirror typeMirror = returnType;

    // Special case for primitive Optional, may become a pattern for specialized types
    if (typeKind.isOptionalSpecializedJdk()) {
      typeParameters = ImmutableList.of(optionalSpecializedType());
      // no delegation to introspect further
      return;
    }

    if (isContainerType()) {
      if (typeMirror.getKind() == TypeKind.DECLARED
          || typeMirror.getKind() == TypeKind.ERROR) {

        DeclaredType declaredType = (DeclaredType) typeMirror;

        List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();

        if (!typeArguments.isEmpty()) {
          final TypeMirror typeArgument = typeArguments.get(0);

          if (isSetType() && protoclass().environment().hasOrdinalModule()) {
            this.generateOrdinalValueSet = new TypeIntrospectionBase() {
              @Override
              protected TypeMirror internalTypeMirror() {
                return typeArgument;
              }

              @Override
              protected TypeHierarchyCollector collectTypeHierarchy(TypeMirror typeMirror) {
                TypeHierarchyCollector collector = containingType.createTypeHierarchyCollector(reporter, element);
                collector.collectFrom(typeMirror);
                return collector;
              }
            }.isOrdinalValue();
          }
          if (isMapType()) {
            TypeMirror typeSecondArgument = typeArguments.get(1);
            if (typeSecondArgument.getKind() == TypeKind.DECLARED) {
              TypeElement typeElement = (TypeElement) ((DeclaredType) typeSecondArgument).asElement();
              this.containedSecondaryTypeElement = typeElement;
            }
          }

          typeMirror = typeArgument;
        }
      }
    } else if (isArrayType()) {
      arrayComponent = ((ArrayType) typeMirror).getComponentType();
      typeMirror = arrayComponent;
    }

    if (typeMirror.getKind() == TypeKind.DECLARED) {
      TypeElement typeElement = (TypeElement) ((DeclaredType) typeMirror).asElement();
      this.containedTypeElement = typeElement;
    }

    introspectTypeMirror(typeMirror);
    introspectSupertypes();
  }

  private String optionalSpecializedType() {
    switch (typeKind) {
    case OPTIONAL_INT_JDK:
      return int.class.getName();
    case OPTIONAL_LONG_JDK:
      return long.class.getName();
    case OPTIONAL_DOUBLE_JDK:
      return double.class.getName();
    default:
      throw new AssertionError();
    }
  }

  public AttributeTypeKind typeKind() {
    return typeKind;
  }

  public boolean isPrimitiveWrapperType() {
    return isPrimitiveWrappedType(returnTypeName);
  }

  private static boolean isRegularMarshalableType(String name) {
    return String.class.getName().equals(name)
        || isPrimitiveType(name);
  }

  public boolean isRequiresMarshalingAdapter() {
    return !isRegularMarshalableType(getElementType())
        || isPrimitiveArrayType();
  }

  public boolean isRequiresMarshalingSecondaryAdapter() {
    return isMapType() && !isRegularMarshalableType(getSecondaryElementType());
  }

  public boolean wrapArrayToIterable() {
    return containingType.isGenerateJdkOnly()
        || isUnwrappedElementPrimitiveType()
        || !(typeKind.isList() || typeKind.isSet() || typeKind.isMultiset());
  }

  /**
   * Suitable for JavaDocs, intemediate name mangling and for Guava intergration.
   * @return the raw collection type
   */
  public String getRawCollectionType() {
    return typeKind.rawSimpleName(rawTypeName);
  }

  public boolean isMultisetType() {
    return typeKind.isMultisetKind();
  }

  public boolean isCustomCollectionType() {
    return typeKind.isCustomCollection();
  }

  public String getRawMapType() {
    return typeKind.rawSimpleName(rawTypeName);
  }

  public String getSecondaryElementType() {
    return secondTypeParameter();
  }

  public String getUnwrappedSecondaryElementType() {
    return nullElements.ban()
        ? unwrapType(secondTypeParameter())
        : getSecondaryElementType();
  }

  public String getWrappedSecondaryElementType() {
    return wrapType(secondTypeParameter());
  }

  public String getUnwrapperOrRawSecondaryElementType() {
    return extractRawType(getWrappedSecondaryElementType());
  }

  public String getUnwrapperOrRawElementType() {
    return extractRawType(getWrappedElementType());
  }

  public boolean isNumberType() {
    TypeKind kind = returnType.getKind();
    return kind.isPrimitive()
        && kind != TypeKind.CHAR
        && kind != TypeKind.BOOLEAN;
  }

  public boolean isFloatType() {
    return isFloat() || isDouble();
  }

  public boolean isFloat() {
    return returnType.getKind() == TypeKind.FLOAT;
  }

  public boolean isDouble() {
    return returnType.getKind() == TypeKind.DOUBLE;
  }

  public boolean hasTypeAnnotations() {
    return returnTypeName.indexOf('@') >= 0;
  }

  public boolean isNonRawElementType() {
    return getElementType().indexOf('<') > 0;
  }

  public boolean isNonRawSecondaryElementType() {
    return getSecondaryElementType().indexOf('<') > 0;
  }

  public boolean isContainerType() {
    // TBD replace with typeKind.isContainerKind() ?
    return isCollectionType()
        || isOptionalType()
        || isMapType();
  }

  public String getWrapperType() {
    return isPrimitive()
        ? wrapType(rawTypeName)
        : returnTypeName;
  }

  public boolean isPrimitive() {
    return returnType.getKind().isPrimitive();
  }

  public boolean isAttributeBuilder() {
    return isAttributeBuilder;
  }

  // undefined value is any less than CONSTRUCTOR_NOT_A_PARAMETER
  private int parameterOrder = Integer.MIN_VALUE;

  private AttributeTypeKind typeKind;
  public boolean jacksonAnyGetter;
  public boolean jacksonValue;
  public boolean hasTypeVariables;
  private ImportsTypeStringResolver importsResolver;

  public @Nullable Instantiation instantiation;

  int getConstructorParameterOrder() {
    boolean parameterOrderIsUndefined = parameterOrder < CONSTRUCTOR_NOT_A_PARAMETER;

    if (parameterOrderIsUndefined) {
      parameterOrder = computeConstructorParameterOrder();
    }

    return parameterOrder;
  }

  private int computeConstructorParameterOrder() {
    boolean enabledAsAllParameters =
        style().allParameters();

    boolean enabledAsAllMandatoryParameters =
        style().allMandatoryParameters() && isMandatory();

    Optional<ParameterMirror> parameterAnnotation = ParameterMirror.find(element);
    if (parameterAnnotation.isPresent()) {
      if (!parameterAnnotation.get().value()) {
        // Cancelled out parameter identified, can return immediately
        return CONSTRUCTOR_NOT_A_PARAMETER;
      }

      int order = parameterAnnotation.get().order();
      boolean orderActuallySpecified = order >= CONSTRUCTOR_PARAMETER_DEFAULT_ORDER;
      // when order is unspecified it would be -1, but we should not treat is as
      // CONSTRUCTOR_NOT_A_PARAMETER, so we assign CONSTRUCTOR_PARAMETER_DEFAULT_ORDER
      // in this case
      if (orderActuallySpecified) {
        return order;
      }
      // We issue this warning only when order is not specified explicitly
      // and it is not a cancelling-out annotation thus the annoation
      // is truly superfluos when allParameters enabled
      if (enabledAsAllParameters) {
        report().annotationNamed(ParameterMirror.simpleName())
            .warning(About.INCOMPAT,
                "Annotation @Value.Parameter is superfluous when Style(allParameters = true)");
      }
      if (enabledAsAllMandatoryParameters) {
        report().annotationNamed(ParameterMirror.simpleName())
            .warning(About.INCOMPAT,
                "Annotation @Value.Parameter is superfluous when Style(allMandatoryParameters = true)"
                    + " and it is mandatory");
      }
      return CONSTRUCTOR_PARAMETER_DEFAULT_ORDER;
    }
    if (enabledAsAllParameters || enabledAsAllMandatoryParameters) {
      return CONSTRUCTOR_PARAMETER_DEFAULT_ORDER;
    }
    if (isAnnotationValueAttribute() && thereAreNoOtherMandatoryAttributes()) {
      // for annotation type, if annotation only contain single mandatory
      // attribute which is called 'value' it will be automatically turned
      // into constructor parameter
      return CONSTRUCTOR_PARAMETER_DEFAULT_ORDER;
    }
    return CONSTRUCTOR_NOT_A_PARAMETER;
  }

  private boolean isAnnotationValueAttribute() {
    return containingType.isAnnotationType()
        && names.get.equals(VALUE_ATTRIBUTE_NAME);
  }

  boolean thereAreNoOtherMandatoryAttributes() {
    List<ValueAttribute> mandatories = containingType.getMandatoryAttributes();
    for (ValueAttribute m : mandatories) {
      if (m != this) {
        return false;
      }
    }
    return true;
  }

  public String toSignature() {
    StringBuilder signature = new StringBuilder();

    signature.append(getAccess());

    return signature.append(returnTypeName)
        .append(" ")
        .append(names.get)
        .append("()")
        .toString();
  }

  public String getAccess() {
    if (element.getModifiers().contains(Modifier.PUBLIC)
        || style().visibility() == ImplementationVisibility.PUBLIC) {
      return "public ";
    } else if (element.getModifiers().contains(Modifier.PROTECTED)) {
      return "protected ";
    }
    return "";
  }

  public String getIntializerAccess() {
    if (style().stagedBuilder()
        || style().alwaysPublicInitializers()
        || element.getModifiers().contains(Modifier.PUBLIC)) {
      return "public ";
    }
    if (element.getModifiers().contains(Modifier.PROTECTED)) {
      return "protected ";
    }
    return "";
  }

  public boolean isPrimitiveElement() {
    return isPrimitiveType(getUnwrappedElementType());
  }

  public boolean isSafeUncheckedCovariantCast() {
    return isOptionalType()
        && !getConsumedElementType().equals(getWrappedElementType());
  }

  public boolean isAuxiliary() {
    return AuxiliaryMirror.isPresent(element);
  }

  public boolean isEncoding() {
    return typeKind == AttributeTypeKind.ENCODING;
  }

  private boolean isMarkedAsMongoId() {
    return IdMirror.isPresent(element);
  }

  boolean isIdAttribute() {
    return isMarkedAsMongoId()
        || ID_ATTRIBUTE_NAME.equals(getSerializedName());
  }

  private boolean isRedacted() {
    return RedactedMirror.isPresent(element);
  }

  public boolean isRedactedCompletely() {
    return isRedacted()
        && getRedactedMask().isEmpty();
  }

  public String getRedactedMask() {
    return isRedacted()
        ? protoclass().styles().style().redactedMask()
        : "";
  }

  /**
   * Initialized Validates things that were not validated otherwise
   * @param instantiationCreator can instantiate encodings
   */
  void initAndValidate(@Nullable InstantiationCreator instantiationCreator) {
    initTypeName();

    if (instantiationCreator != null
        && !isGenerateLazy) {
      this.instantiation = instantiationCreator.tryInstantiateFor(
          reporter,
          returnTypeName,
          names,
          containingType,
          isGenerateDefault || isGenerateDerived);
    }

    initTypeKind();
    initOrderKind();
    initBuilderParamsIfApplicable();
    initMiscellaneous();

    initSpecialAnnotations();
    validateThrowsClause();
    validateTypeAndAnnotations();

    initAttributeValueType();
    if (supportBuiltinContainerTypes()) {
      initImmutableCopyOf();
    }

    initAttributeBuilder();
  }

  private Set<String> thrownCheckedExceptions = ImmutableSet.of();

  private void validateThrowsClause() {
    if (element.getKind() != ElementKind.METHOD) {
      return;
    }
    ImmutableSet<String> checkedExceptions = collectThrownCheckedExceptions();
    if (!checkedExceptions.isEmpty()) {
      if (isGenerateLazy) {
        this.thrownCheckedExceptions = checkedExceptions;
      } else {
        report().warning(About.INCOMPAT,
            "Checked exceptions in 'throws' clause are not supported for regular abstract,"
                + " @Value.Derived, and @Value.Default attributes due to implementation difficulties"
                + " and unclear semantics. This message reported as a warning to preserve compatibility,"
                + " but there are high chances you'll see compile error in generated classed caused by this."
                + " NOTE: for @Value.Lazy attributes, checked exceptions are supported by simple propagation:"
                + " neither runtime nor checked exceptions will not be memoised for lazy attributes,"
                + " leading to lazy computation to be re-evaluated again and again until regular value"
                + " returned and memoised.");
      }
    }
  }

  private ImmutableSet<String> collectThrownCheckedExceptions() {
    Set<String> exceptions = new HashSet<>(2);
    Environment env = protoclass().environment();
    for (TypeMirror thrown : ((ExecutableElement) element).getThrownTypes()) {
      if (env.isCheckedException(thrown)) {
        exceptions.add(thrown.toString());
      }
    }
    return ImmutableSet.copyOf(exceptions);
  }

  public Set<String> getThrownCheckedExceptions() {
    return thrownCheckedExceptions;
  }

  private void initAttributeBuilder() {
    AttributeBuilderReflection attributeBuilderReflection =
        AttributeBuilderReflection.forValueType(this);
    isAttributeBuilder = attributeBuilderReflection.isAttributeBuilder();

    if (isAttributeBuilder) {
      attributeBuilderDescriptor = attributeBuilderReflection.getAttributeBuilderDescriptor();
    }
  }

  private void initImmutableCopyOf() {
    ensureTypeIntrospected();
    this.isGenerateImmutableCopyOf = containingType.kind().isValue()
        && !style().immutableCopyOfRoutinesNames().isEmpty()
        && (typeKind.isRegular() || typeKind.isOptionalKind())
        && !isPrimitiveOrWrapped(rawTypeName)
        && !isUnwrappedElementPrimitiveType()
        && !isStringType()
        && !isEnumType()
        && attributeValueType == null;
  }

  private void initOrderKind() {
    if (typeKind.isSortedKind()) {
      checkOrderAnnotations();
      if (orderKind == OrderKind.NONE) {
        this.typeKind = AttributeTypeKind.REGULAR;
      }
    }
  }

  private void initTypeName() {
    this.importsResolver = new ImportsTypeStringResolver(protoclass().declaringType().orNull(), getDeclaringType());
    this.importsResolver.hierarchyTraversalForUnresolvedTypes(
        protoclass().environment().round(),
        this.containingType.extendedClasses(),
        this.containingType.implementedInterfaces(),
        this.containingType.unresolvedYetArguments);

    TypeStringProvider provider = new TypeStringProvider(
        reporter,
        element,
        returnType,
        importsResolver,
        protoclass().constitution().generics().vars(),
        null);

    provider.sourceExtractionCache = containingType;
    provider.forAttribute = true;
    provider.processNestedTypeUseAnnotations = true;
    provider.process();

    this.hasSomeUnresolvedTypes = provider.hasSomeUnresovedTypes();
    this.rawTypeName = provider.rawTypeName();
    this.returnTypeName = provider.returnTypeName();
    this.typeParameters = provider.typeParameters();
    this.hasTypeVariables = provider.hasTypeVariables;
    this.nullElements = provider.nullElements;
    if (provider.nullableTypeAnnotation) {
      this.nullability = NullabilityAnnotationInfo.forTypeUse();
    }
  }

  public NullElements nullElements = NullElements.BAN;
  public boolean isSuppressedOptional;

  public enum NullElements {
    BAN,
    ALLOW,
    SKIP,
    NOP;

    public boolean ban() {
      return this == BAN;
    }

    public boolean allow() {
      return this == ALLOW || this == NOP;
    }

    public boolean skip() {
      return this == SKIP;
    }
  }

  private void initAttributeValueType() {

    if ((style().deepImmutablesDetection()
        || style().attributeBuilderDetection()
        || containingType.isGenerateCriteria())
        && containedTypeElement != null) {
      // prevent recursion in case we have the same type
      if (CachingElements.equals(containedTypeElement, containingType.element)) {
        // We don't propagate type arguments so we don't support it, sorry
        if (containingType.generics().isEmpty()) {
          this.attributeValueType = containingType;
        }
      } else {
        Environment environment = protoclass().environment();
        for (Protoclass p : environment.protoclassesFrom(Collections.singleton(containedTypeElement))) {
          if ((p.kind().isDefinedValue() || p.kind().isModifiable())
              && canAccessImplementation(p)
              && p.constitution().generics().isEmpty()) {
            this.attributeValueType = environment.composeValue(p);
          }
          break;
        }
      }
    }
  }

  private boolean canAccessImplementation(Protoclass p) {
    return p.constitution().implementationVisibility().isPublic()
        || (!p.constitution().implementationVisibility().isPrivate()
            && p.constitution().implementationPackage().equals(p.constitution().definingPackage()));
  }

  public String implementationModifiableType() {
    if (isAttributeValueKindModifyFrom()) {
      return attributeValueType.constitution.typeModifiable().toString();
    }
    return getType();
  }

  public String implementationType() {
    if (isAttributeValueKindCopy()) {
      return attributeValueType.typeValue().toString();
    }
    return getType();
  }

  public boolean hasAttributeValue() {
    return attributeValueType != null;
  }

  public boolean attributeValueKindIsCollectionOfModifiable() {
    return attributeValueType != null
        && typeKind.isCollectionKind()
        && attributeValueType.kind().isModifiable()
        && attributeValueType.isGenerateFilledFrom();
  }

  public boolean isAttributeValueKindCopy() {
    return attributeValueType != null
        && typeKind.isRegular()
        && attributeValueType.kind().isValue()
        && attributeValueType.isUseCopyConstructor();
  }

  public boolean isAttributeValueKindModifyFrom() {
    return attributeValueType != null
        && typeKind.isRegular()
        && attributeValueType.kind().isModifiable()
        && attributeValueType.isGenerateFilledFrom();
  }

  public Set<ValueAttribute> getConstructorParameters() {
    if (attributeValueType != null && attributeValueType.isUseConstructor()) {
      return attributeValueType.getConstructorArguments();
    }
    return Collections.emptySet();
  }

  private void initTypeKind() {
    if (instantiation != null) {
      typeKind = AttributeTypeKind.ENCODING;
    } else if (isGenerateDerived) {
      typeKind = AttributeTypeKind.REGULAR;
      ensureTypeIntrospected();
    } else if (returnType.getKind() == TypeKind.ARRAY) {
      typeKind = AttributeTypeKind.ARRAY;
      ensureTypeIntrospected();
    } else {
      typeKind = AttributeTypeKind.forRawType(rawTypeName);
      ensureTypeIntrospected();
      typeKind = typeKind.havingEnumFirstTypeParameter(hasEnumContainedElementType());
    }
  }

  private boolean supportBuiltinContainerTypes() {
    return protoclass().styles().style().builtinContainerAttributes();
  }

  public static class WholeTypeVariable {
    public final boolean is;
    public final boolean not;
    public final int index;

    WholeTypeVariable(int index) {
      this.index = index;
      this.is = index >= 0;
      this.not = !is;
    }
  }

  public WholeTypeVariable getWholeTypeVariable() {
    return getWholeTypeVariable(false);
  }

  public WholeTypeVariable getSecondaryWholeTypeVariable() {
    return getWholeTypeVariable(true);
  }

  private WholeTypeVariable getWholeTypeVariable(boolean secondary) {
    if (!hasTypeVariables) {
      return NON_WHOLE_TYPE_VARIABLE;
    }

    if (secondary && !isMapType()) {
      return NON_WHOLE_TYPE_VARIABLE;
    }

    String typeString = secondary
        ? getSecondaryElementType()
        : getElementType();

    if (!containingType.generics().isEmpty()) {
      for (Parameter p : containingType.generics().parameters) {
        if (p.var.equals(typeString)) {
          return new WholeTypeVariable(p.index);
        }
      }
    }

    return NON_WHOLE_TYPE_VARIABLE;
  }

  private boolean hasEnumContainedElementType() {
    return containedTypeElement != null
        && containedTypeElement.getKind() == ElementKind.ENUM;
  }

  DeclaringType getDeclaringType() {
    return containingType.inferDeclaringType(element);
  }

  private void validateTypeAndAnnotations() {
    boolean wasOptional = isOptionalType();

    if (!typeKind.isRegular() && !supportBuiltinContainerTypes()) {
      typeKind = AttributeTypeKind.REGULAR;
    }

    if (typeKind.isContainerKind() && typeParameters.isEmpty()) {
      typeKind = AttributeTypeKind.REGULAR;
      if (!SuppressedWarnings.forElement(element, false, false).rawtypes) {
        report().warning(About.UNTYPE,
            "Raw container types treated as regular attributes, nothing special generated."
                + " It is better to avoid raw types at all times");
      }
    }

    if (typeKind.isContainerKind()) {
      boolean hasWildcardInType = returnTypeName.indexOf('?') >= 0;
      if (hasWildcardInType) {
        if (hasNakedWildcardArguments()) {
          typeKind = AttributeTypeKind.REGULAR;
          report()
              .annotationNamed(DefaultMirror.simpleName())
              .warning(About.UNTYPE,
                  "Wildcards are not supported as elements or key/values. Make it lose its special treatment");
        }
      }
    }

    if (isNullable()) {
      if (isOptionalType()) {
        typeKind = AttributeTypeKind.REGULAR;
        report()
            .annotationNamed(DefaultMirror.simpleName())
            .warning(About.UNTYPE, "@Nullable on a Optional attribute make it lose its special treatment");
      } else if (isPrimitive()) {
        report()
            .annotationNamed(nullability.simpleName())
            .error("@%s could not be used with primitive type attibutes", nullability.simpleName());
      } else if (containingType.isAnnotationType()) {
        report()
            .annotationNamed(nullability.simpleName())
            .error("@%s could not be used with annotation attribute, use default value", nullability.simpleName());
      }
    }

    if (isGenerateDefault && isOptionalType()) {
      typeKind = AttributeTypeKind.REGULAR;
      report()
          .annotationNamed(DefaultMirror.simpleName())
          .warning(About.UNTYPE, "@Value.Default on a optional attribute make it lose its special treatment");
    }

    if (isContainerType() && containingType.isUseStrictBuilder()) {
      if (isGenerateDefault) {
        typeKind = AttributeTypeKind.REGULAR;
        report()
            .annotationNamed(DefaultMirror.simpleName())
            .warning(About.UNTYPE,
                "@Value.Default on a container attribute make it lose its special treatment (when strictBuilder = true)");
      } else if (isNullable()) {
        typeKind = AttributeTypeKind.REGULAR;
        report()
            .annotationNamed(nullability.simpleName())
            .warning(About.UNTYPE,
                "@%s on a container attribute make it lose its special treatment (when strictBuilder = true)",
                nullability.simpleName());
      }
    }

    if (containingType.isAnnotationType() && isAuxiliary()) {
      report()
          .annotationNamed(AuxiliaryMirror.simpleName())
          .error("@Value.Auxiliary cannot be used on annotation attribute to not violate annotation spec");
    }

    if (!isGenerateJdkOnly()
        && (nullElements == NullElements.ALLOW || nullElements == NullElements.SKIP)) {
      report().warning(About.INCOMPAT,
          "Guava collection implementation does not allow null elements,"
              + " @AllowNulls/@SkipNulls annotation will be ignored."
              + " Switch Style.jdkOnly=true to use collections that permit nulls as values");
    }

    if (isOptionalType()
        && containedTypeElement != null // for specialized optional types it can be null
        && AttributeTypeKind.forRawType(containedTypeElement.getQualifiedName().toString()).isOptionalKind()) {
      typeKind = AttributeTypeKind.REGULAR;
      report().warning(About.UNTYPE,
          "Optional<Optional<*>> is turned into regular attribute to avoid ambiguity problems");
    }

    if (wasOptional && !isOptionalType()) {
      isSuppressedOptional = true;
    }
  }

  private boolean hasNakedWildcardArguments() {
    for (String t : typeParameters()) {
      if (t.startsWith("?")) {
        return true;
      }
    }
    return false;
  }

  private @Nullable NullabilityAnnotationInfo isAccessorNullableAccessor(Element element) {
    for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
      TypeElement annotationElement = (TypeElement) annotation.getAnnotationType().asElement();
      Name simpleName = annotationElement.getSimpleName();
      Name qualifiedName = annotationElement.getQualifiedName();
      if (isNullableAnnotation(simpleName, qualifiedName)) {
        return ImmutableNullabilityAnnotationInfo.of(annotationElement);
      }
    }
    return null;
  }

  private void initSpecialAnnotations() {
    Environment environment = containingType.constitution.protoclass().environment();
    ImmutableList.Builder<AnnotationInjection> annotationInjections = null;

    for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
      MetaAnnotated metaAnnotated = MetaAnnotated.from(annotation, environment);

      if (metaAnnotated.injectAnnotation().isPresent()) {
        InjectionInfo info = metaAnnotated.injectAnnotation().get();
        if (annotationInjections == null) {
          annotationInjections = ImmutableList.builder();
        }
        annotationInjections.add(info.injectionFor(annotation, environment));
      }

      TypeElement annotationElement = (TypeElement) annotation.getAnnotationType().asElement();
      Name simpleName = annotationElement.getSimpleName();
      Name qualifiedName = annotationElement.getQualifiedName();
      if (isNullableAnnotation(simpleName, qualifiedName)) {
        nullability = ImmutableNullabilityAnnotationInfo.of(annotationElement);
      } else if (simpleName.contentEquals(TypeStringProvider.EPHEMERAL_ANNOTATION_ALLOW_NULLS)) {
        nullElements = NullElements.ALLOW;
      } else if (simpleName.contentEquals(TypeStringProvider.EPHEMERAL_ANNOTATION_SKIP_NULLS)) {
        nullElements = NullElements.SKIP;
      }
    }
    if (containingType.isGenerateJacksonProperties()
        && typeKind.isMap()
        && Proto.isAnnotatedWith(element, Annotations.JACKSON_ANY_GETTER)) {
      jacksonAnyGetter = true;
    }
    if (containingType.isGenerateJacksonMapped()
        && (isGenerateAbstract || isGenerateDefault)
        && Proto.isAnnotatedWith(element, Annotations.JACKSON_VALUE)) {
      jacksonValue = true;
    }
    if (isCollectionType()
        && nullElements == NullElements.BAN
        && protoclass().styles().style().validationMethod() == ValidationMethod.NONE) {
      nullElements = NullElements.NOP;
    }
    if (annotationInjections != null) {
      this.annotationInjections = annotationInjections.build();
    }
  }

  private boolean isNullableAnnotation(Name simpleName, Name qualifiedName) {
    return qualifiedName.contentEquals(Annotations.JAVAX_CHECK_FOR_NULL)
        || qualifiedName.contentEquals(Annotations.JAVAX_NULLABLE)
        || simpleName.contentEquals(containingType.names().nullableAnnotation);
  }

  public boolean isNullableCollector() {
    return typeKind.isCollectionOrMapping()
        && (isNullable() || containingType.isDeferCollectionAllocation());
  }

  public boolean isDeferCollectionAllocation() {
    return typeKind.isCollectionOrMapping()
        && containingType.isDeferCollectionAllocation();
  }

  private void initMiscellaneous() {
    this.deprecated = protoclass()
        .processing()
        .getElementUtils()
        .isDeprecated(CachingElements.getDelegate(element));

    this.docComment = containingType.extractDocComment(element);
    if (!isPrimitive()
        && isMandatory()
        && protoclass().styles().style().validationMethod() != ValidationMethod.SIMPLE) {
      this.nullability = NullabilityAnnotationInfo.forTypeUse();
    }
  }

  private void initBuilderParamsIfApplicable() {
    if (!protoclass().environment().hasBuilderModule()) {
      return;
    }
    isBuilderParameter = FParameterMirror.isPresent(element);
    Optional<SwitchMirror> switcher = SwitchMirror.find(element);
    if (switcher.isPresent()) {
      if (isBuilderParameter) {
        report().annotationNamed(FParameterMirror.simpleName())
            .error("@%s and @%s annotations cannot be used on a same factory parameter",
                FParameterMirror.simpleName(),
                SwitchMirror.simpleName());
        isBuilderParameter = false;
      }
      if (!isEnumType()) {
        report().annotationNamed(SwitchMirror.simpleName())
            .error("@%s annotation applicable only to enum parameters", SwitchMirror.simpleName());
      } else {
        builderSwitcherModel = new SwitcherModel(switcher.get(), names, containedTypeElement);
      }
    }
  }

  @Nullable
  public SwitcherModel builderSwitcherModel;
  public boolean isBuilderParameter;
  boolean hasSomeUnresolvedTypes;

  public boolean hasBuilderSwitcherDefault() {
    return isBuilderSwitcher() && builderSwitcherModel.hasDefault();
  }

  public boolean isBuilderSwitcher() {
    return builderSwitcherModel != null;
  }

  public boolean canUseNullAsUndefined() {
    return !isPrimitive()
        && !isNullable()
        && !typeKind.isCollectionOrMapping()
        && !containingType.isUseStrictBuilder();
  }

  public boolean requiresTrackIsSet() {
    if (isGenerateDefault && isPrimitive()) {
      // because privimitive cannot be null
      return true;
    }
    if (isGenerateDefault && isNullable()) {
      // nullable arrays should be able to distinguish null from default
      return true;
    }
    if (typeKind.isCollectionOrMapping() && isGenerateDefault) {
      // becase builder/collector is used and have to distinguish non-default value is set
      return true;
    }
    if (containingType.isUseStrictBuilder()
        && !isMandatory()
        && !typeKind.isCollectionOrMapping()) {
      // non-mandatory attributes without add/put methods
      // should be checked if it was already initialized
      // for a strict builder
      return true;
    }
    return false;
  }

  public boolean isGenerateImmutableCopyOf;

  public Collection<TypeElement> getEnumElements() {
    if (isEnumType()) {
      return Collections.singletonList(containedTypeElement);
    }
    if (!isContainerType()) {
      List<TypeElement> elements = Lists.newArrayListWithCapacity(2);
      if (hasEnumContainedElementType()) {
        elements.add(containedTypeElement);
      }
      if (isMapType() && containedSecondaryTypeElement.getKind() == ElementKind.ENUM) {
        elements.add(containedSecondaryTypeElement);
      }
      return elements;
    }
    return Collections.emptyList();
  }

  boolean hasConstructorParameterCustomOrder() {
    return getConstructorParameterOrder() > ValueAttribute.CONSTRUCTOR_PARAMETER_DEFAULT_ORDER;
  }

  private Protoclass protoclass() {
    return containingType.constitution.protoclass();
  }

  public String getGenericArgs() {
    String type = getType();
    int indexOfGenerics = type.indexOf('<');
    if (indexOfGenerics > 0) {
      return type.substring(indexOfGenerics);
    }
    return "";
  }

  public AttributeBuilderDescriptor getAttributeBuilderDescriptor() {
    return attributeBuilderDescriptor;
  }

  Reporter report() {
    return reporter.withElement(element);
  }

  public CharSequence getTypeTokenOfElement() {
    return containingType.getGsonTypeTokens().sourceFor(getElementType());
  }

  public CharSequence getTypeTokenOfSecondaryElement() {
    return containingType.getGsonTypeTokens().sourceFor(getSecondaryElementType());
  }

  public Element originalElement() {
    return CachingElements.getDelegate(element);
  }

  public Element originalTypeElement() {
    return containingType.originalElement();
  }

  @Override
  public StyleInfo style() {
    return containingType.constitution.style();
  }

  public boolean supportsInternalImplConstructor() {
    return !isEncoding() || instantiation.supportsInternalImplConstructor();
  }

  public Collection<String> fieldInjectedAnnotations() {
    return collectInjections(Where.FIELD);
  }

  public Collection<String> accessorInjectedAnnotations() {
    return collectInjections(Where.ACCESSOR);
  }

  public Collection<String> syntheticFieldsInjectedAnnotations() {
    return collectInjections(Where.SYNTHETIC_FIELDS);
  }

  public Collection<String> initializerInjectedAnnotations() {
    return collectInjections(Where.INITIALIZER);
  }

  public Collection<String> constructorParameterInjectedAnnotations() {
    return collectInjections(Where.CONSTRUCTOR_PARAMETER);
  }

  public Collection<String> elementInitializerInjectedAnnotations() {
    return collectInjections(Where.ELEMENT_INITIALIZER);
  }

  private Collection<String> collectInjections(Where target) {
    return AnnotationInjections.collectInjections(element,
        target,
        Collections.singleton(name()),
        annotationInjections,
        containingType.getDeclaringTypeAnnotationInjections(),
        containingType.getDeclaringTypeEnclosingAnnotationInjections(),
        containingType.getDeclaringPackageAnnotationInjections());
  }

  @Override
  public String toString() {
    return "Attribute[" + name() + "]";
  }

  void initNullabilitySupertype(ExecutableElement accessor) {
    if (nullabilityInSupertype == null && !isPrimitive() && !isNullable()) {
      nullabilityInSupertype = isAccessorNullableAccessor(accessor);
    }
  }

  public boolean isNullableInSupertype() {
    return nullabilityInSupertype != null;
  }

  public String atNullableInSupertypeLocal() {
    return nullabilityInSupertype != null ? nullabilityInSupertype.asLocalPrefix() : "";
  }

  enum ToName implements Function<ValueAttribute, String> {
    FUNCTION;
    @Override
    public String apply(ValueAttribute input) {
      return input.name();
    }
  }
}


package org.immutables.criteria.constraints;

import java.util.function.UnaryOperator;

/**
 * Link between front-end (codegened Criteria) and back-end (built {@link Expression}).
 */
public final class CriteriaContext<R> {

  private final CriteriaCreator<R> creator;
  private final DnfExpression<?> expression;
  private final Path<?> path;
  private final Operator operator;

  public CriteriaContext(CriteriaCreator<R> creator) {
    this(Operators.AND, DnfExpression.create(), null, creator);
  }

  private CriteriaContext(Operator operator, DnfExpression<?> expression, Path<?> path, CriteriaCreator<R> creator) {
    this.creator = creator;
    this.expression = expression;
    this.path = path;
    this.operator = operator;
  }

  public R create() {
    return creator.create(this);
  }

  /**
   *  adds an intermediate step (list of paths usually)
   */
  public CriteriaContext<R> add(Path<?> path) {
    final Path<?> newPath = this.path != null ? Expressions.path(this.path.path() + "." + path.path()) : path;
    return new CriteriaContext<>(operator, expression, newPath, creator);
  }

  public CriteriaContext<R> or() {
    if (operator == Operators.OR) {
      return this;
    }

    return new CriteriaContext<>(Operators.OR, expression, path, creator);
  }

  public Expression<?> expression() {
    return this.expression;
  }

  @SuppressWarnings("unchecked")
  public R create(UnaryOperator<Expression<?>> operator) {
    final Expression<Object> apply = (Expression<Object>) operator.apply(path);
    final DnfExpression<Object> existing = (DnfExpression<Object>) expression;
    final DnfExpression<?> newExpression = this.operator == Operators.AND ? existing.and(apply) : existing.or(apply);
    return new CriteriaContext<R>(Operators.AND, newExpression, null, creator).create();
  }

}


package org.immutables.criteria;

import com.google.common.base.Preconditions;
import org.immutables.criteria.constraints.Call;
import org.immutables.criteria.constraints.Expression;
import org.immutables.criteria.constraints.ExpressionVisitor;
import org.immutables.criteria.constraints.Expressions;
import org.immutables.criteria.constraints.Literal;
import org.immutables.criteria.constraints.Operator;
import org.immutables.criteria.constraints.Operators;
import org.immutables.criteria.constraints.Path;

import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
 * Evaluator (predicate) based on reflection. Uses expression visitor API to construct the predicate.
 *
 *
Probably most useful in testing scenarios

 */
public class InMemoryExpressionEvaluator<T> implements Predicate<T> {

  /**
   * Sentinel used for Three-Valued Logic: true / false / unknown
   */
  private static final Object UNKNOWN = new Object();

  private final Expression<T> expression;

  private InMemoryExpressionEvaluator(Expression<T> expression) {
    this.expression = Objects.requireNonNull(expression, "expression");
  }

  /**
   * Factory method to create evaluator instance
   */
  public static <T> Predicate<T> of(Expression<T> expression) {
    if (Expressions.isNil(expression)) {
      // always true
      return instance -> true;
    }

    return new InMemoryExpressionEvaluator<>(expression);
  }

  @Override
  public boolean test(T instance) {
    Objects.requireNonNull(instance, "instance");
    final LocalVisitor visitor = new LocalVisitor(instance);
    return Boolean.TRUE.equals(expression.accept(visitor, null));
  }

  private static class LocalVisitor implements ExpressionVisitor<Object, Void> {

    private final ValueExtractor<Object> extractor;

    private LocalVisitor(Object instance) {
      this.extractor = new ReflectionFieldExtractor<>(instance);
    }

    @Override
    public Object visit(Call<?> call, Void context) {
      final Operator op = call.getOperator();
      final List<Expression<?>> args = call.getArguments();

      if (op == Operators.EQUAL || op == Operators.NOT_EQUAL) {
        Preconditions.checkArgument(args.size() == 2, "Size should be 2 for %s but was %s", op, args.size());
        final Object left = args.get(0).accept(this, null);
        final Object right = args.get(1).accept(this, null);

        if (left == UNKNOWN || right == UNKNOWN) {
          return UNKNOWN;
        }

        final boolean equals = Objects.equals(left, right);
        return (op == Operators.EQUAL) == equals;
      }

      if (op == Operators.IN || op == Operators.NOT_IN) {
        Preconditions.checkArgument(args.size() == 2, "Size should be 2 for %s but was %s", op, args.size());
        final Object left = args.get(0).accept(this, null);
        if (left == UNKNOWN) {
          return UNKNOWN;
        }
        @SuppressWarnings("unchecked")
        final Iterable<Object> right = (Iterable<Object>) args.get(1).accept(this, null);
        Preconditions.checkNotNull(right, "not expected to be null %s", args.get(1));
        final Stream<Object> stream = StreamSupport.stream(right.spliterator(), false);

        return op == Operators.IN ? stream.anyMatch(r -> Objects.equals(left, r)) : stream.noneMatch(r -> Objects.equals(left, r));
      }

      if (op == Operators.IS_ABSENT || op == Operators.IS_PRESENT) {
        Preconditions.checkArgument(args.size() == 1, "Size should be 1 for %s but was %s", op, args.size());
        final Object left = args.get(0).accept(this, null);

        if (left instanceof java.util.Optional) {
          Optional<?> opt = (java.util.Optional<?>) left;
          return (op == Operators.IS_ABSENT) != opt.isPresent();
        }

        if (left instanceof com.google.common.base.Optional) {
          // guava Optional
          com.google.common.base.Optional<?> opt = (com.google.common.base.Optional<?>) left;
          return (op == Operators.IS_ABSENT) != opt.isPresent();
        }

        if (left == UNKNOWN) {
         return (op == Operators.IS_ABSENT);
        }

        return (op == Operators.IS_ABSENT) ? Objects.isNull(left) : Objects.nonNull(left);
      }

      if (op == Operators.AND) {
        Preconditions.checkArgument(!args.isEmpty(), "empty args for %s", op);
        boolean prev = Boolean.TRUE;
        for (Expression<?> exp:args) {
          Object result = exp.accept(this, null);
          if (Boolean.FALSE.equals(result)) {
            return Boolean.FALSE;
          } else if (result == null || result == UNKNOWN) {
            return UNKNOWN;
          } else {
            prev = prev && (Boolean) result;
          }
        }

        return prev;
      }

      if (op == Operators.OR) {
        Preconditions.checkArgument(!args.isEmpty(), "empty args for %s", op);
        boolean prev = Boolean.FALSE;
        for (Expression<?> exp:args) {
          Object result = exp.accept(this, null);
          if (Boolean.TRUE.equals(result)) {
            return Boolean.TRUE;
          } else if (result == null || result == UNKNOWN) {
            return UNKNOWN;
          } else {
            prev = prev || (Boolean) result;
          }
        }

        return prev;
      }

      // comparables
      if (Arrays.asList(Operators.GREATER_THAN, Operators.GREATER_THAN_OR_EQUAL,
              Operators.LESS_THAN, Operators.LESS_THAN_OR_EQUAL).contains(op)) {

        Preconditions.checkArgument(args.size() == 2, "Size should be 2 for %s but was %s", op, args.size());

        @SuppressWarnings("unchecked")
        Comparable<Object> left = (Comparable<Object>) args.get(0).accept(this, null);
        if (left == UNKNOWN || left == null) {
          return UNKNOWN;
        }
        @SuppressWarnings("unchecked")
        Comparable<Object> right = (Comparable<Object>) args.get(1).accept(this, null);
        if (right == UNKNOWN || right == null) {
          return UNKNOWN;
        }

        final int compare = left.compareTo(right);

        if (op == Operators.GREATER_THAN) {
          return compare > 0;
        } else if (op == Operators.GREATER_THAN_OR_EQUAL) {
          return compare >= 0;
        } else if (op == Operators.LESS_THAN) {
          return compare < 0;
        } else if (op == Operators.LESS_THAN_OR_EQUAL) {
          return compare <= 0;
        }
      }

      throw new UnsupportedOperationException("Don't know how to handle " + op);
    }

    @Override
    public Object visit(Literal<?> literal, Void context) {
      return literal.value();
    }

    @Override
    public Object visit(Path<?> path, Void context) {
      return extractor.extract(path.path());
    }
  }

  private interface ValueExtractor<T> {
    @Nullable
    Object extract(String property);
  }

  private static class ReflectionFieldExtractor<T> implements ValueExtractor<T> {
    private final T object;

    private ReflectionFieldExtractor(T object) {
      this.object = object;
    }

    @Nullable
    @Override
    public Object extract(String property) {
      Objects.requireNonNull(property, "property");

      Object result = object;

      for (String name:property.split("\\.")) {
        result = extract(result, name);
        if (result == UNKNOWN) {
          break;
        }
      }

      return result;

    }

    private static Object extract(Object instance, String property) {
      if (property.isEmpty()) {
        return instance;
      }

      if (instance == null) {
        return UNKNOWN;
      }

      try {
        // TODO caching
        final Field field = instance.getClass().getDeclaredField(property);
        if (!field.isAccessible()) {
          field.setAccessible(true);
        }
        return field.get(instance);
      } catch (NoSuchFieldException | IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    }
  }

}


/*
   Copyright 2013-2018 Immutables Authors and Contributors

   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 org.immutables.mongo.concurrent;

import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.ForwardingListenableFuture.SimpleForwardingListenableFuture;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Provides default wrapping implementation of {@link FluentFuture}
 */
public final class FluentFutures {
  private FluentFutures() {}

  private static class WrapingFluentFuture<V>
      extends SimpleForwardingListenableFuture<V>
      implements FluentFuture<V> {

    private final Executor executor;

    private WrapingFluentFuture(ListenableFuture<V> future, Executor executor) {
      super(future);
      this.executor = checkNotNull(executor);
    }

    @Override
    public V getUnchecked() {
      return Futures.getUnchecked(this);
    }

    @Override
    public FluentFuture<V> addCallback(FutureCallback<V> callback) {
      Futures.addCallback(this, callback, executor);
      return this;
    }

    @Override
    public FluentFuture<V> catching(Function<Throwable, V> fallback) {
      return from(Futures.catching(this, Throwable.class, fallback, executor));
    }

    @Override
    public FluentFuture<V> withFallbackValue(final V value) {
      return catching(new Function<Throwable, V>() {
        @Override
        public V apply(Throwable t) {
          return value;
        }
      });
    }

    @Override
    public <T> FluentFuture<T> transform(Function<? super V, ? extends T> function) {
      return from(Futures.transform(this, function, executor));
    }

    @Override
    public <T> FluentFuture<T> asyncTransform(AsyncFunction<? super V, ? extends T> function) {
      return from(Futures.transformAsync(this, function, executor));
    }

    @Override
    public FluentFuture<V> withExecutor(Executor executor) {
      if (this.executor == executor) {
        return this;
      }
      return new WrapingFluentFuture<>(delegate(), executor);
    }

    @Override
    public <T> FluentFuture<T> lazyTransform(Function<? super V, ? extends T> function) {
      return new LazyTransformedFluentFuture<>(this, function, executor);
    }
  }

  private static class LazyTransformedFluentFuture<V, F> extends WrapingFluentFuture<V> {
    private final Function<? super F, ? extends V> function;
    private final ListenableFuture<F> fromFuture;

    // Any get operation will go through transform function
    @SuppressWarnings("unchecked")
    LazyTransformedFluentFuture(
        ListenableFuture<F> fromFuture,
        Function<? super F, ? extends V> function,
        Executor executor) {
      super((ListenableFuture<V>) fromFuture, executor);
      this.fromFuture = fromFuture;
      this.function = function;
    }

    @Override
    public V get() throws InterruptedException, ExecutionException {
      return function.apply(fromFuture.get());
    }

    @Override
    public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
      return function.apply(fromFuture.get(timeout, unit));
    }
  }

  /**
   * Unnecessary conversion, already have a fluent future. This method exists solely to detect
   * unnecessary conversion from a {@link FluentFuture}.
   * @deprecated you don't need to convert to a fluent future, it is already is
   * @param <V> value type
   * @param future future
   * @return same instance
   */
  @Deprecated
  public static <V> FluentFuture<V> from(FluentFuture<V> future) {
    return future;
  }

  /**
   * Wraps listenable future with a fluent future.
   * @param <V> value type
   * @param future future
   * @return fluent instance
   */
  public static <V> FluentFuture<V> from(ListenableFuture<V> future) {
    if (future instanceof FluentFuture<?>) {
      return (FluentFuture<V>) future;
    }
    return new WrapingFluentFuture<>(future, MoreExecutors.directExecutor());
  }
}