/*
 * Copyright 1999-2006 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

/*
 * Code derived from com.sun.tools.javac.tree.Pretty, from the langtools project.
 * A version can be found at, for example, http://hg.openjdk.java.net/jdk7/build/langtools
 */
package lombok.delombok;

import static com.sun.tools.javac.code.Flags.*;
import static lombok.javac.Javac.*;
import static lombok.javac.JavacTreeMaker.TreeTag.treeTag;
import static lombok.javac.JavacTreeMaker.TypeTag.typeTag;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import lombok.javac.CommentInfo;
import lombok.javac.CommentInfo.EndConnection;
import lombok.javac.CommentInfo.StartConnection;
import lombok.javac.JavacTreeMaker.TreeTag;
import lombok.javac.JavacTreeMaker.TypeTag;

import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.DocCommentTable;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCArrayAccess;
import com.sun.tools.javac.tree.JCTree.JCArrayTypeTree;
import com.sun.tools.javac.tree.JCTree.JCAssert;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCAssignOp;
import com.sun.tools.javac.tree.JCTree.JCBinary;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCBreak;
import com.sun.tools.javac.tree.JCTree.JCCase;
import com.sun.tools.javac.tree.JCTree.JCCatch;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.JCTree.JCConditional;
import com.sun.tools.javac.tree.JCTree.JCContinue;
import com.sun.tools.javac.tree.JCTree.JCDoWhileLoop;
import com.sun.tools.javac.tree.JCTree.JCEnhancedForLoop;
import com.sun.tools.javac.tree.JCTree.JCErroneous;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCExpressionStatement;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCForLoop;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCIf;
import com.sun.tools.javac.tree.JCTree.JCImport;
import com.sun.tools.javac.tree.JCTree.JCInstanceOf;
import com.sun.tools.javac.tree.JCTree.JCLabeledStatement;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCModifiers;
import com.sun.tools.javac.tree.JCTree.JCNewArray;
import com.sun.tools.javac.tree.JCTree.JCNewClass;
import com.sun.tools.javac.tree.JCTree.JCParens;
import com.sun.tools.javac.tree.JCTree.JCPrimitiveTypeTree;
import com.sun.tools.javac.tree.JCTree.JCReturn;
import com.sun.tools.javac.tree.JCTree.JCSkip;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.tree.JCTree.JCSwitch;
import com.sun.tools.javac.tree.JCTree.JCSynchronized;
import com.sun.tools.javac.tree.JCTree.JCThrow;
import com.sun.tools.javac.tree.JCTree.JCTry;
import com.sun.tools.javac.tree.JCTree.JCTypeApply;
import com.sun.tools.javac.tree.JCTree.JCTypeCast;
import com.sun.tools.javac.tree.JCTree.JCTypeParameter;
import com.sun.tools.javac.tree.JCTree.JCUnary;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.tree.JCTree.JCWhileLoop;
import com.sun.tools.javac.tree.JCTree.JCWildcard;
import com.sun.tools.javac.tree.JCTree.LetExpr;
import com.sun.tools.javac.tree.JCTree.TypeBoundKind;
import com.sun.tools.javac.tree.TreeInfo;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Position;
//import com.sun.tools.javac.code.TypeTags;

/** Prints out a tree as an indented Java source program.
 *
 *  <p><b>This is NOT part of any API supported by Sun Microsystems.  If
 *  you write code that depends on this, you do so at your own risk.
 *  This code and its internal interfaces are subject to change or
 *  deletion without notice.</b>
 */
@SuppressWarnings("all") // Mainly sun code that has other warning settings
public class PrettyCommentsPrinter extends JCTree.Visitor {
	private static final TreeTag PARENS = treeTag("PARENS");
	private static final TreeTag IMPORT = treeTag("IMPORT");
	private static final TreeTag VARDEF = treeTag("VARDEF");
	private static final TreeTag SELECT = treeTag("SELECT");
	
	private static final Map<TreeTag, String> OPERATORS;
	
	// StandardFlags | DEFAULT
	private static final long EXTENDED_STANDARD_FLAGS = 0x0fffL | 1L<<43;
	
	static {
		Map<TreeTag, String> map = new HashMap<TreeTag, String>();
		
		map.put(treeTag("POS"), "+");
		map.put(treeTag("NEG"), "-");
		map.put(treeTag("NOT"), "!");
		map.put(treeTag("COMPL"), "~");
		map.put(treeTag("PREINC"), "++");
		map.put(treeTag("PREDEC"), "--");
		map.put(treeTag("POSTINC"), "++");
		map.put(treeTag("POSTDEC"), "--");
		map.put(treeTag("NULLCHK"), "<*nullchk*>");
		map.put(treeTag("OR"), "||");
		map.put(treeTag("AND"), "&&");
		map.put(treeTag("EQ"), "==");
		map.put(treeTag("NE"), "!=");
		map.put(treeTag("LT"), "<");
		map.put(treeTag("GT"), ">");
		map.put(treeTag("LE"), "<=");
		map.put(treeTag("GE"), ">=");
		map.put(treeTag("BITOR"), "|");
		map.put(treeTag("BITXOR"), "^");
		map.put(treeTag("BITAND"), "&");
		map.put(treeTag("SL"), "<<");
		map.put(treeTag("SR"), ">>");
		map.put(treeTag("USR"), ">>>");
		map.put(treeTag("PLUS"), "+");
		map.put(treeTag("MINUS"), "-");
		map.put(treeTag("MUL"), "*");
		map.put(treeTag("DIV"), "/");
		map.put(treeTag("MOD"), "%");
		
		map.put(treeTag("BITOR_ASG"), "|=");
		map.put(treeTag("BITXOR_ASG"), "^=");
		map.put(treeTag("BITAND_ASG"), "&=");
		map.put(treeTag("SL_ASG"), "<<=");
		map.put(treeTag("SR_ASG"), ">>=");
		map.put(treeTag("USR_ASG"), ">>>=");
		map.put(treeTag("PLUS_ASG"), "+=");
		map.put(treeTag("MINUS_ASG"), "-=");
		map.put(treeTag("MUL_ASG"), "*=");
		map.put(treeTag("DIV_ASG"), "/=");
		map.put(treeTag("MOD_ASG"), "%=");
		
		OPERATORS = map;
	}
	
	private List<CommentInfo> comments;
	private final JCCompilationUnit cu;
	private boolean onNewLine = true;
	private boolean aligned = false;
	private boolean inParams = false;
	
	private boolean needsSpace = false;
	private boolean needsNewLine = false;
	private boolean needsAlign = false;
	
	// Flag for try-with-resources to make them not final and not print the last semicolon.
	// This flag is set just before printing the vardef and cleared when printing its modifiers.
	private boolean suppressFinalAndSemicolonsInTry = false;
	
	private final FormatPreferences formatPreferences;
	
	public PrettyCommentsPrinter(Writer out, JCCompilationUnit cu, List<CommentInfo> comments, FormatPreferences preferences) {
		this.out = out;
		this.comments = comments;
		this.cu = cu;
		this.formatPreferences = preferences;
	}
	
	private int endPos(JCTree tree) {
		return getEndPosition(tree, cu);
	}
	
	private void consumeComments(int until) throws IOException {
		consumeComments(until, null);
	}
	
	private void consumeComments(int until, JCTree tree) throws IOException {
		boolean prevNewLine = onNewLine;
		CommentInfo head = comments.head;
		while (comments.nonEmpty() && head.pos < until) {
			printComment(head);
			comments = comments.tail;
			head = comments.head;
		}
		if (!onNewLine && prevNewLine) {
			println();
		}
	}
	
	private void consumeTrailingComments(int from) throws IOException {
		boolean prevNewLine = onNewLine;
		CommentInfo head = comments.head;
		boolean stop = false;
		while (comments.nonEmpty() && head.prevEndPos == from && !stop && !(head.start == StartConnection.ON_NEXT_LINE || head.start == StartConnection.START_OF_LINE)) {
			from = head.endPos;
			printComment(head);
			stop = (head.end == EndConnection.ON_NEXT_LINE);
			comments = comments.tail;
			head = comments.head;
		}
		if (!onNewLine && prevNewLine) {
			println();
		}
	}
	
	private void printComment(CommentInfo comment) throws IOException {
		prepareComment(comment.start);
		print(comment.content);
		switch (comment.end) {
		case ON_NEXT_LINE:
			if (!aligned) {
				needsNewLine = true;
				needsAlign = true;
			}
			break;
		case AFTER_COMMENT:
			needsSpace = true;
			break;
		case DIRECT_AFTER_COMMENT:
			// do nothing
			break;
		}
	}
	
	private void prepareComment(StartConnection start) throws IOException {
		switch (start) {
			case DIRECT_AFTER_PREVIOUS:
				needsSpace = false;
				break;
			case AFTER_PREVIOUS:
				needsSpace = true;
				break;
			case START_OF_LINE:
				needsNewLine = true;
				needsAlign = false;
				break;
			case ON_NEXT_LINE:
				if (!aligned) {
					needsNewLine = true;
					needsAlign = true;
				}
				break;
		}
	}
	
	/** The output stream on which trees are printed.
	 */
	Writer out;
	
	/** The current left margin.
	 */
	int lmargin = 0;
	
	/** The enclosing class name.
	 */
	Name enclClassName;
	
	/** A hashtable mapping trees to their documentation comments
	 *  (can be null)
	 */
	Map<JCTree, String> docComments = null;
	DocCommentTable docTable = null;
	
	String getJavadocFor(JCTree node) {
		if (docComments != null) return docComments.get(node);
		if (docTable != null) return docTable.getCommentText(node);
		return null;
	}
	
	/** Align code to be indented to left margin.
	 */
	void align() throws IOException {
		onNewLine = false;
		aligned = true;
		needsAlign = false;
		for (int i = 0; i < lmargin; i++) out.write(formatPreferences.indent());
	}
	
	/** Increase left margin by indentation width.
	 */
	void indent() {
		lmargin++;
	}
	
	/** Decrease left margin by indentation width.
	 */
	void undent() {
		lmargin--;
	}
	
	/** Enter a new precedence level. Emit a `(' if new precedence level
	 *  is less than precedence level so far.
	 *  @param contextPrec	The precedence level in force so far.
	 *  @param ownPrec		The new precedence level.
	 */
	void open(int contextPrec, int ownPrec) throws IOException {
		if (ownPrec < contextPrec) out.write("(");
	}
	
	/** Leave precedence level. Emit a `(' if inner precedence level
	 *  is less than precedence level we revert to.
	 *  @param contextPrec	The precedence level we revert to.
	 *  @param ownPrec		The inner precedence level.
	 */
	void close(int contextPrec, int ownPrec) throws IOException {
		if (ownPrec < contextPrec) out.write(")");
	}
	
	/** Print string, replacing all non-ascii character with unicode escapes.
	 */
	public void print(Object s) throws IOException {
		boolean align = needsAlign;
		if (needsNewLine && !onNewLine) {
			println();
		}
		if (align && !aligned) {
			align();
		}
		if (needsSpace && !onNewLine && !aligned) {
			out.write(' ');
		}
		needsSpace = false;
		
		out.write(s.toString());
		
		onNewLine = false;
		aligned = false;
	}
	
	/** Print new line.
	 */
	public void println() throws IOException {
		onNewLine = true;
		aligned = false;
		needsNewLine = false;
		out.write(lineSep);
	}
	
	String lineSep = System.getProperty("line.separator");
	
	/**************************************************************************
	 * Traversal methods
	 *************************************************************************/
	
	/** Exception to propagate IOException through visitXXX methods */
	private static class UncheckedIOException extends Error {
		static final long serialVersionUID = -4032692679158424751L;
		UncheckedIOException(IOException e) {
			super(e.getMessage(), e);
		}
	}
	
	/** Visitor argument: the current precedence level.
	 */
	int prec;
	
	/** Visitor method: print expression tree.
	 *  @param prec  The current precedence level.
	 */
	public void printExpr(JCTree tree, int prec) throws IOException {
		
		int prevPrec = this.prec;
		try {
			this.prec = prec;
			if (tree == null) print("/*missing*/");
			else {
				consumeComments(tree.pos, tree);
				tree.accept(this);
				int endPos = endPos(tree);
				consumeTrailingComments(endPos);
			}
		} catch (UncheckedIOException ex) {
			IOException e = new IOException(ex.getMessage());
			e.initCause(ex);
			throw e;
		} finally {
			this.prec = prevPrec;
		}
	}
	
	/** Derived visitor method: print expression tree at minimum precedence level
	 *  for expression.
	 */
	public void printExpr(JCTree tree) throws IOException {
		printExpr(tree, TreeInfo.noPrec);
	}
	
	/** Derived visitor method: print statement tree.
	 */
	public void printStat(JCTree tree) throws IOException {
		if (isEmptyStat(tree)) {
			// printEmptyStat(); // -- starting in java 7, these get lost, so to be consistent, we never print them.
		} else {
			printExpr(tree, TreeInfo.notExpression);
		}
	}
	
	public void printEmptyStat() throws IOException {
		print(";");
	}
	
	public boolean isEmptyStat(JCTree tree) {
		if (!(tree instanceof JCBlock)) return false;
		JCBlock block = (JCBlock) tree;
		return (Position.NOPOS == block.pos) && block.stats.isEmpty();
	}

	/** Derived visitor method: print list of expression trees, separated by given string.
	 *  @param sep the separator string
	 */
	public <T extends JCTree> void printExprs(List<T> trees, String sep) throws IOException {
		if (trees.nonEmpty()) {
			printExpr(trees.head);
			for (List<T> l = trees.tail; l.nonEmpty(); l = l.tail) {
				print(sep);
				printExpr(l.head);
			}
		}
	}
	
	/** Derived visitor method: print list of expression trees, separated by commas.
	 */
	public <T extends JCTree> void printExprs(List<T> trees) throws IOException {
		printExprs(trees, ", ");
	}
	
	/** Derived visitor method: print list of statements, each on a separate line.
	 */
	public void printStats(List<? extends JCTree> trees) throws IOException {
		for (List<? extends JCTree> l = trees; l.nonEmpty(); l = l.tail) {
			if (isSuppressed(l.head)) continue;
			if (!suppressAlignmentForEmptyLines(l.head)) align();
			printStat(l.head);
			println();
		}
	}
	
	private boolean suppressAlignmentForEmptyLines(JCTree tree) {
		return !formatPreferences.fillEmpties() && startsWithNewLine(tree);
	}
	
	private boolean startsWithNewLine(JCTree tree) {
		return tree instanceof JCMethodDecl || tree instanceof JCClassDecl;
	}
	
	private boolean isSuppressed(JCTree tree) {
		if (isEmptyStat(tree)) {
			return true;
		}
		if (tree instanceof JCExpressionStatement) {
			return isNoArgsSuperCall(((JCExpressionStatement)tree).expr);
		}
		return false;
	}
	
	/** Print a set of modifiers.
	 */
	public void printFlags(long flags) throws IOException {
		if ((flags & SYNTHETIC) != 0) print("/*synthetic*/ ");
		if (suppressFinalAndSemicolonsInTry) {
			flags = flags & ~FINAL;
			suppressFinalAndSemicolonsInTry = false;
		}
		print(TreeInfo.flagNames(flags));
		if ((flags & EXTENDED_STANDARD_FLAGS) != 0) print(" ");
		if ((flags & ANNOTATION) != 0) print("@");
	}
	
	public void printAnnotations(List<JCAnnotation> trees) throws IOException {
		for (List<JCAnnotation> l = trees; l.nonEmpty(); l = l.tail) {
			printStat(l.head);
			if (inParams) { 
				print(" ");
			}
			else {
				println();
				align();
			}
		}
	}
	
	/** Print documentation comment, if it exists
	 *  @param tree	The tree for which a documentation comment should be printed.
	 */
	public void printDocComment(JCTree tree) throws IOException {
		String dc = getJavadocFor(tree);
		if (dc == null) return;
		print("/**"); println();
		int pos = 0;
		int endpos = lineEndPos(dc, pos);
		boolean atStart = true;
		while (pos < dc.length()) {
			String line = dc.substring(pos, endpos);
			if (line.trim().isEmpty() && atStart) {
				atStart = false;
				continue;
			}
			atStart = false;
			align();
			print(" *");
			if (pos < dc.length() && dc.charAt(pos) > ' ') print(" ");
			print(dc.substring(pos, endpos)); println();
			pos = endpos + 1;
			endpos = lineEndPos(dc, pos);
		}
		align(); print(" */"); println();
		align();
	}
//where
	static int lineEndPos(String s, int start) {
		int pos = s.indexOf('\n', start);
		if (pos < 0) pos = s.length();
		return pos;
	}
	
	/** If type parameter list is non-empty, print it enclosed in "<...>" brackets.
	 */
	public void printTypeParameters(List<JCTypeParameter> trees) throws IOException {
		if (trees.nonEmpty()) {
			print("<");
			printExprs(trees);
			print(">");
		}
	}
	
	/** Print a block.
	 */
	public void printBlock(List<? extends JCTree> stats, JCTree container) throws IOException {
		print("{");
		println();
		indent();
		printStats(stats);
		consumeComments(endPos(container));
		undent();
		align();
		print("}");
	}
	
	/** Print a block.
	 */
	public void printEnumBody(List<JCTree> stats) throws IOException {
		print("{");
		println();
		indent();
		boolean first = true;
		for (List<JCTree> l = stats; l.nonEmpty(); l = l.tail) {
			if (isEnumerator(l.head)) {
				if (!first) {
					print(",");
					println();
				}
				align();
				printStat(l.head);
				first = false;
			}
		}
		print(";");
		println();
		int x = 0;
		for (List<JCTree> l = stats; l.nonEmpty(); l = l.tail) {
			x++;
			if (!isEnumerator(l.head)) {
				if (!suppressAlignmentForEmptyLines(l.head)) align();
				printStat(l.head);
				println();
			}
		}
		undent();
		align();
		print("}");
	}
	
	public void printEnumMember(JCVariableDecl tree) throws IOException {
		printAnnotations(tree.mods.annotations);
		print(tree.name);
		if (tree.init instanceof JCNewClass) {
			JCNewClass constructor = (JCNewClass) tree.init;
			if (constructor.args != null && constructor.args.nonEmpty()) {
				print("(");
				printExprs(constructor.args);
				print(")");
			}
			if (constructor.def != null && constructor.def.defs != null) {
				print(" ");
				printBlock(constructor.def.defs, constructor.def);
			}
		}
	}
	
	/** Is the given tree an enumerator definition? */
	boolean isEnumerator(JCTree t) {
		return VARDEF.equals(treeTag(t)) && (((JCVariableDecl) t).mods.flags & ENUM) != 0;
	}
	
	/** Print unit consisting of package clause and import statements in toplevel,
	 *  followed by class definition. if class definition == null,
	 *  print all definitions in toplevel.
	 *  @param tree	 The toplevel tree
	 *  @param cdef	 The class definition, which is assumed to be part of the
	 *				  toplevel tree.
	 */
	public void printUnit(JCCompilationUnit tree, JCClassDecl cdef) throws IOException {
		Object dc = getDocComments(tree);
		loadDocCommentsTable(dc);
		printDocComment(tree);
		if (tree.pid != null) {
			consumeComments(tree.pos, tree);
			print("package ");
			printExpr(tree.pid);
			print(";");
			println();
		}
		boolean firstImport = true;
		for (List<JCTree> l = tree.defs;
		l.nonEmpty() && (cdef == null || IMPORT.equals(treeTag(l.head)));
		l = l.tail) {
			if (IMPORT.equals(treeTag(l.head))) {
				JCImport imp = (JCImport)l.head;
				Name name = TreeInfo.name(imp.qualid);
				if (name == name.table.fromChars(new char[] {'*'}, 0, 1) ||
						cdef == null ||
						isUsed(TreeInfo.symbol(imp.qualid), cdef)) {
					if (firstImport) {
						firstImport = false;
						println();
					}
					printStat(imp);
				}
			} else {
				printStat(l.head);
			}
		}
		if (cdef != null) {
			printStat(cdef);
			println();
		}
	}
	// where
	@SuppressWarnings("unchecked")
	private void loadDocCommentsTable(Object dc) {
		if (dc instanceof Map<?, ?>) this.docComments = (Map) dc;
		else if (dc instanceof DocCommentTable) this.docTable = (DocCommentTable) dc;
	}
	
	boolean isUsed(final Symbol t, JCTree cdef) {
		class UsedVisitor extends TreeScanner {
			public void scan(JCTree tree) {
				if (tree!=null && !result) tree.accept(this);
			}
			boolean result = false;
			public void visitIdent(JCIdent tree) {
				if (tree.sym == t) result = true;
			}
		}
		UsedVisitor v = new UsedVisitor();
		v.scan(cdef);
		return v.result;
	}
	
	/**************************************************************************
	 * Visitor methods
	 *************************************************************************/
	
	public void visitTopLevel(JCCompilationUnit tree) {
		try {
			printUnit(tree, null);
			consumeComments(Integer.MAX_VALUE);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitImport(JCImport tree) {
		try {
			print("import ");
			if (tree.staticImport) print("static ");
			printExpr(tree.qualid);
			print(";");
			println();
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitClassDef(JCClassDecl tree) {
		try {
			consumeComments(tree.pos, tree);
			println(); align();
			printDocComment(tree);
			printAnnotations(tree.mods.annotations);
			printFlags(tree.mods.flags & ~INTERFACE);
			Name enclClassNamePrev = enclClassName;
			enclClassName = tree.name;
			if ((tree.mods.flags & INTERFACE) != 0) {
				print("interface " + tree.name);
				printTypeParameters(tree.typarams);
				if (tree.implementing.nonEmpty()) {
					print(" extends ");
					printExprs(tree.implementing);
				}
			} else {
				if ((tree.mods.flags & ENUM) != 0)
					print("enum " + tree.name);
				else
					print("class " + tree.name);
				printTypeParameters(tree.typarams);
				if (getExtendsClause(tree) != null) {
					print(" extends ");
					printExpr(getExtendsClause(tree));
				}
				if (tree.implementing.nonEmpty()) {
					print(" implements ");
					printExprs(tree.implementing);
				}
			}
			print(" ");
			// <Added for delombok by Reinier Zwitserloot>
			if ((tree.mods.flags & INTERFACE) != 0) {
				removeImplicitModifiersForInterfaceMembers(tree.defs);
			}
			// </Added for delombok by Reinier Zwitserloot>
			if ((tree.mods.flags & ENUM) != 0) {
				printEnumBody(tree.defs);
			} else {
				printBlock(tree.defs, tree);
			}
			enclClassName = enclClassNamePrev;
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	// Added for delombok by Reinier Zwitserloot
	private void removeImplicitModifiersForInterfaceMembers(List<JCTree> defs) {
		for (JCTree def :defs) {
			if (def instanceof JCVariableDecl) {
				((JCVariableDecl) def).mods.flags &= ~(Flags.PUBLIC | Flags.STATIC | Flags.FINAL);
			}
			if (def instanceof JCMethodDecl) {
				((JCMethodDecl) def).mods.flags &= ~(Flags.PUBLIC | Flags.ABSTRACT);
			}
			if (def instanceof JCClassDecl) {
				((JCClassDecl) def).mods.flags &= ~(Flags.PUBLIC | Flags.STATIC);
			}
		}
	}
	
	public void visitMethodDef(JCMethodDecl tree) {
		try {
			boolean isConstructor = tree.name == tree.name.table.fromChars("<init>".toCharArray(), 0, 6);
			// when producing source output, omit anonymous constructors
			if (isConstructor && enclClassName == null) return;
			boolean isGeneratedConstructor = isConstructor && ((tree.mods.flags & Flags.GENERATEDCONSTR) != 0);
			if (isGeneratedConstructor) return;
			println(); align();
			printDocComment(tree);
			printExpr(tree.mods);
			printTypeParameters(tree.typarams);
			if (tree.typarams != null && tree.typarams.length() > 0) print(" ");
			if (tree.name == tree.name.table.fromChars("<init>".toCharArray(), 0, 6)) {
				print(enclClassName != null ? enclClassName : tree.name);
			} else {
				printExpr(tree.restype);
				print(" " + tree.name);
			}
			print("(");
			inParams = true;
			printExprs(tree.params);
			inParams = false;
			print(")");
			if (tree.thrown.nonEmpty()) {
				print(" throws ");
				printExprs(tree.thrown);
			}
			if (tree.defaultValue != null) {
			  print(" default ");
			  print(tree.defaultValue);
			}
			if (tree.body != null) {
				print(" ");
				printBlock(tree.body.stats, tree.body);
			} else {
				print(";");
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitVarDef(JCVariableDecl tree) {
		try {
			boolean suppressSemi = suppressFinalAndSemicolonsInTry;
			if (getJavadocFor(tree) != null) {
				println(); align();
			}
			printDocComment(tree);
			if ((tree.mods.flags & ENUM) != 0) {
				printEnumMember(tree);
			} else {
				printExpr(tree.mods);
				if ((tree.mods.flags & VARARGS) != 0) {
					printExpr(((JCArrayTypeTree) tree.vartype).elemtype);
					print("... " + tree.name);
				} else {
					printExpr(tree.vartype);
					print(" " + tree.name);
				}
				if (tree.init != null) {
					print(" = ");
					printExpr(tree.init);
				}
				if (prec == TreeInfo.notExpression && !suppressSemi) print(";");
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitSkip(JCSkip tree) {
		try {
			print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitBlock(JCBlock tree) {
		try {
			consumeComments(tree.pos);
			printFlags(tree.flags);
			printBlock(tree.stats, tree);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitDoLoop(JCDoWhileLoop tree) {
		try {
			print("do ");
			printStat(tree.body);
			align();
			print(" while ");
			if (PARENS.equals(treeTag(tree.cond))) {
				printExpr(tree.cond);
			} else {
				print("(");
				printExpr(tree.cond);
				print(")");
			}
			print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitWhileLoop(JCWhileLoop tree) {
		try {
			print("while ");
			if (PARENS.equals(treeTag(tree.cond))) {
				printExpr(tree.cond);
			} else {
				print("(");
				printExpr(tree.cond);
				print(")");
			}
			print(" ");
			printStat(tree.body);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitForLoop(JCForLoop tree) {
		try {
			print("for (");
			if (tree.init.nonEmpty()) {
				if (VARDEF.equals(treeTag(tree.init.head))) {
					printExpr(tree.init.head);
					for (List<JCStatement> l = tree.init.tail; l.nonEmpty(); l = l.tail) {
						JCVariableDecl vdef = (JCVariableDecl)l.head;
						print(", " + vdef.name + " = ");
						printExpr(vdef.init);
					}
				} else {
					printExprs(tree.init);
				}
			}
			print("; ");
			if (tree.cond != null) printExpr(tree.cond);
			print("; ");
			printExprs(tree.step);
			print(") ");
			printStat(tree.body);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitForeachLoop(JCEnhancedForLoop tree) {
		try {
			print("for (");
			printExpr(tree.var);
			print(" : ");
			printExpr(tree.expr);
			print(") ");
			printStat(tree.body);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitLabelled(JCLabeledStatement tree) {
		try {
			print(tree.label + ":");
			if (isEmptyStat(tree.body) || tree.body instanceof JCSkip) {
				print(" ;");
			} else if (tree.body instanceof JCBlock) {
				print(" ");
				printStat(tree.body);
			} else {
				println();
				align();
				printStat(tree.body);
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitSwitch(JCSwitch tree) {
		try {
			print("switch ");
			if (PARENS.equals(treeTag(tree.selector))) {
				printExpr(tree.selector);
			} else {
				print("(");
				printExpr(tree.selector);
				print(")");
			}
			print(" {");
			println();
			printStats(tree.cases);
			align();
			print("}");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitCase(JCCase tree) {
		try {
			if (tree.pat == null) {
				print("default");
			} else {
				print("case ");
				printExpr(tree.pat);
			}
			print(": ");
			println();
			indent();
			printStats(tree.stats);
			undent();
			align();
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitSynchronized(JCSynchronized tree) {
		try {
			print("synchronized ");
			if (PARENS.equals(treeTag(tree.lock))) {
				printExpr(tree.lock);
			} else {
				print("(");
				printExpr(tree.lock);
				print(")");
			}
			print(" ");
			printStat(tree.body);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitTry(JCTry tree) {
		try {
			print("try ");
			List<?> resources = null;
			try {
				Field f = JCTry.class.getField("resources");
				resources = (List<?>) f.get(tree);
			} catch (Exception ignore) {
				// In JDK6 and down this field does not exist; resources will retain its initializer value which is what we want.
			}
			
			if (resources != null && resources.nonEmpty()) {
				print("(");
				int remaining = resources.size();
				if (remaining == 1) {
					JCTree var = (JCTree) resources.get(0);
					suppressFinalAndSemicolonsInTry = true;
					printStat(var);
					print(") ");
				} else {
					indent(); indent();
					for (Object var0 : resources) {
						println();
						align();
						JCTree var = (JCTree) var0;
						suppressFinalAndSemicolonsInTry = true;
						printStat(var);
						remaining--;
						if (remaining > 0) print(";");
					}
					print(") ");
					undent(); undent();
				}
			}
			
			printStat(tree.body);
			for (List<JCCatch> l = tree.catchers; l.nonEmpty(); l = l.tail) {
				printStat(l.head);
			}
			if (tree.finalizer != null) {
				print(" finally ");
				printStat(tree.finalizer);
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitCatch(JCCatch tree) {
		try {
			print(" catch (");
			printExpr(tree.param);
			print(") ");
			printStat(tree.body);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitConditional(JCConditional tree) {
		try {
			open(prec, TreeInfo.condPrec);
			printExpr(tree.cond, TreeInfo.condPrec);
			print(" ? ");
			printExpr(tree.truepart, TreeInfo.condPrec);
			print(" : ");
			printExpr(tree.falsepart, TreeInfo.condPrec);
			close(prec, TreeInfo.condPrec);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitIf(JCIf tree) {
		try {
			print("if ");
			if (PARENS.equals(treeTag(tree.cond))) {
				printExpr(tree.cond);
			} else {
				print("(");
				printExpr(tree.cond);
				print(")");
			}
			print(" ");
			printStat(tree.thenpart);
			if (tree.elsepart != null) {
				print(" else ");
				printStat(tree.elsepart);
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	private boolean isNoArgsSuperCall(JCExpression expr) {
		if (!(expr instanceof JCMethodInvocation)) return false;
		JCMethodInvocation tree = (JCMethodInvocation) expr;
		if (!tree.typeargs.isEmpty() || !tree.args.isEmpty()) return false;
		if (!(tree.meth instanceof JCIdent)) return false;
		return ((JCIdent) tree.meth).name.toString().equals("super");
	}
	
	public void visitExec(JCExpressionStatement tree) {
		if (isNoArgsSuperCall(tree.expr)) return;
		try {
			printExpr(tree.expr);
			if (prec == TreeInfo.notExpression) print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitBreak(JCBreak tree) {
		try {
			print("break");
			if (tree.label != null) print(" " + tree.label);
			print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitContinue(JCContinue tree) {
		try {
			print("continue");
			if (tree.label != null) print(" " + tree.label);
			print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitReturn(JCReturn tree) {
		try {
			print("return");
			if (tree.expr != null) {
				print(" ");
				printExpr(tree.expr);
			}
			print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitThrow(JCThrow tree) {
		try {
			print("throw ");
			printExpr(tree.expr);
			print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitAssert(JCAssert tree) {
		try {
			print("assert ");
			printExpr(tree.cond);
			if (tree.detail != null) {
				print(" : ");
				printExpr(tree.detail);
			}
			print(";");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitApply(JCMethodInvocation tree) {
		try {
			if (!tree.typeargs.isEmpty()) {
				if (SELECT.equals(treeTag(tree.meth))) {
					JCFieldAccess left = (JCFieldAccess)tree.meth;
					printExpr(left.selected);
					print(".<");
					printExprs(tree.typeargs);
					print(">" + left.name);
				} else {
					print("<");
					printExprs(tree.typeargs);
					print(">");
					printExpr(tree.meth);
				}
			} else {
				printExpr(tree.meth);
			}
			print("(");
			printExprs(tree.args);
			print(")");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitNewClass(JCNewClass tree) {
		try {
			if (tree.encl != null) {
				printExpr(tree.encl);
				print(".");
			}
			print("new ");
			if (!tree.typeargs.isEmpty()) {
				print("<");
				printExprs(tree.typeargs);
				print(">");
			}
			printExpr(tree.clazz);
			print("(");
			printExprs(tree.args);
			print(")");
			if (tree.def != null) {
				Name enclClassNamePrev = enclClassName;
				enclClassName =
						tree.def.name != null ? tree.def.name :
							tree.type != null && tree.type.tsym.name != tree.type.tsym.name.table.fromChars(new char[0], 0, 0) ? tree.type.tsym.name :
								null;
				if ((tree.def.mods.flags & Flags.ENUM) != 0) print("/*enum*/");
				printBlock(tree.def.defs, tree.def);
				enclClassName = enclClassNamePrev;
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitNewArray(JCNewArray tree) {
		try {
			if (tree.elemtype != null) {
				print("new ");
				JCTree elem = tree.elemtype;
				if (elem instanceof JCArrayTypeTree)
					printBaseElementType((JCArrayTypeTree) elem);
				else
					printExpr(elem);
				for (List<JCExpression> l = tree.dims; l.nonEmpty(); l = l.tail) {
					print("[");
					printExpr(l.head);
					print("]");
				}
				if (elem instanceof JCArrayTypeTree)
					printBrackets((JCArrayTypeTree) elem);
			}
			if (tree.elems != null) {
				if (tree.elemtype != null) print("[]");
				print("{");
				printExprs(tree.elems);
				print("}");
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitParens(JCParens tree) {
		try {
			print("(");
			printExpr(tree.expr);
			print(")");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitAssign(JCAssign tree) {
		try {
			open(prec, TreeInfo.assignPrec);
			printExpr(tree.lhs, TreeInfo.assignPrec + 1);
			print(" = ");
			printExpr(tree.rhs, TreeInfo.assignPrec);
			close(prec, TreeInfo.assignPrec);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public String operatorName(TreeTag tag) {
		String result = OPERATORS.get(tag);
		if (result == null) throw new Error();
		return result;
	}
	
	public void visitAssignop(JCAssignOp tree) {
		try {
			open(prec, TreeInfo.assignopPrec);
			printExpr(tree.lhs, TreeInfo.assignopPrec + 1);
			String opname = operatorName(treeTag(tree));
			print(" " + opname + " ");
			printExpr(tree.rhs, TreeInfo.assignopPrec);
			close(prec, TreeInfo.assignopPrec);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitUnary(JCUnary tree) {
		try {
			int ownprec = isOwnPrec(tree);
			String opname = operatorName(treeTag(tree));
			open(prec, ownprec);
			if (isPrefixUnary(tree)) {
				print(opname);
				printExpr(tree.arg, ownprec);
			} else {
				printExpr(tree.arg, ownprec);
				print(opname);
			}
			close(prec, ownprec);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	private int isOwnPrec(JCExpression tree) {
		return treeTag(tree).getOperatorPrecedenceLevel();
	}
	
	private boolean isPrefixUnary(JCUnary tree) {
		return treeTag(tree).isPrefixUnaryOp();
	}
	
	public void visitBinary(JCBinary tree) {
		try {
			int ownprec = isOwnPrec(tree);
			String opname = operatorName(treeTag(tree));
			open(prec, ownprec);
			printExpr(tree.lhs, ownprec);
			print(" " + opname + " ");
			printExpr(tree.rhs, ownprec + 1);
			close(prec, ownprec);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitTypeCast(JCTypeCast tree) {
		try {
			open(prec, TreeInfo.prefixPrec);
			print("(");
			printExpr(tree.clazz);
			print(")");
			printExpr(tree.expr, TreeInfo.prefixPrec);
			close(prec, TreeInfo.prefixPrec);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitTypeTest(JCInstanceOf tree) {
		try {
			open(prec, TreeInfo.ordPrec);
			printExpr(tree.expr, TreeInfo.ordPrec);
			print(" instanceof ");
			printExpr(tree.clazz, TreeInfo.ordPrec + 1);
			close(prec, TreeInfo.ordPrec);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitIndexed(JCArrayAccess tree) {
		try {
			printExpr(tree.indexed, TreeInfo.postfixPrec);
			print("[");
			printExpr(tree.index);
			print("]");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitSelect(JCFieldAccess tree) {
		try {
			printExpr(tree.selected, TreeInfo.postfixPrec);
			print("." + tree.name);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitIdent(JCIdent tree) {
		try {
			print(tree.name);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitLiteral(JCLiteral tree) {
		TypeTag typeTag = typeTag(tree);
		try {
			if (CTC_INT.equals(typeTag)) print(tree.value.toString());
			else if (CTC_LONG.equals(typeTag)) print(tree.value + "L");
			else if (CTC_FLOAT.equals(typeTag)) print(tree.value + "F");
			else if (CTC_DOUBLE.equals(typeTag)) print(tree.value.toString());
			else if (CTC_CHAR.equals(typeTag)) {
				print("\'" + quoteChar((char)((Number)tree.value).intValue()) + "\'");
			}
			else if (CTC_BOOLEAN.equals(typeTag)) print(((Number)tree.value).intValue() == 1 ? "true" : "false");
			else if (CTC_BOT.equals(typeTag)) print("null");
			else print("\"" + quoteChars(tree.value.toString()) + "\"");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public static String quoteChars(String s) {
		StringBuilder buf = new StringBuilder();
		for (int i = 0; i < s.length(); i++) {
			buf.append(quoteChar(s.charAt(i)));
		}
		return buf.toString();
	}
	
	/**
	 * Escapes a character if it has an escape sequence or is non-printable
	 * ASCII. Leaves non-ASCII characters alone.
	 */
	public static String quoteChar(char ch) {
		switch (ch) {
		case '\b':
			return "\\b";
		case '\f':
			return "\\f";
		case '\n':
			return "\\n";
		case '\r':
			return "\\r";
		case '\t':
			return "\\t";
		case '\'':
			return "\\'";
		case '\"':
			return "\\\"";
		case '\\':
			return "\\\\";
		default:
			return ch < 32 ? String.format("\\%03o", (int) ch) : String.valueOf(ch);
		}
	}
	
	public void visitTypeIdent(JCPrimitiveTypeTree tree) {
		TypeTag typetag = typeTag(tree);
		try {
			if (CTC_BYTE.equals(typetag)) print ("byte");
			else if (CTC_CHAR.equals(typetag)) print ("char");
			else if (CTC_SHORT.equals(typetag)) print ("short");
			else if (CTC_INT.equals(typetag)) print ("int");
			else if (CTC_LONG.equals(typetag)) print ("long");
			else if (CTC_FLOAT.equals(typetag)) print ("float");
			else if (CTC_DOUBLE.equals(typetag)) print ("double");
			else if (CTC_BOOLEAN.equals(typetag)) print ("boolean");
			else if (CTC_VOID.equals(typetag)) print ("void");
			else print("error");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitTypeArray(JCArrayTypeTree tree) {
		try {
			printBaseElementType(tree);
			printBrackets(tree);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	// Prints the inner element type of a nested array
	private void printBaseElementType(JCArrayTypeTree tree) throws IOException {
		JCTree elem = tree.elemtype;
		while (elem instanceof JCWildcard)
			elem = ((JCWildcard) elem).inner;
		if (elem instanceof JCArrayTypeTree)
			printBaseElementType((JCArrayTypeTree) elem);
		else
			printExpr(elem);
	}
	
	// prints the brackets of a nested array in reverse order
	private void printBrackets(JCArrayTypeTree tree) throws IOException {
		JCTree elem;
		while (true) {
			elem = tree.elemtype;
			print("[]");
			if (!(elem instanceof JCArrayTypeTree)) break;
			tree = (JCArrayTypeTree) elem;
		}
	}
	
	public void visitTypeApply(JCTypeApply tree) {
		try {
			printExpr(tree.clazz);
			print("<");
			printExprs(tree.arguments);
			print(">");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitTypeParameter(JCTypeParameter tree) {
		try {
			List<JCExpression> annotations = readExpressionListOrNil(tree, "annotations");
			if (!annotations.isEmpty()) {
				printExprs(annotations);
				print(" ");
			}
			print(tree.name);
			if (tree.bounds.nonEmpty()) {
				print(" extends ");
				printExprs(tree.bounds, " & ");
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	@Override
	public void visitWildcard(JCWildcard tree) {
		try {
			Object kind = tree.getClass().getField("kind").get(tree);
			print(kind);
			if (kind != null && kind.getClass().getSimpleName().equals("TypeBoundKind")) {
				kind = kind.getClass().getField("kind").get(kind);
			}
			
			if (tree.getKind() != Tree.Kind.UNBOUNDED_WILDCARD)
				printExpr(tree.inner);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		} catch (NoSuchFieldException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}
	
	public void visitTypeBoundKind(TypeBoundKind tree) {
		try {
			print(String.valueOf(tree.kind));
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitErroneous(JCErroneous tree) {
		try {
			print("(ERROR)");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitLetExpr(LetExpr tree) {
		try {
			print("(let " + tree.defs + " in " + tree.expr + ")");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitModifiers(JCModifiers mods) {
		try {
			printAnnotations(mods.annotations);
			printFlags(mods.flags);
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitAnnotation(JCAnnotation tree) {
		try {
			print("@");
			printExpr(tree.annotationType);
			if (tree.args.nonEmpty()) {
				print("(");
				if (tree.args.length() == 1 && tree.args.get(0) instanceof JCAssign) {
					 JCExpression lhs = ((JCAssign)tree.args.get(0)).lhs;
					 if (lhs instanceof JCIdent && ((JCIdent)lhs).name.toString().equals("value")) tree.args = List.of(((JCAssign)tree.args.get(0)).rhs);
				}
				printExprs(tree.args);
				print(")");
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitTree(JCTree tree) {
		try {
			String simpleName = tree.getClass().getSimpleName();
			if ("JCTypeUnion".equals(simpleName)) {
				printExprs(readExpressionList(tree, "alternatives"), " | ");
				return;
			} else if ("JCTypeIntersection".equals(simpleName)) {
				printExprs(readExpressionList(tree, "bounds"), " & ");
				return;
			} else if ("JCLambda".equals(simpleName)) {
				visitLambda0(tree);
				return;
			} else if ("JCMemberReference".equals(simpleName)) {
				visitReference0(tree);
				return;
			} else if ("JCAnnotatedType".equals(simpleName)) {
				visitAnnotatedType0(tree);
			} else {
				print("(UNKNOWN[" + tree.getClass().getSimpleName() + "]: " + tree + ")");
				println();
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	private void visitLambda0(JCTree tree) {
		try {
			@SuppressWarnings("unchecked")
			List<JCVariableDecl> params = (List<JCVariableDecl>) readTreeList(tree, "params");
			boolean explicit = true;
			int paramLength = params.size();
			if (paramLength != 1) print("(");
			try {
				explicit = readObject(tree, "paramKind").toString().equals("EXPLICIT");
			} catch (Exception e) {}
			if (explicit) {
				printExprs(params);
			} else {
				String sep = "";
				for (JCVariableDecl param : params) {
					print(sep);
					print(param.name);
					sep = ", ";
				}
			}
			if (paramLength != 1) print(")");
			print(" -> ");
			printExpr(readTree(tree, "body"));
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	

	public void visitReference0(JCTree tree) {
		try {
			printExpr(readTree(tree, "expr"));
			print("::");
			List<JCExpression> typeArgs = readExpressionList(tree, "typeargs");
			if (typeArgs != null) {
				print("<");
				printExprs(typeArgs);
				print(">");
			}
			print(readObject(tree, "mode").toString().equals("INVOKE") ? readObject(tree, "name") : "new");
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	public void visitAnnotatedType0(JCTree tree) {
		try {
			JCTree underlyingType = readTree(tree, "underlyingType");
			if (underlyingType instanceof JCFieldAccess) {
				printExpr(((JCFieldAccess) underlyingType).selected);
				print(".");
				printExprs(readExpressionList(tree, "annotations"));
				print(" ");
				print(((JCFieldAccess) underlyingType).name);
			} else {
				printExprs(readExpressionList(tree, "annotations"));
				print(" ");
				printExpr(underlyingType);
			}
		} catch (IOException e) {
			throw new UncheckedIOException(e);
		}
	}
	
	private JCTree readTree(JCTree tree, String fieldName) {
		try {
			return (JCTree) readObject0(tree, fieldName);
		} catch (Exception e) {
			return null;
		}
	}
	
	@SuppressWarnings("unchecked")
	private List<? extends JCTree> readTreeList(JCTree tree, String fieldName) throws IOException {
		try {
			return (List<? extends JCTree>) readObject0(tree, fieldName);
		} catch (Exception e) {
			return List.nil();
		}
	}	
	
	@SuppressWarnings("unchecked")
	private List<JCExpression> readExpressionList(JCTree tree, String fieldName) throws IOException {
		try {
			return (List<JCExpression>) readObject0(tree, fieldName);
		} catch (Exception e) {
			return List.nil();
		}
	}
	
	@SuppressWarnings("unchecked")
	private List<JCExpression> readExpressionListOrNil(JCTree tree, String fieldName) throws IOException {
		try {
			return (List<JCExpression>) readObject0(tree, fieldName, List.nil());
		} catch (Exception e) {
			return List.nil();
		}
	}
	
	private Object readObject(JCTree tree, String fieldName) {
		try {
			return readObject0(tree, fieldName);
		} catch (Exception e) {
			return null;
		}
	}
	
	@SuppressWarnings("unchecked")
	private Object readObject0(JCTree tree, String fieldName) throws Exception {
		try {
			return tree.getClass().getDeclaredField(fieldName).get(tree);
		} catch (Exception e) {
			print("ERROR_READING_FIELD");
			throw e;
		}
	}
	
	@SuppressWarnings("unchecked")
	private Object readObject0(JCTree tree, String fieldName, Object defaultValue) throws Exception {
		try {
			return tree.getClass().getDeclaredField(fieldName).get(tree);
		} catch (Exception e) {
			return defaultValue;
		}
	}
}
