package org.checkerframework.common.util.debug;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.tree.JCTree.JCNewArray;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;

/**
 * A utility class for displaying the structure of the AST of a program.
 *
 * <p>The class is actually an annotation processor; in order to use it, invoke the compiler on the
 * source file(s) for which you wish to view the structure of the program. You may also wish to use
 * the {@code -proc:only} javac option to stop compilation after annotation processing. (But, in
 * general {@code -proc:only} causes type annotation processors not to be run.)
 *
 * <p>The utility will display the {@link Kind} of each node it encounters while scanning the AST,
 * indented according to its depth in the tree. Additionally, the names of identifiers and member
 * selection trees are displayed (since these names are not tree nodes and therefore not directly
 * visited during AST traversal).
 *
 * @see org.checkerframework.common.util.debug.TreePrinter
 */
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class TreeDebug extends AbstractProcessor {

    protected Visitor createSourceVisitor(CompilationUnitTree root) {
        return new Visitor();
    }

    private static final String LINE_SEPARATOR = System.lineSeparator();

    public static class Visitor extends TreePathScanner<Void, Void> {

        private final StringBuilder buf;

        public Visitor() {
            buf = new StringBuilder();
        }

        @Override
        public Void scan(Tree node, Void p) {

            // Indent according to subtrees.
            if (getCurrentPath() != null) {
                for (TreePath tp = getCurrentPath(); tp != null; tp = tp.getParentPath()) {
                    buf.append("  ");
                }
            }

            // Add node kind to the buffer.
            if (node == null) {
                buf.append("null");
            } else {
                buf.append(node.getKind());
            }
            buf.append(LINE_SEPARATOR);

            // Visit subtrees.
            super.scan(node, p);

            // Display and clear the buffer.
            System.out.print(buf.toString());
            buf.setLength(0);

            return null;
        }

        /**
         * Splices additional information for a node into the buffer.
         *
         * @param text additional information for the node
         */
        private final void insert(Object text) {
            buf.insert(buf.length() - 1, " ");
            buf.insert(buf.length() - 1, text);
        }

        @Override
        public Void visitIdentifier(IdentifierTree node, Void p) {
            insert(node);
            return super.visitIdentifier(node, p);
        }

        @Override
        public Void visitMemberSelect(MemberSelectTree node, Void p) {
            insert(node.getExpression() + "." + node.getIdentifier());
            return super.visitMemberSelect(node, p);
        }

        @Override
        public Void visitNewArray(NewArrayTree node, Void p) {
            insert(((JCNewArray) node).annotations);
            insert("|");
            insert(((JCNewArray) node).dimAnnotations);
            return super.visitNewArray(node, p);
        }

        @Override
        public Void visitLiteral(LiteralTree node, Void p) {
            insert(node.getValue());
            return super.visitLiteral(node, p);
        }
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement element : ElementFilter.typesIn(roundEnv.getRootElements())) {
            TreePath path = Trees.instance(processingEnv).getPath(element);
            new Visitor().scan(path, null);
        }
        return false;
    }
}
