// $Id: ASTPrinter.java,v 1.1 2005/07/12 20:27:17 steveebersole Exp $
package org.hibernate.hql.ast.util;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.hql.ast.tree.DisplayableNode;
import org.hibernate.util.StringHelper;

import antlr.collections.AST;

/**
 * An 'ASCII art' AST printer for debugging ANTLR grammars.
 *
 * @author Joshua Davis (pgmjsd@sourceforge.net)
 */
public class ASTPrinter {
	private Map tokenTypeNamesByTokenType;
	private Class tokenTypeConstants;
	private boolean showClassNames = true;

	/**
	 * Constructs an org.hibernate.hql.antlr.ASTPrinter, given the class that contains the token type
	 * constants (typically the '{grammar}TokenTypes' interface generated by
	 * ANTLR).
	 *
	 * @param tokenTypeConstants The class with token type constants in it.
	 */
	public ASTPrinter(Class tokenTypeConstants) {
		this.tokenTypeConstants = tokenTypeConstants;
	}

	/**
	 * Returns true if the node class names will be displayed.
	 *
	 * @return true if the node class names will be displayed.
	 */
	public boolean isShowClassNames() {
		return showClassNames;
	}

	/**
	 * Enables or disables AST node class name display.
	 *
	 * @param showClassNames true to enable class name display, false to disable
	 */
	public void setShowClassNames(boolean showClassNames) {
		this.showClassNames = showClassNames;
	}

	/**
	 * Prints the AST in 'ASCII art' tree form to the specified print stream.
	 *
	 * @param ast The AST to print.
	 * @param out The print stream.
	 */
	private void showAst(AST ast, PrintStream out) {
		showAst( ast, new PrintWriter( out ) );
	}

	/**
	 * Prints the AST in 'ASCII art' tree form to the specified print writer.
	 *
	 * @param ast The AST to print.
	 * @param pw  The print writer.
	 */
	public void showAst(AST ast, PrintWriter pw) {
		ArrayList parents = new ArrayList();
		showAst( parents, pw, ast );
		pw.flush();
	}

	/**
	 * Prints the AST in 'ASCII art' tree form into a string.
	 *
	 * @param ast    The AST to display.
	 * @param header The header for the display.
	 * @return The AST in 'ASCII art' form, as a string.
	 */
	public String showAsString(AST ast, String header) {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		PrintStream ps = new PrintStream( baos );
		ps.println( header );
		showAst( ast, ps );
		ps.flush();
		return new String( baos.toByteArray() );
	}

	/**
	 * Get a single token type name in the specified set of token type constants (interface).
	 *
	 * @param tokenTypeConstants Token type constants interface (e.g. HqlSqlTokenTypes.class).
	 * @param type               The token type ( typically from ast.getType() ).
	 * @return The token type name, *or* the integer value if the name could not be found for some reason.
	 */
	public static String getConstantName(Class tokenTypeConstants, int type) {
		String tokenTypeName = null;
		if ( tokenTypeConstants != null ) {
			Field[] fields = tokenTypeConstants.getFields();
			for ( int i = 0; i < fields.length; i++ ) {
				Field field = fields[i];
				tokenTypeName = getTokenTypeName( field, type, true );
				if ( tokenTypeName != null ) {
					break;	// Stop if found.
				}
			} // for
		} // if type constants were provided

		// Use the integer value if no token type name was found
		if ( tokenTypeName == null ) {
			tokenTypeName = Integer.toString( type );
		}

		return tokenTypeName;
	}

	private static String getTokenTypeName(Field field, int type, boolean checkType) {
		if ( Modifier.isStatic( field.getModifiers() ) ) {
			try {
				Object value = field.get( null );
				if ( !checkType ) {
					return field.getName();
				}
				else if ( value instanceof Integer ) {
					Integer integer = ( Integer ) value;
					if ( integer.intValue() == type ) {
						return field.getName();
					}
				} // if value is an integer
			} // try
			catch ( IllegalArgumentException ignore ) {
			}
			catch ( IllegalAccessException ignore ) {
			}
		} // if the field is static
		return null;
	}

	/**
	 * Returns the token type name for the given token type.
	 *
	 * @param type The token type.
	 * @return String - The token type name from the token type constant class,
	 *         or just the integer as a string if none exists.
	 */
	private String getTokenTypeName(int type) {
		// If the class with the constants in it was not supplied, just
		// use the integer token type as the token type name.
		if ( tokenTypeConstants == null ) {
			return Integer.toString( type );
		}

		// Otherwise, create a type id -> name map from the class if it
		// hasn't already been created.
		if ( tokenTypeNamesByTokenType == null ) {
			Field[] fields = tokenTypeConstants.getFields();
			tokenTypeNamesByTokenType = new HashMap();
			String tokenTypeName = null;
			for ( int i = 0; i < fields.length; i++ ) {
				Field field = fields[i];
				tokenTypeName = getTokenTypeName( field, type, false );
				if ( tokenTypeName != null ) {
					try {
						tokenTypeNamesByTokenType.put( field.get( null ), field.getName() );
					}
					catch ( IllegalAccessException ignore ) {
					}
				}
			} // for
		} // if the map hasn't been created.

		return ( String ) tokenTypeNamesByTokenType.get( new Integer( type ) );
	}

	private void showAst(ArrayList parents, PrintWriter pw, AST ast) {
		if ( ast == null ) {
			pw.println( "AST is null!" );
			return;
		}

		for ( int i = 0; i < parents.size(); i++ ) {
			AST parent = ( AST ) parents.get( i );
			if ( parent.getNextSibling() == null ) {

				pw.print( "   " );
			}
			else {
				pw.print( " | " );
			}
		}

		if ( ast.getNextSibling() == null ) {
			pw.print( " \\-" );
		}
		else {
			pw.print( " +-" );
		}

		showNode( pw, ast );

		ArrayList newParents = new ArrayList( parents );
		newParents.add( ast );
		for ( AST child = ast.getFirstChild(); child != null; child = child.getNextSibling() ) {
			showAst( newParents, pw, child );
		}
		newParents.clear();
	}

	private void showNode(PrintWriter pw, AST ast) {
		String s = nodeToString( ast, isShowClassNames() );
		pw.println( s );
	}

	public String nodeToString(AST ast, boolean showClassName) {
		if ( ast == null ) {
			return "{null}";
		}
		StringBuffer buf = new StringBuffer();
		buf.append( "[" ).append( getTokenTypeName( ast.getType() ) ).append( "] " );
		if ( showClassName ) {
			buf.append( StringHelper.unqualify( ast.getClass().getName() ) ).append( ": " );
		}

        buf.append( "'" );
        String text = ast.getText();
        appendEscapedMultibyteChars(text, buf);
        buf.append( "'" );
		if ( ast instanceof DisplayableNode ) {
			DisplayableNode displayableNode = ( DisplayableNode ) ast;
			// Add a space before the display text.
			buf.append( " " ).append( displayableNode.getDisplayText() );
		}
		String s = buf.toString();
		return s;
	}

    public static void appendEscapedMultibyteChars(String text, StringBuffer buf) {
        char[] chars = text.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char aChar = chars[i];
            if (aChar > 256) {
                buf.append("\\u");
                buf.append(Integer.toHexString(aChar));
            }
            else
                buf.append(aChar);
        }
    }

    public static String escapeMultibyteChars(String text)
    {
        StringBuffer buf = new StringBuffer();
        appendEscapedMultibyteChars(text,buf);
        return buf.toString();
    }
}
