package org.checkerframework.framework.testchecker.util;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.tree.JCTree;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.tools.JavaFileObject;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.framework.source.SourceChecker;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;

/**
 * A specialized checker for testing purposes. It compares an expression's annotated type to an
 * expected type.
 *
 * <p>The expected type is written in a stylized comment (starting with "///") in the same Java
 * source file. The comment appears either on the same line as the expression, or else by itself on
 * the line preceding the expression.
 *
 * <p>The comments are of two forms:
 *
 * <ul>
 *   <li>{@code /// <expected type>}: to specify the type of the expression in the expression
 *       statement
 *   <li>{@code /// <subtree> -:- <expected type>}: to specify the type of the given subexpression
 *       within the line.
 * </ul>
 *
 * The specified types are allowed to use simple names (e.g., {@code List<String>}), instead of
 * fully qualified names (e.g., {@code java.util.List<java.lang.String>}).
 *
 * <p>Example:
 *
 * <pre>
 *  void test() {
 *      // Comments in the same line
 *      Collections.<@NonNull String>emptyList();  /// List<@NonNull String>
 *      List<@NonNull String> l = Collections.emptyList(); /// Collections.emptyList() -:- List<@NonNull String>
 *
 *      // Comments in the previous lines
 *      /// List<@NonNull String>
 *      Collections.<@NonNull String>emptyList();
 *
 *      /// Collections.emptyList() -:- List<@NonNull String>
 *      List<@NonNull String> l = Collections.emptyList();
 *  }
 * </pre>
 *
 * The fully qualified name of the custom <i>AnnotatedTypeFactory</i> is specified through an {@code
 * -Afactory} command-line argument (e.g. {@code
 * -Afactory=checkers.nullness.NullnessAnnotatedTypeFactory}). The factory needs to have a
 * constructor of the form {@code <init>(ProcessingEnvironment, CompilationUnitTree)}.
 */
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions({"checker"})
public class FactoryTestChecker extends BaseTypeChecker {
  SourceChecker checker;

  @Override
  public void initChecker() {
    super.initChecker();

    // Find factory constructor
    String checkerClassName = getOption("checker");
    try {
      if (checkerClassName != null) {
        Class<?> checkerClass = Class.forName(checkerClassName);
        Constructor<?> constructor = checkerClass.getConstructor();
        Object o = constructor.newInstance();
        if (o instanceof SourceChecker) {
          checker = (SourceChecker) o;
        }
      }
    } catch (Exception e) {
      throw new BugInCF("Couldn't load " + checkerClassName + " class.");
    }
  }

  /*
  @Override
  public AnnotatedTypeFactory createTypeFactory() {
      return checker.createTypeFactory();
  }*/

  @Override
  public Properties getMessagesProperties() {
    // We don't have any properties
    Properties prop = new Properties();
    prop.setProperty(
        "type.unexpected",
        "unexpected type for the given tree%n"
            + "Tree       : %s%n"
            + "Found      : %s%n"
            + "Expected   : %s%n");
    return prop;
  }

  @Override
  protected BaseTypeVisitor<?> createSourceVisitor() {
    return new ToStringVisitor(this);
  }

  /** Builds the expected type for the trees from the source file of the tree compilation unit. */
  // This method is extremely ugly
  private Map<TreeSpec, String> buildExpected(CompilationUnitTree tree) {
    Map<TreeSpec, String> expected = new HashMap<>();
    try {
      JavaFileObject o = tree.getSourceFile();
      File sourceFile = new File(o.toUri());
      LineNumberReader reader = new LineNumberReader(new FileReader(sourceFile));
      String line = reader.readLine();
      Pattern prevsubtreePattern = Pattern.compile("\\s*///(.*)-:-(.*)");
      Pattern prevfulltreePattern = Pattern.compile("\\s*///(.*)");
      Pattern subtreePattern = Pattern.compile("(.*)///(.*)-:-(.*)");
      Pattern fulltreePattern = Pattern.compile("(.*)///(.*)");
      while (line != null) {
        Matcher prevsubtreeMatcher = prevsubtreePattern.matcher(line);
        Matcher prevfulltreeMatcher = prevfulltreePattern.matcher(line);
        Matcher subtreeMatcher = subtreePattern.matcher(line);
        Matcher fulltreeMatcher = fulltreePattern.matcher(line);
        if (prevsubtreeMatcher.matches()) {
          String treeString = prevsubtreeMatcher.group(1).trim();
          if (treeString.endsWith(";")) {
            treeString = treeString.substring(0, treeString.length() - 1);
          }
          TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber() + 1);
          expected.put(treeSpec, canonizeTypeString(prevsubtreeMatcher.group(2)));
        } else if (prevfulltreeMatcher.matches()) {
          String treeString = reader.readLine().trim();
          if (treeString.endsWith(";")) {
            treeString = treeString.substring(0, treeString.length() - 1);
          }
          TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber());
          expected.put(treeSpec, canonizeTypeString(prevfulltreeMatcher.group(1)));
        } else if (subtreeMatcher.matches()) {
          String treeString = subtreeMatcher.group(2).trim();
          if (treeString.endsWith(";")) {
            treeString = treeString.substring(0, treeString.length() - 1);
          }
          TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber());
          expected.put(treeSpec, canonizeTypeString(subtreeMatcher.group(3)));
        } else if (fulltreeMatcher.matches()) {
          String treeString = fulltreeMatcher.group(1).trim();
          if (treeString.endsWith(";")) {
            treeString = treeString.substring(0, treeString.length() - 1);
          }
          TreeSpec treeSpec = new TreeSpec(treeString.trim(), reader.getLineNumber());
          expected.put(treeSpec, canonizeTypeString(fulltreeMatcher.group(2)));
        }
        line = reader.readLine();
      }
      reader.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return expected;
  }

  /** A method to canonize the tree representation. */
  private static String canonizeTreeString(String str) {
    String canon = str.trim();
    Pattern pattern = Pattern.compile("(@\\S+)\\(\\)");
    Matcher matcher = pattern.matcher(canon);
    while (matcher.find()) {
      canon = matcher.replaceFirst(matcher.group(1));
      matcher.reset(canon);
    }
    return canon.trim();
  }

  /**
   * A method to canonize type string representation. It removes any unnecessary white spaces and
   * finds the type simple name instead of the fully qualified name.
   *
   * @param str the type string representation
   * @return a canonical representation of the type
   */
  private static String canonizeTypeString(String str) {
    String canon = str.trim();
    canon = canon.replaceAll("\\s+", " ");
    // Remove spaces between [ ]
    canon = canon.replaceAll("\\[\\s+", "[");
    canon = canon.replaceAll("\\s+\\]", "]");

    // Remove spaces between < >
    canon = canon.replaceAll("<\\s+", "<");
    canon = canon.replaceAll("\\s+>", ">");

    // Take simply names!
    canon = canon.replaceAll("[^\\<]*\\.(?=\\w)", "");
    return canon;
  }

  /**
   * A data structure that encapsulate a string and the line number that string appears in the
   * buffer
   */
  private static class TreeSpec {
    public final String treeString;
    public final long lineNumber;

    public TreeSpec(String treeString, long lineNumber) {
      this.treeString = canonizeTreeString(treeString);
      this.lineNumber = lineNumber;
    }

    @Override
    public int hashCode() {
      return Objects.hash(treeString, lineNumber);
    }

    @Override
    public boolean equals(@Nullable Object o) {
      if (o instanceof TreeSpec) {
        TreeSpec other = (TreeSpec) o;
        return treeString.equals(other.treeString) && lineNumber == other.lineNumber;
      }
      return false;
    }

    @Override
    public String toString() {
      return lineNumber + ":" + treeString;
    }
  }

  /**
   * A specialized visitor that compares the actual and expected types for the specified trees and
   * report an error if they differ
   */
  private class ToStringVisitor extends BaseTypeVisitor<GenericAnnotatedTypeFactory<?, ?, ?, ?>> {
    Map<TreeSpec, String> expected;

    public ToStringVisitor(BaseTypeChecker checker) {
      super(checker);
      this.expected = buildExpected(root);
    }

    @Override
    public Void scan(Tree tree, Void p) {
      if (TreeUtils.isExpressionTree(tree)) {
        ExpressionTree expTree = (ExpressionTree) tree;
        TreeSpec treeSpec =
            new TreeSpec(
                expTree.toString().trim(), root.getLineMap().getLineNumber(((JCTree) expTree).pos));
        if (expected.containsKey(treeSpec)) {
          String actualType = canonizeTypeString(atypeFactory.getAnnotatedType(expTree).toString());
          String expectedType = expected.get(treeSpec);
          if (!actualType.equals(expectedType)) {

            // The key is added above using a setProperty call, which is not supported
            // by the CompilerMessagesChecker.
            @SuppressWarnings("compilermessages")
            @CompilerMessageKey String key = "type.unexpected";
            FactoryTestChecker.this.reportError(
                tree, key, tree.toString(), actualType, expectedType);
          }
        }
      }
      return super.scan(tree, p);
    }
  }
}
