package annotator.find;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.lang.model.type.TypeKind;

import annotations.el.InnerTypeLocation;
import annotator.Main;

import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WildcardTree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry;
import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntryKind;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;

/**
 * GenericArrayLocationCriterion represents the criterion specifying the location
 * of an element in the generic/array hierarchy as specified by the
 * JSR 308 proposal.
 */
public class GenericArrayLocationCriterion implements Criterion {
  private static final boolean debug = false;

  // the full location list
  private final List<TypePathEntry> location;

  // represents all but the last element of the location list
  // TODO: this field is initialized, but never read!
  // I removed it, see the version control history.
  // private Criterion parentCriterion;

  /**
   * Creates a new GenericArrayLocationCriterion specifying that the element
   * is an outer type, such as:
   *  <code>@A List&lt;Integer&gt;</code>
   * or
   *  <code>Integer @A []</code>
   */
  public GenericArrayLocationCriterion() {
    this(new ArrayList<TypePathEntry>());
  }

  /**
   * Creates a new GenericArrayLocationCriterion representing the given
   * location.
   *
   * @param innerTypeLoc the location of the element being represented
   */
  public GenericArrayLocationCriterion(InnerTypeLocation innerTypeLoc) {
    this(innerTypeLoc.location);
  }

  private GenericArrayLocationCriterion(List<TypePathEntry> location) {
    this.location = location;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isSatisfiedBy(TreePath path, Tree leaf) {
    assert path == null || path.getLeaf() == leaf;
    return isSatisfiedBy(path);
  }

  /**
   * Determines if the given list holds only {@link TypePathEntry}s with the tag
   * {@link TypePathEntryKind#ARRAY}.
   *
   * @param location the list to check
   * @return {@code true} if the list only contains
   *         {@link TypePathEntryKind#ARRAY}, {@code false} otherwise.
   */
  private boolean containsOnlyArray(List<TypePathEntry> location) {
    for (TypePathEntry tpe : location) {
      if (tpe.tag != TypePathEntryKind.ARRAY) {
        return false;
      }
    }
    return true;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isSatisfiedBy(TreePath path) {
    if (path == null || path.getParentPath() == null) {
      if (debug) {
        System.out.println("GenericArrayLocationCriterion.isSatisfiedBy() with null path gives false.");
      }
      return false;
    }

    if (debug) {
      System.out.printf("GenericArrayLocationCriterion.isSatisfiedBy():%n  leaf of path: %s%n  searched location: %s%n",
              path.getLeaf(), location);
    }

    TreePath pathRemaining = path;
    Tree leaf = path.getLeaf();

    // Don't allow annotations directly on these tree kinds if the child type is
    // a MEMBER_SELECT. This way we'll continue to traverse deeper in the tree
    // to find the correct MEMBER_SELECT.
    Tree child = null;
    if (leaf.getKind() == Tree.Kind.PARAMETERIZED_TYPE) {
      child = ((ParameterizedTypeTree) leaf).getType();
    } else if (leaf.getKind() == Tree.Kind.VARIABLE) {
      child = ((VariableTree) leaf).getType();
    } else if (leaf.getKind() == Tree.Kind.NEW_CLASS) {
      child = ((NewClassTree) leaf).getIdentifier();
    } else if (leaf.getKind() == Tree.Kind.NEW_ARRAY && !location.isEmpty()) {
      child = ((NewArrayTree) leaf).getType();
    }
    if (child != null && child.getKind() == Tree.Kind.MEMBER_SELECT) {
      JCExpression exp = ((JCFieldAccess) child).getExpression();
      if (exp.type != null && exp.type.getKind() == TypeKind.PACKAGE
          || location.isEmpty()
          || (location.get(location.size()-1)).tag
              != TypePathEntryKind.INNER_TYPE) {
          return false;
      }
    }

    if (leaf.getKind() == Tree.Kind.MEMBER_SELECT) {
      JCFieldAccess fieldAccess = (JCFieldAccess) leaf;
      if (isStatic(fieldAccess)) {
        // If this MEMBER_SELECT is for a static class...
        if (location.isEmpty()) {
          // ...and it does not go on a compound type, this is the right place.
          return true;
        } else if (isGenericOrArray(path.getParentPath().getLeaf())
            && isGenericOrArray(path.getParentPath().getParentPath().getLeaf())) {
          // If the two parents above this are compound types, then skip
          // the compound type directly above. For example, to get to Outer.Inner
          // of Outer.Inner<K, V> we had to get through the PARAMETERIZED_TYPE
          // node. But we didn't go deeper in the compound type in the way the
          // type path defines, we just went deeper to get to the outer type. So
          // skip this node later when checking to make sure that we're in the
          // correct part of the compound type.
          pathRemaining = path.getParentPath();
        }
      } else {
        JCExpression exp = fieldAccess.getExpression();
        if (exp.getKind() == Tree.Kind.MEMBER_SELECT && exp.type != null
            && exp.type.getKind() == TypeKind.PACKAGE) {
          if (location.isEmpty()) {
            return true;
          } // else, keep going to make sure we're in the right part of the
            // compound type
        } else {
          if (!location.isEmpty()
              && location.get(location.size()-1).tag
                  != TypePathEntryKind.INNER_TYPE) {
            return false;
          }
        }
      }
    }

    if (location.isEmpty()) {
      // no inner type location, want to annotate outermost type
      // e.g.,  @Nullable List list;
      //        @Nullable List<String> list;
      //        String @Nullable [] array;
      leaf = path.getLeaf();
      Tree parent = path.getParentPath().getLeaf();

      boolean result = ((leaf.getKind() == Tree.Kind.NEW_ARRAY)
                        || (leaf.getKind() == Tree.Kind.NEW_CLASS)
                        || (leaf.getKind() == Tree.Kind.ANNOTATED_TYPE
                            && isSatisfiedBy(TreePath.getPath(path,
                              ((AnnotatedTypeTree) leaf).getUnderlyingType())))
                        || ((isGenericOrArray(leaf)
                             // or, it might be a raw type
                             || (leaf.getKind() == Tree.Kind.IDENTIFIER) // IdentifierTree
                             || (leaf.getKind() == Tree.Kind.METHOD) // MethodTree
                             || (leaf.getKind() == Tree.Kind.TYPE_PARAMETER) // TypeParameterTree
                             // I don't know why a GenericArrayLocationCriterion
                             // is being created in this case, but it is.
                             || (leaf.getKind() == Tree.Kind.PRIMITIVE_TYPE) // PrimitiveTypeTree
                             // TODO: do we need wildcards here?
                             // || leaf instanceof WildcardTree
                             )
                            && ! isGenericOrArray(parent)));
      if (debug) {
        System.out.printf("GenericArrayLocationCriterion.isSatisfiedBy: locationInParent==null%n  leaf=%s (%s)%n  parent=%s (%s)%n  => %s (%s %s)%n",
                  leaf, leaf.getClass(), parent, parent.getClass(), result, isGenericOrArray(leaf), ! isGenericOrArray(parent));
      }

      return result;
    }

    // If we've made it this far then we've determined that *if* this is the right
    // place to insert the annotation this is the MEMBER_SELECT it should be
    // inserted on. So remove the rest of the MEMBER_SELECTs to get down to the
    // compound type and make sure the compound type location matches.
    while (pathRemaining.getParentPath().getLeaf().getKind() == Tree.Kind.MEMBER_SELECT) {
      pathRemaining = pathRemaining.getParentPath();
    }

    List<TypePathEntry> locationRemaining = new ArrayList<TypePathEntry>(location);

    while (locationRemaining.size() != 0) {
      // annotating an inner type
      leaf = pathRemaining.getLeaf();
      if ((leaf.getKind() == Tree.Kind.NEW_ARRAY)
          && containsOnlyArray(locationRemaining)) {
        if (debug) {
          System.out.println("Found a matching NEW_ARRAY");
        }
        return true;
      }
      TreePath parentPath = pathRemaining.getParentPath();
      if (parentPath == null) {
        if (debug) {
          System.out.println("Parent path is null and therefore false.");
        }
        return false;
      }
      Tree parent = parentPath.getLeaf();

      if (parent.getKind() == Tree.Kind.ANNOTATED_TYPE) {
          // If the parent is an annotated type, we did not really go up a level.
          // Therefore, skip up one more level.
          parentPath = parentPath.getParentPath();
          parent = parentPath.getLeaf();
      }

      if (debug) {
        System.out.printf("locationRemaining: %s, leaf: %s parent: %s %s%n",
                         locationRemaining, Main.treeToString(leaf), Main.treeToString(parent), parent.getClass());
      }

      TypePathEntry loc = locationRemaining.get(locationRemaining.size()-1);
      if (loc.tag == TypePathEntryKind.INNER_TYPE) {
        if (leaf.getKind() == Tree.Kind.PARAMETERIZED_TYPE) {
          leaf = parent;
          parentPath = parentPath.getParentPath();
          parent = parentPath.getLeaf();
        }
        if (leaf.getKind() != Tree.Kind.MEMBER_SELECT) { return false; }

        JCFieldAccess fieldAccess = (JCFieldAccess) leaf;
        if (isStatic(fieldAccess)) {
          return false;
        }
        locationRemaining.remove(locationRemaining.size()-1);
        leaf = fieldAccess.selected;
        pathRemaining = parentPath;
            // TreePath.getPath(pathRemaining.getCompilationUnit(), leaf);
      } else if (loc.tag == TypePathEntryKind.WILDCARD
          && leaf.getKind() == Tree.Kind.UNBOUNDED_WILDCARD) {
        // Check if the leaf is an unbounded wildcard instead of the parent, since unbounded
        // wildcard has no members so it can't be the parent of anything.
        if (locationRemaining.size() == 0) {
          return false;
        }

        // The following check is necessary because Oracle has decided that
        //   x instanceof Class<? extends Object>
        // will remain illegal even though it means the same thing as
        //   x instanceof Class<?>.
        TreePath gpath = parentPath.getParentPath();
        if (gpath != null) {  // TODO: skip over existing annotations?
          Tree gparent = gpath.getLeaf();
          if (gparent.getKind() == Tree.Kind.INSTANCE_OF) {
            TreeFinder.warn.debug("WARNING: wildcard bounds not allowed "
                + "in 'instanceof' expression; skipping insertion%n");
            return false;
          } else if (gparent.getKind() == Tree.Kind.PARAMETERIZED_TYPE) {
            gpath = gpath.getParentPath();
            if (gpath != null
                && gpath.getLeaf().getKind() == Tree.Kind.ARRAY_TYPE) {
              TreeFinder.warn.debug("WARNING: wildcard bounds not allowed "
                  + "in generic array type; skipping insertion%n");
              return false;
            }
          }
        }
        locationRemaining.remove(locationRemaining.size() - 1);
      } else if (parent.getKind() == Tree.Kind.PARAMETERIZED_TYPE) {
        if (loc.tag != TypePathEntryKind.TYPE_ARGUMENT) {
          return false;
        }

        // Find the outermost type in the AST; if it has parameters,
        // pop the stack once for each inner type on the end of the type
        // path and check the parameter.
        Tree inner = ((ParameterizedTypeTree) parent).getType();
        int i = locationRemaining.size() - 1;  // last valid type path index
        locationRemaining.remove(i--);
        while (inner.getKind() == Tree.Kind.MEMBER_SELECT
            && !isStatic((JCFieldAccess) inner)) {
          // fieldAccess.type != null && fieldAccess.type.getKind() == TypeKind.DECLARED
          // && fieldAccess.type.tsym.isStatic()
          // TODO: check whether MEMBER_SELECT indicates inner or qualifier?
          if (i < 0) { break; }
          if (locationRemaining.get(i).tag != TypePathEntryKind.INNER_TYPE) {
            return false;
          }
          locationRemaining.remove(i--);
          inner = ((MemberSelectTree) inner).getExpression();
          if (inner.getKind() == Tree.Kind.ANNOTATED_TYPE) {
            inner = ((AnnotatedTypeTree) inner).getUnderlyingType();
          }
          if (inner.getKind() == Tree.Kind.PARAMETERIZED_TYPE) {
            inner = ((ParameterizedTypeTree) inner).getType();
          }
        }
        if (i >= 0 && locationRemaining.get(i).tag ==
            TypePathEntryKind.INNER_TYPE) {
          return false;
        }

        // annotating List<@A Integer>
        // System.out.printf("parent instanceof ParameterizedTypeTree: %s loc=%d%n",
        //                   Main.treeToString(parent), loc);
        List<? extends Tree> childTrees =
            ((ParameterizedTypeTree) parent).getTypeArguments();
        boolean found = false;
        if (childTrees.size() > loc.arg) {
          Tree childi = childTrees.get(loc.arg);
          if (childi.getKind() == Tree.Kind.ANNOTATED_TYPE) {
            childi = ((AnnotatedTypeTree) childi).getUnderlyingType();
          }
          if (childi == leaf) {
            for (TreePath outerPath = parentPath.getParentPath();
                outerPath.getLeaf().getKind() == Tree.Kind.MEMBER_SELECT
                    && !isStatic((JCFieldAccess) outerPath.getLeaf());
                outerPath = outerPath.getParentPath()) {
              outerPath = outerPath.getParentPath();
              if (outerPath.getLeaf().getKind() == Tree.Kind.ANNOTATED_TYPE) {
                outerPath = outerPath.getParentPath();
              }
              if (outerPath.getLeaf().getKind() != Tree.Kind.PARAMETERIZED_TYPE) {
                break;
              }
              parentPath = outerPath;
            }
            pathRemaining = parentPath;
            found = true;
          }
        }
        if (!found) {
          if (debug) {
            System.out.printf("Generic failed for leaf: %s: nr children: %d loc: %s child: %s%n",
                             leaf, childTrees.size(), loc,
                             ((childTrees.size() > loc.arg) ? childTrees.get(loc.arg) : null));
          }
          return false;
        }
      } else if (parent.getKind() == Tree.Kind.EXTENDS_WILDCARD
                 || parent.getKind() == Tree.Kind.SUPER_WILDCARD) {
        if (loc.tag != TypePathEntryKind.WILDCARD || locationRemaining.size() == 1) {
          // If there's only one location left, this can't be a match since a wildcard
          // needs to be in another kind of compound type.
          return false;
        }
        locationRemaining.remove(locationRemaining.size() - 1);
        // annotating List<? extends @A Integer>
        // System.out.printf("parent instanceof extends WildcardTree: %s loc=%d%n",
        //                   Main.treeToString(parent), loc);
        WildcardTree wct = (WildcardTree) parent;
        Tree boundTree = wct.getBound();

        if (debug) {
          String wildcardType;
          if (parent.getKind() == Tree.Kind.EXTENDS_WILDCARD) {
            wildcardType = "ExtendsWildcard";
          } else {
            wildcardType = "SuperWildcard";
          }
          System.out.printf("%s with bound %s gives %s%n", wildcardType, boundTree, boundTree.equals(leaf));
        }

        if (boundTree.equals(leaf)) {
          if (locationRemaining.isEmpty()) {
            return true;
          } else {
            pathRemaining = parentPath;
          }
        } else {
          return false;
        }
      } else if (parent.getKind() == Tree.Kind.ARRAY_TYPE) {
        if (loc.tag != TypePathEntryKind.ARRAY) {
          return false;
        }
        locationRemaining.remove(locationRemaining.size() - 1);
        // annotating Integer @A []
        parentPath = TreeFinder.largestContainingArray(parentPath);
        parent = parentPath.getLeaf();
        // System.out.printf("parent instanceof ArrayTypeTree: %s loc=%d%n",
        //                   parent, loc);
        Tree elt = ((ArrayTypeTree) parent).getType();
        while (locationRemaining.size() > 0
                && locationRemaining.get(locationRemaining.size() - 1).tag == TypePathEntryKind.ARRAY) {
          if (elt.getKind() != Tree.Kind.ARRAY_TYPE) { // ArrayTypeTree
            if (debug) {
              System.out.printf("Element: %s is not an ArrayTypeTree and therefore false.\n", elt);
            }
            return false;
          }
          elt = ((ArrayTypeTree) elt).getType();
          locationRemaining.remove(locationRemaining.size() - 1);
        }

        boolean b = elt.equals(leaf);
        if (debug) {
          System.out.printf("parent %s instanceof ArrayTypeTree: %b %s %s %s%n",
                           parent, elt.equals(leaf), elt, leaf, loc);
          System.out.printf("b=%s elt=%s leaf=%s%n", b, elt, leaf);
        }

        // TODO:  The parent criterion should be exact, not just "in".
        // Otherwise the criterion [1]  matches  [5 4 3 2 1].
        // This is a disadvantage of working from the inside out instead of the outside in.
        if (b) {
          pathRemaining = parentPath;
        } else {
          return false;
        }
      } else if (parent.getKind() == Tree.Kind.NEW_ARRAY) {
        if (loc.tag != TypePathEntryKind.ARRAY) {
          return false;
        }
        if (debug) {
          System.out.println("Parent is a NEW_ARRAY and always gives true.");
        }
        return true;
      } else {
        if (debug) {
          System.out.printf("unrecognized parent kind = %s%n", parent.getKind());
        }
        return false;
      }
    }

    // no (remaining) inner type location, want to annotate outermost type
    // e.g.,  @Nullable List list;
    //        @Nullable List<String> list;
    TreePath parentPath = pathRemaining.getParentPath();
    if (parentPath == null) {
      if (debug) {
        System.out.println("Parent path is null and therefore false.");
      }
      return false;
    }
    Tree parent = pathRemaining.getParentPath().getLeaf();
    if (debug) {
      leaf = pathRemaining.getLeaf();
      System.out.printf("No (remaining) inner type location:%n  leaf: %s %b%n  parent: %s %b%n  result: %s%n",
            Main.treeToString(leaf), isGenericOrArray(leaf),
            Main.treeToString(parent), isGenericOrArray(parent),
            ! isGenericOrArray(parent));
    }

    return ! isGenericOrArray(parent);
  }

  /**
   * @param fieldAccess
   * @return
   */
  private boolean isStatic(JCFieldAccess fieldAccess) {
    return fieldAccess.type != null
        && fieldAccess.type.getKind() == TypeKind.DECLARED
        && fieldAccess.type.tsym.isStatic();
  }

  private boolean isGenericOrArray(Tree t) {
    return ((t.getKind() == Tree.Kind.PARAMETERIZED_TYPE)
            || (t.getKind() == Tree.Kind.ARRAY_TYPE)
            || (t.getKind() == Tree.Kind.EXTENDS_WILDCARD)
            || (t.getKind() == Tree.Kind.SUPER_WILDCARD)
            || (t.getKind() == Tree.Kind.ANNOTATED_TYPE &&
                    isGenericOrArray(((AnnotatedTypeTree)t).getUnderlyingType()))
            // Monolithic:  one node for entire "new".  So, handle specially.
            // || (t.getKind() == Tree.Kind.NEW_ARRAY)
            );
  }

  @Override
  public Kind getKind() {
    return Criterion.Kind.GENERIC_ARRAY_LOCATION;
  }

  @Override
  public String toString() {
    return "GenericArrayLocationCriterion at " +
    ((location.isEmpty())
     ? "outermost type"
     : ("( " + location.toString() + " )"));
  }

  /**
   * Gets the type path location of this criterion.
   *
   * @return an unmodifiable list of {@link TypePathEntry}s
   */
  public List<TypePathEntry> getLocation() {
    return Collections.unmodifiableList(location);
  }
}