package testlib.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.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.framework.source.Result;
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)}.
 */
/*
 * The code here is one of the most ugliest I have ever written.  I should revise
 * it in the future.  - Mahmood
 */
@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 getMessages() {
        // 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(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 CompilerMessageChecker
                        @SuppressWarnings("compilermessages")
                        Result res =
                                Result.failure(
                                        "type.unexpected",
                                        tree.toString(),
                                        actualType,
                                        expectedType);
                        FactoryTestChecker.this.report(res, tree);
                    }
                }
            }
            return super.scan(tree, p);
        }
    }
}
