package org.checkerframework.checker.nullness;

import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.Tree;
import java.util.Collection;
import java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.value.qual.ArrayLen;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.util.AnnotatedTypes;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;

/**
 * Determines the nullness type of calls to {@link java.util.Collection#toArray()}.
 *
 * @checker_framework.manual #nullness-collection-toarray Nullness and conversions from collections
 *     to arrays
 * @checker_framework.manual #constant-value-checker Constant Value Checker
 */
public class CollectionToArrayHeuristics {

  /** The processing environment. */
  private final ProcessingEnvironment processingEnv;

  /** The checker, used for issuing diagnostic messages. */
  private final BaseTypeChecker checker;

  /** The type factory. */
  private final NullnessAnnotatedTypeFactory atypeFactory;

  /** Whether to trust {@code @ArrayLen(0)} annotations. */
  private final boolean trustArrayLenZero;

  /** The Collection type. */
  private final AnnotatedDeclaredType collectionType;

  /** The Collection.toArray(T[]) method. */
  private final ExecutableElement collectionToArrayE;

  /** The Collection.size() method. */
  private final ExecutableElement size;

  /** The ArrayLen.value field/element. */
  private final ExecutableElement arrayLenValueElement;

  /**
   * Create a CollectionToArrayHeuristics.
   *
   * @param checker the checker, used for issuing diagnostic messages
   * @param factory the type factory
   */
  public CollectionToArrayHeuristics(
      BaseTypeChecker checker, NullnessAnnotatedTypeFactory factory) {
    this.processingEnv = checker.getProcessingEnvironment();
    this.checker = checker;
    this.atypeFactory = factory;

    this.collectionType =
        factory.fromElement(ElementUtils.getTypeElement(processingEnv, Collection.class));
    this.collectionToArrayE =
        TreeUtils.getMethod("java.util.Collection", "toArray", processingEnv, "T[]");
    this.size = TreeUtils.getMethod("java.util.Collection", "size", 0, processingEnv);
    this.arrayLenValueElement = TreeUtils.getMethod(ArrayLen.class, "value", 0, processingEnv);

    this.trustArrayLenZero =
        checker.getLintOption(
            NullnessChecker.LINT_TRUSTARRAYLENZERO, NullnessChecker.LINT_DEFAULT_TRUSTARRAYLENZERO);
  }

  /**
   * If the method invocation is a call to {@code toArray}, then it manipulates the returned type of
   * {@code method} arg to contain the appropriate nullness. Otherwise, it does nothing.
   *
   * @param tree method invocation tree
   * @param method invoked method type
   */
  public void handle(MethodInvocationTree tree, AnnotatedExecutableType method) {
    if (TreeUtils.isMethodInvocation(tree, collectionToArrayE, processingEnv)) {
      assert !tree.getArguments().isEmpty() : tree;
      ExpressionTree argument = tree.getArguments().get(0);
      boolean receiverIsNonNull = receiverIsCollectionOfNonNullElements(tree);
      boolean argIsHandled =
          isHandledArrayCreation(argument, receiverName(tree.getMethodSelect()))
              || (trustArrayLenZero && isArrayLenZeroFieldAccess(argument));
      setComponentNullness(receiverIsNonNull && argIsHandled, method.getReturnType());

      // TODO: We need a mechanism to prevent nullable collections
      // from inserting null elements into a nonnull arrays.
      if (!receiverIsNonNull) {
        setComponentNullness(false, method.getParameterTypes().get(0));
      }

      if (receiverIsNonNull && !argIsHandled) {
        if (argument.getKind() != Tree.Kind.NEW_ARRAY) {
          checker.reportWarning(tree, "toarray.nullable.elements.not.newarray");
        } else {
          checker.reportWarning(tree, "toarray.nullable.elements.mismatched.size");
        }
      }
    }
  }

  /**
   * Sets the nullness of the component of the array type.
   *
   * @param isNonNull indicates which annotation ({@code NonNull} or {@code Nullable}) should be
   *     inserted
   * @param type the array type
   */
  private void setComponentNullness(boolean isNonNull, AnnotatedTypeMirror type) {
    assert type.getKind() == TypeKind.ARRAY;
    AnnotatedTypeMirror compType = ((AnnotatedArrayType) type).getComponentType();
    compType.replaceAnnotation(isNonNull ? atypeFactory.NONNULL : atypeFactory.NULLABLE);
  }

  /**
   * Returns true if {@code argument} is one of the array creation trees that the heuristic handles.
   *
   * @param argument the tree passed to {@link Collection#toArray(Object[]) Collection.toArray(T[])}
   * @param receiver the expression for the receiver collection
   * @return true if the argument is handled and assume to return nonnull elements
   */
  private boolean isHandledArrayCreation(Tree argument, String receiver) {
    if (argument.getKind() != Tree.Kind.NEW_ARRAY) {
      return false;
    }
    NewArrayTree newArr = (NewArrayTree) argument;

    // empty array initializer
    if (newArr.getInitializers() != null) {
      return newArr.getInitializers().isEmpty();
    }

    assert !newArr.getDimensions().isEmpty();
    Tree dimension = newArr.getDimensions().get(newArr.getDimensions().size() - 1);

    // 0-length array creation
    if (dimension.toString().equals("0")) {
      return true;
    }

    // size()-length array creation
    if (TreeUtils.isMethodInvocation(dimension, size, processingEnv)) {
      MethodInvocationTree invok = (MethodInvocationTree) dimension;
      String invokReceiver = receiverName(invok.getMethodSelect());
      return invokReceiver.equals(receiver);
    }

    return false;
  }

  /**
   * Returns true if the argument is a field access expression, where the field has declared type
   * {@code @ArrayLen(0)}.
   *
   * @param argument an expression tree
   * @return true if the argument is a field access expression, where the field has declared type
   *     {@code @ArrayLen(0)}
   */
  private boolean isArrayLenZeroFieldAccess(ExpressionTree argument) {
    Element el = TreeUtils.elementFromUse(argument);
    if (el != null && el.getKind().isField()) {
      TypeMirror t = ElementUtils.getType(el);
      if (t.getKind() == TypeKind.ARRAY) {
        List<? extends AnnotationMirror> ams = t.getAnnotationMirrors();
        for (AnnotationMirror am : ams) {
          if (atypeFactory.areSameByClass(am, ArrayLen.class)) {
            List<Integer> lens =
                AnnotationUtils.getElementValueArray(am, arrayLenValueElement, Integer.class);
            if (lens.size() == 1 && lens.get(0) == 0) {
              return true;
            }
          }
        }
      }
    }
    return false;
  }

  /**
   * Returns {@code true} if the method invocation tree receiver is collection that contains
   * non-null elements (i.e. its type argument is {@code @NonNull}.
   *
   * @param tree a method invocation
   * @return true if the receiver is a collection of non-null elements
   */
  private boolean receiverIsCollectionOfNonNullElements(MethodInvocationTree tree) {
    // check receiver
    AnnotatedTypeMirror receiver = atypeFactory.getReceiverType(tree);
    AnnotatedDeclaredType collection =
        AnnotatedTypes.asSuper(atypeFactory, receiver, collectionType);

    if (collection.getTypeArguments().isEmpty() // raw type
        || !collection.getTypeArguments().get(0).hasEffectiveAnnotation(atypeFactory.NONNULL)) {
      return false;
    }
    return true;
  }

  /**
   * The name of the receiver object of the tree.
   *
   * @param tree either an identifier tree or a member select tree
   */
  // This method is quite sloppy, but works most of the time
  private String receiverName(Tree tree) {
    if (tree.getKind() == Tree.Kind.MEMBER_SELECT) {
      return ((MemberSelectTree) tree).getExpression().toString();
    } else {
      return "this";
    }
  }
}
