package org.checkerframework.common.util.debug;

import com.sun.source.util.TreePath;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.AbstractElementVisitor7;
import org.checkerframework.framework.source.SourceChecker;
import org.checkerframework.framework.source.SourceVisitor;
import org.checkerframework.framework.type.AnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.javacutil.AbstractTypeProcessor;
import org.checkerframework.javacutil.AnnotationProvider;

/**
 * Outputs the method signatures of a class with fully annotated types.
 *
 * <p>The class determines the effective annotations for a checker in source or the classfile.
 * Finding the effective annotations is useful for the following purposes:
 *
 * <ol>
 *   <li value="1">Debugging annotations in classfile
 *   <li value="2">Debugging the default annotations that are implicitly added by the checker
 * </ol>
 *
 * <p>The class can be used in two possible ways, depending on the type file:
 *
 * <ol>
 *   <li id="a">From source: the class is to be used as an annotation processor when reading
 *       annotations from source. It can be invoked via the command:
 *       <p>{@code javac -processor SignaturePrinter <java files> ...}
 *   <li id="b">From classfile: the class is to be used as an independent app when reading
 *       annotations from classfile. It can be invoked via the command:
 *       <p>{@code java SignaturePrinter <class name>}
 * </ol>
 *
 * By default, only the annotations explicitly written by the user are emitted. To view the default
 * and effective annotations in a class that are associated with a checker, the fully qualified name
 * of the checker needs to be passed as {@code -Achecker=} argument, e.g.
 *
 * <pre>{@code
 * javac -processor SignaturePrinter
 *       -Achecker=org.checkerframework.checker.nullness.NullnessChecker JavaFile.java
 * }</pre>
 */
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
@SupportedOptions("checker")
public class SignaturePrinter extends AbstractTypeProcessor {

    private SourceChecker checker;

    ///////// Initialization /////////////
    private void init(ProcessingEnvironment env, String checkerName) {
        if (checkerName != null) {
            try {
                Class<?> checkerClass = Class.forName(checkerName);
                Constructor<?> cons = checkerClass.getConstructor();
                checker = (SourceChecker) cons.newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            checker =
                    new SourceChecker() {

                        @Override
                        protected SourceVisitor<?, ?> createSourceVisitor() {
                            return null;
                        }

                        @Override
                        public AnnotationProvider getAnnotationProvider() {
                            throw new UnsupportedOperationException(
                                    "getAnnotationProvider is not implemented for this class.");
                        }
                    };
        }
        checker.init(env);
    }

    @Override
    public void typeProcessingStart() {
        super.typeProcessingStart();
        String checkerName = processingEnv.getOptions().get("checker");
        init(processingEnv, checkerName);
    }

    @Override
    public void typeProcess(TypeElement element, TreePath p) {
        // TODO: fix this mess
        // checker.currentPath = p;
        // CompilationUnitTree root = p != null ? p.getCompilationUnit() : null;
        // ElementPrinter printer = new ElementPrinter(checker.createTypeFactory(), System.out);
        // printer.visit(element);
    }

    ////////// Printer //////////
    static class ElementPrinter extends AbstractElementVisitor7<Void, Void> {
        private static final String INDENTION = "    ";

        private final PrintStream out;
        private String indent = "";
        private final AnnotatedTypeFactory factory;

        public ElementPrinter(AnnotatedTypeFactory factory, PrintStream out) {
            this.factory = factory;
            this.out = out;
        }

        public void printTypeParams(List<? extends AnnotatedTypeVariable> params) {
            if (params.isEmpty()) {
                return;
            }

            out.print("<");
            boolean isntFirst = false;
            for (AnnotatedTypeMirror param : params) {
                if (isntFirst) {
                    out.print(", ");
                }
                isntFirst = true;
                out.print(param);
            }
            out.print("> ");
        }

        public void printParameters(AnnotatedExecutableType type) {
            ExecutableElement elem = type.getElement();

            out.print("(");
            for (int i = 0; i < type.getParameterTypes().size(); ++i) {
                if (i != 0) {
                    out.print(", ");
                }
                printVariable(
                        type.getParameterTypes().get(i),
                        elem.getParameters().get(i).getSimpleName());
            }
            out.print(")");
        }

        public void printThrows(AnnotatedExecutableType type) {
            if (type.getThrownTypes().isEmpty()) {
                return;
            }

            out.print(" throws ");

            boolean isntFirst = false;
            for (AnnotatedTypeMirror thrown : type.getThrownTypes()) {
                if (isntFirst) {
                    out.print(", ");
                }
                isntFirst = true;
                out.print(thrown);
            }
        }

        public void printVariable(AnnotatedTypeMirror type, Name name, boolean isVarArg) {
            out.print(type);
            if (isVarArg) {
                out.println("...");
            }
            out.print(' ');
            out.print(name);
        }

        public void printVariable(AnnotatedTypeMirror type, Name name) {
            printVariable(type, name, false);
        }

        public void printType(AnnotatedTypeMirror type) {
            out.print(type);
            out.print(' ');
        }

        public void printName(CharSequence name) {
            out.print(name);
        }

        @Override
        public Void visitExecutable(ExecutableElement e, Void p) {
            out.print(indent);

            AnnotatedExecutableType type = factory.getAnnotatedType(e);
            printTypeParams(type.getTypeVariables());
            if (e.getKind() != ElementKind.CONSTRUCTOR) {
                printType(type.getReturnType());
            }
            printName(e.getSimpleName());
            printParameters(type);
            printThrows(type);
            out.println(';');

            return null;
        }

        @Override
        public Void visitPackage(PackageElement e, Void p) {
            throw new IllegalArgumentException("Cannot process packages");
        }

        private String typeIdentifier(TypeElement e) {
            switch (e.getKind()) {
                case INTERFACE:
                    return "interface";
                case CLASS:
                    return "class";
                case ANNOTATION_TYPE:
                    return "@interface";
                case ENUM:
                    return "enum";
                default:
                    throw new IllegalArgumentException("Not a type element: " + e.getKind());
            }
        }

        @Override
        public Void visitType(TypeElement e, Void p) {
            String prevIndent = indent;

            out.print(indent);
            out.print(typeIdentifier(e));
            out.print(' ');
            out.print(e.getSimpleName());
            out.print(' ');
            AnnotatedDeclaredType dt = factory.getAnnotatedType(e);
            printSupers(dt);
            out.println("{");

            indent += INDENTION;

            for (Element enclosed : e.getEnclosedElements()) {
                this.visit(enclosed);
            }

            indent = prevIndent;
            out.print(indent);
            out.println("}");

            return null;
        }

        private void printSupers(AnnotatedDeclaredType dt) {
            if (dt.directSuperTypes().isEmpty()) {
                return;
            }

            out.print("extends ");

            boolean isntFirst = false;
            for (AnnotatedDeclaredType st : dt.directSuperTypes()) {
                if (isntFirst) {
                    out.print(", ");
                }
                isntFirst = true;
                out.print(st);
            }
            out.print(' ');
        }

        @Override
        public Void visitTypeParameter(TypeParameterElement e, Void p) {
            throw new IllegalStateException("Shouldn't visit any type parameters");
        }

        @Override
        public Void visitVariable(VariableElement e, Void p) {
            if (!e.getKind().isField()) {
                throw new IllegalStateException("can only process fields, received " + e.getKind());
            }

            out.print(indent);
            AnnotatedTypeMirror type = factory.getAnnotatedType(e);
            this.printVariable(type, e.getSimpleName());
            out.println(';');

            return null;
        }
    }

    public static void printUsage() {
        System.out.println("   Usage: java SignaturePrinter [-Achecker=<checkerName>] classname");
    }

    private static final String CHECKER_ARG = "-Achecker=";

    public static void main(String[] args) {
        if (!(args.length == 1 && !args[0].startsWith(CHECKER_ARG))
                && !(args.length == 2 && args[0].startsWith(CHECKER_ARG))) {
            printUsage();
            return;
        }

        // process arguments
        String checkerName = "";
        if (args[0].startsWith(CHECKER_ARG)) {
            checkerName = args[0].substring(CHECKER_ARG.length());
        }

        // Setup compiler environment
        Context context = new Context();
        JavacProcessingEnvironment env = JavacProcessingEnvironment.instance(context);
        SignaturePrinter printer = new SignaturePrinter();
        printer.init(env, checkerName);

        String className = args[args.length - 1];
        TypeElement elem = env.getElementUtils().getTypeElement(className);
        if (elem == null) {
            System.err.println("Couldn't find class: " + className);
            return;
        }

        printer.typeProcess(elem, null);
    }
}
