package org.checkerframework.common.util.debug;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import java.util.Collection;
import java.util.Set;
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.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
import org.checkerframework.framework.type.QualifierHierarchy;
import org.checkerframework.framework.util.MultiGraphQualifierHierarchy;
import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;

/**
 * A testing class that can be used to test {@link TypeElement}. In particular it tests that the
 * types read from classfiles are the same to the ones from java files.
 *
 * <p>For testing, you need to do the following:
 *
 * <ol>
 *   <li>Run the Checker on the source file like any checker:
 *       <pre>{@code
 * java -processor org.checkerframework.common.util.debug.TypeOutputtingChecker [source-file]
 *
 * }</pre>
 *   <li>Run the Checker on the bytecode, by simply running the main and passing the qualified name,
 *       e.g.
 *       <pre>{@code
 * java org.checkerframework.common.util.debug.TypeOutputtingChecker [qualified-name]
 *
 * }</pre>
 *   <li>Apply a simple diff on the two outputs
 * </ol>
 */
public class TypeOutputtingChecker extends BaseTypeChecker {

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

    /**
     * Prints the types of the class and all of its enclosing fields, methods, and inner classes.
     */
    public static class Visitor extends BaseTypeVisitor<GenericAnnotatedTypeFactory<?, ?, ?, ?>> {
        String currentClass;

        public Visitor(BaseTypeChecker checker) {
            super(checker);
        }

        // Print types of classes, methods, and fields
        @Override
        public void processClassTree(ClassTree node) {
            TypeElement element = TreeUtils.elementFromDeclaration(node);
            currentClass = element.getSimpleName().toString();

            AnnotatedDeclaredType type = atypeFactory.getAnnotatedType(node);
            System.out.println(node.getSimpleName() + "\t" + type + "\t" + type.directSuperTypes());

            super.processClassTree(node);
        }

        @Override
        public Void visitMethod(MethodTree node, Void p) {
            ExecutableElement elem = TreeUtils.elementFromDeclaration(node);

            AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(node);
            System.out.println(currentClass + "." + elem + "\t\t" + type);
            // Don't dig deeper
            return null;
        }

        @Override
        public Void visitVariable(VariableTree node, Void p) {
            VariableElement elem = TreeUtils.elementFromDeclaration(node);
            if (elem.getKind().isField()) {
                AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(node);
                System.out.println(currentClass + "." + elem + "\t\t" + type);
            }

            // Don't dig deeper
            return null;
        }
    }

    public static void main(String[] args) {
        new TypeOutputtingChecker().run(args);
    }

    public void run(String[] args) {
        ProcessingEnvironment env = JavacProcessingEnvironment.instance(new Context());
        Elements elements = env.getElementUtils();

        // TODO: Instead of using a GeneralAnnotatedTypeFactory, just use standard javac classes
        // to print explicit annotations.
        AnnotatedTypeFactory atypeFactory = new GeneralAnnotatedTypeFactory(this);

        for (String className : args) {
            TypeElement typeElt = elements.getTypeElement(className);
            printClassType(typeElt, atypeFactory);
        }
    }

    /**
     * Prints the types of the class and all of its enclosing fields, methods, and inner classes.
     */
    protected static void printClassType(TypeElement typeElt, AnnotatedTypeFactory atypeFactory) {
        assert typeElt != null;

        String simpleName = typeElt.getSimpleName().toString();
        // Output class info
        AnnotatedDeclaredType type = atypeFactory.fromElement(typeElt);
        System.out.println(simpleName + "\t" + type + "\t" + type.directSuperTypes());

        // output fields and methods
        for (Element enclosedElt : typeElt.getEnclosedElements()) {
            if (enclosedElt instanceof TypeElement) {
                printClassType((TypeElement) enclosedElt, atypeFactory);
            }
            if (!enclosedElt.getKind().isField() && !(enclosedElt instanceof ExecutableElement)) {
                continue;
            }
            AnnotatedTypeMirror memberType = atypeFactory.fromElement(enclosedElt);
            System.out.println(simpleName + "." + enclosedElt + "\t\t" + memberType);
        }
    }

    /**
     * Stores any explicit annotation in AnnotatedTypeMirrors. It doesn't have a qualifier
     * hierarchy, so it violates most of the specifications for AnnotatedTypeMirrors and
     * AnnotatedTypeFactorys, which may cause crashes and other unexpected behaviors.
     */
    public static class GeneralAnnotatedTypeFactory extends AnnotatedTypeFactory {

        public GeneralAnnotatedTypeFactory(BaseTypeChecker checker) {
            super(checker);
            postInit();
        }

        @Override
        public void postProcessClassTree(ClassTree tree) {
            // Do not store the qualifiers determined by this factory.
            // This factory adds declaration annotations as type annotations,
            // because TypeFromElement needs to read declaration annotations
            // and this factory blindly supports all annotations.
            // When storing those annotation to bytecode, the compiler chokes.
            // See testcase tests/nullness/GeneralATFStore.java
        }

        /** Return true to support any qualifier. No handling of aliases. */
        @Override
        public boolean isSupportedQualifier(AnnotationMirror a) {
            return true;
        }

        @Override
        public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) {
            return new GeneralQualifierHierarchy(factory);
        }

        /**
         * A very limited QualifierHierarchy that is used for access to qualifiers from different
         * type systems.
         */
        static class GeneralQualifierHierarchy extends MultiGraphQualifierHierarchy {

            public GeneralQualifierHierarchy(MultiGraphFactory factory) {
                super(factory);
            }

            // Always return true
            @Override
            public boolean isValid() {
                return true;
            }

            // Return the qualifier itself instead of the top.
            @Override
            public AnnotationMirror getTopAnnotation(AnnotationMirror start) {
                return start;
            }

            // Return the qualifier itself instead of the bottom.
            @Override
            public AnnotationMirror getBottomAnnotation(AnnotationMirror start) {
                return start;
            }

            // Never find a corresponding qualifier.
            @Override
            public AnnotationMirror findAnnotationInSameHierarchy(
                    Collection<? extends AnnotationMirror> annotations,
                    AnnotationMirror annotationMirror) {
                return null;
            }

            // Not needed - raises error.
            @Override
            public Set<AnnotationMirror> getTopAnnotations() {
                throw new BugInCF(
                        "GeneralQualifierHierarchy:getTopAnnotations() shouldn't be called");
            }

            // Not needed - should raise error. Unfortunately, in inference we ask for bottom
            // annotations.
            // Return a dummy value that does no harm.
            @Override
            public Set<AnnotationMirror> getBottomAnnotations() {
                // throw new BugInCF("GeneralQualifierHierarchy.getBottomAnnotations()
                // shouldn't be called");
                return AnnotationUtils.createAnnotationSet();
            }

            // Not needed - raises error.
            @Override
            public Set<? extends AnnotationMirror> getTypeQualifiers() {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.getTypeQualifiers() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) {
                throw new BugInCF("GeneralQualifierHierarchy.isSubtype() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public boolean isSubtypeTypeVariable(
                    AnnotationMirror subAnno, AnnotationMirror superAnno) {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.isSubtypeTypeVariable() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public boolean isSubtype(
                    Collection<? extends AnnotationMirror> rhs,
                    Collection<? extends AnnotationMirror> lhs) {
                throw new BugInCF("GeneralQualifierHierarchy.isSubtype() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public boolean isSubtypeTypeVariable(
                    Collection<? extends AnnotationMirror> subAnnos,
                    Collection<? extends AnnotationMirror> superAnnos) {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.isSubtypeTypeVariable() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.leastUpperBound() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public AnnotationMirror leastUpperBoundTypeVariable(
                    AnnotationMirror a1, AnnotationMirror a2) {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.leastUpperBoundTypeVariable() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.greatestLowerBound() shouldn't be called.");
            }

            // Not needed - raises error.
            @Override
            public AnnotationMirror greatestLowerBoundTypeVariable(
                    AnnotationMirror a1, AnnotationMirror a2) {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.greatestLowerBoundTypeVariable() shouldn't be called.");
            }

            @Override
            public AnnotationMirror getPolymorphicAnnotation(AnnotationMirror start) {
                throw new BugInCF(
                        "GeneralQualifierHierarchy.getPolymorphicAnnotation() shouldn't be called.");
            }
        }
    }
}
