package annotations.io;

import java.util.ArrayDeque;
import java.util.Deque;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;

/**
 * Structure bundling an {@link ASTPath} with information about its
 *  starting point. Necessary because the {@link ASTPath} structure
 *  does not include the declaration from which it originates.
 *
 * @author dbro
 */
public class ASTRecord implements Comparable<ASTRecord> {
  /**
   * The AST to which this {@code ASTRecord} pertains.
   */
  public final CompilationUnitTree ast;

  /**
   * Name of the enclosing class declaration.
   */
  public final String className;

  /**
   * Name of the enclosing method declaration, or null if there is none.
   */
  public final String methodName;

  /**
   * Name of the enclosing variable declaration, or null if there is none.
   */
  public final String varName;

  /**
   * Path through AST, from specified declaration to descendant node.
   */
  public final ASTPath astPath;

  public ASTRecord(CompilationUnitTree ast, String className,
      String methodName, String varName, ASTPath astPath) {
    this.ast = ast;
    this.className = className;
    this.methodName = methodName;
    this.varName = varName;
    // FIXME: ensure path is canonical
    if (varName != null) {
      // TODO?
    } else if (methodName != null) {
      int n = astPath.size();
      if (n > 0 && astPath.get(0).getTreeKind() != Tree.Kind.METHOD
          && astPath.get(0).getTreeKind() != Tree.Kind.VARIABLE) {
        ASTPath bodyPath = ASTPath.empty().add(
            new ASTPath.ASTEntry(Tree.Kind.METHOD, ASTPath.BODY));
        for (int i = 0; i < n; i++) { bodyPath = bodyPath.add(astPath.get(i)); }
        astPath = bodyPath;
      }
    }
    this.astPath = astPath;
  }

  public ASTRecord newArrayLevel(int depth) {
    return new ASTRecord(ast, className, methodName, varName,
        astPath.extendNewArray(depth));
  }

  public ASTRecord replacePath(ASTPath newPath) {
    return new ASTRecord(ast, className, methodName, varName, newPath);
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof ASTRecord && equals((ASTRecord) o);
  }

  public boolean equals(ASTRecord astRecord) {
    return compareTo(astRecord) == 0;
  }

  @Override
  public int compareTo(ASTRecord rec) {
    int d = ast == null
        ? rec.ast == null ? 0 : -1
        : rec.ast == null ? 1 : Integer
            .compare(ast.hashCode(), rec.ast.hashCode());
    if (d == 0) {
      d = className == null
          ? rec.className == null ? 0 : -1
          : rec.className == null ? 1 : className.compareTo(rec.className);
      if (d == 0) {
        d = methodName == null
            ? rec.methodName == null ? 0 : -1
            : rec.methodName == null ? 1 : methodName.compareTo(rec.methodName);
        if (d == 0) {
          d = varName == null
              ? rec.varName == null ? 0 : -1
              : rec.varName == null ? 1 : varName.compareTo(rec.varName);
          if (d == 0) {
            d = astPath == null
                ? rec.astPath == null ? 0 : -1
                : rec.astPath == null ? 1 : astPath.compareTo(rec.astPath);
          }
        }
      }
    }
    return d;
  }

  @Override
  public int hashCode() {
    return ast.hashCode()
        ^ (className == null ? 0
            : Integer.rotateRight(className.hashCode(), 3))
        ^ (methodName == null ? 0
            : Integer.rotateRight(methodName.hashCode(), 6))
        ^ (varName == null ? 0
            : Integer.rotateRight(varName.hashCode(), 9))
        ^ (astPath == null ? 0
            : Integer.rotateRight(astPath.hashCode(), 12));
  }

  /**
   * Indicates whether this record identifies the given {@link TreePath}.
   */
  public boolean matches(TreePath treePath) {
    String clazz = null;
    String meth = null;
    String var = null;
    boolean matchVars = false;  // members only!
    Deque<Tree> stack = new ArrayDeque<Tree>();
    for (Tree tree : treePath) { stack.push(tree); }
    while (!stack.isEmpty()) {
      Tree tree = stack.pop();
      switch (tree.getKind()) {
      case CLASS:
      case INTERFACE:
      case ENUM:
      case ANNOTATION_TYPE:
        clazz = ((ClassTree) tree).getSimpleName().toString();
        meth = null;
        var = null;
        matchVars = true;
        break;
      case METHOD:
        assert meth == null;
        meth = ((MethodTree) tree).getName().toString();
        matchVars = false;
        break;
      case VARIABLE:
        if (matchVars) {
          assert var == null;
          var = ((VariableTree) tree).getName().toString();
          matchVars = false;
        }
        break;
      default:
        matchVars = false;
        continue;
      }
    }
    return className.equals(clazz)
        && (methodName == null ? meth == null : methodName.equals(meth))
        && (varName == null ? var == null : varName.equals(var))
        && astPath.matches(treePath);
  }

  @Override
  public String toString() {
    return new StringBuilder()
        .append(className == null ? "" : className).append(":")
        .append(methodName == null ? "" : methodName).append(":")
        .append(varName == null ? "" : varName).append(":")
        .append(astPath).toString();
  }

  public ASTRecord extend(ASTPath.ASTEntry entry) {
    return new ASTRecord(ast, className, methodName, varName,
        astPath.extend(entry));
  }

  public ASTRecord extend(Kind kind, String sel) {
    return extend(new ASTPath.ASTEntry(kind, sel));
  }

  public ASTRecord extend(Kind kind, String sel, int arg) {
    return extend(new ASTPath.ASTEntry(kind, sel, arg));
  }
}