/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.hql.internal.ast;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import org.hibernate.NullPrecedence;
import org.hibernate.QueryException;
import org.hibernate.dialect.function.SQLFunction;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.internal.antlr.SqlGeneratorBase;
import org.hibernate.hql.internal.antlr.SqlTokenTypes;
import org.hibernate.hql.internal.ast.tree.CollectionPathNode;
import org.hibernate.hql.internal.ast.tree.CollectionSizeNode;
import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.FunctionNode;
import org.hibernate.hql.internal.ast.tree.Node;
import org.hibernate.hql.internal.ast.tree.ParameterContainer;
import org.hibernate.hql.internal.ast.tree.ParameterNode;
import org.hibernate.hql.internal.ast.util.ASTPrinter;
import org.hibernate.hql.internal.ast.util.TokenPrinters;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.param.ParameterSpecification;
import org.hibernate.type.Type;

import antlr.RecognitionException;
import antlr.SemanticException;
import antlr.collections.AST;

/**
 * Generates SQL by overriding callback methods in the base class, which does
 * the actual SQL AST walking.
 *
 * @author Joshua Davis
 * @author Steve Ebersole
 */
public class SqlGenerator extends SqlGeneratorBase implements ErrorReporter {
	private static final CoreMessageLogger LOG = CoreLogging.messageLogger( SqlGenerator.class );

	public static boolean REGRESSION_STYLE_CROSS_JOINS;

	/**
	 * all append invocations on the buf should go through this Output instance variable.
	 * The value of this variable may be temporarily substituted by sql function processing code
	 * to catch generated arguments.
	 * This is because sql function templates need arguments as separate string chunks
	 * that will be assembled into the target dialect-specific function call.
	 */
	private SqlWriter writer = new DefaultWriter();

	private ParseErrorHandler parseErrorHandler;
	private SessionFactoryImplementor sessionFactory;
	private LinkedList<SqlWriter> outputStack = new LinkedList<SqlWriter>();
	private List<ParameterSpecification> collectedParameters = new ArrayList<ParameterSpecification>();


	// handle trace logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	private int traceDepth;

	@Override
	public void traceIn(String ruleName, AST tree) {
		if ( !LOG.isTraceEnabled() ) {
			return;
		}
		if ( inputState.guessing > 0 ) {
			return;
		}
		String prefix = StringHelper.repeat( '-', ( traceDepth++ * 2 ) ) + "-> ";
		String traceText = ruleName + " (" + buildTraceNodeName( tree ) + ")";
		LOG.trace( prefix + traceText );
	}

	private String buildTraceNodeName(AST tree) {
		return tree == null
				? "???"
				: tree.getText() + " [" + TokenPrinters.SQL_TOKEN_PRINTER.getTokenTypeName( tree.getType() ) + "]";
	}

	@Override
	public void traceOut(String ruleName, AST tree) {
		if ( !LOG.isTraceEnabled() ) {
			return;
		}
		if ( inputState.guessing > 0 ) {
			return;
		}
		String prefix = "<-" + StringHelper.repeat( '-', ( --traceDepth * 2 ) ) + " ";
		LOG.trace( prefix + ruleName );
	}

	public List<ParameterSpecification> getCollectedParameters() {
		return collectedParameters;
	}

	@Override
	protected void out(String s) {
		if ( exprs.size() > 1 ) {
			super.out( s );
		}
		else {
			writer.clause( s );
		}
	}

	@Override
	protected void out(AST n) {
		if ( n instanceof Node ) {
			out( ( (Node) n ).getRenderText( sessionFactory ) );
		}
		else {
			super.out( n );
		}

		if ( n instanceof ParameterNode ) {
			collectedParameters.add( ( (ParameterNode) n ).getHqlParameterSpecification() );
		}
		else if ( n instanceof ParameterContainer ) {
			if ( ( (ParameterContainer) n ).hasEmbeddedParameters() ) {
				ParameterSpecification[] specifications = ( (ParameterContainer) n ).getEmbeddedParameters();
				if ( specifications != null ) {
					collectedParameters.addAll( Arrays.asList( specifications ) );
				}
			}
		}
	}

	@Override
	protected void betweenFunctionArguments() {
		writer.betweenFunctionArguments();
	}

	@Override
	public void reportError(RecognitionException e) {
		parseErrorHandler.reportError( e );
	}

	@Override
	public void reportError(String s) {
		parseErrorHandler.reportError( s );
	}

	@Override
	public void reportWarning(String s) {
		parseErrorHandler.reportWarning( s );
	}

	public ParseErrorHandler getParseErrorHandler() {
		return parseErrorHandler;
	}

	public SqlGenerator(SessionFactoryImplementor sfi) {
		super();
		parseErrorHandler = new ErrorTracker();
		sessionFactory = sfi;
	}

	public String getSQL() {
		return getStringBuilder().toString();
	}

	@Override
	protected void optionalSpace() {
		int c = getLastChar();
		switch ( c ) {
			case -1:
				return;
			case ' ':
				return;
			case ')':
				return;
			case '(':
				return;
			default:
				out( " " );
		}
	}

	@Override
	protected void beginFunctionTemplate(AST node, AST nameNode) {
		// NOTE for AGGREGATE both nodes are the same; for METHOD the first is the METHOD, the second is the
		// 		METHOD_NAME
		FunctionNode functionNode = (FunctionNode) node;
		SQLFunction sqlFunction = functionNode.getSQLFunction();
		if ( sqlFunction == null ) {
			// if SQLFunction is null we just write the function out as it appears in the hql statement
			super.beginFunctionTemplate( node, nameNode );
		}
		else {
			// this function has a registered SQLFunction -> redirect output and catch the arguments
			outputStack.addFirst( writer );
			if ( node.getType() == CAST ) {
				writer = new CastFunctionArguments();
			}
			else {
				writer = new StandardFunctionArguments();
			}
		}
	}

	@Override
	protected void endFunctionTemplate(AST node) {
		FunctionNode functionNode = (FunctionNode) node;
		SQLFunction sqlFunction = functionNode.getSQLFunction();
		if ( sqlFunction == null ) {
			super.endFunctionTemplate( node );
		}
		else {
			final Type functionType = functionNode.getFirstArgumentType();
			// this function has a registered SQLFunction -> redirect output and catch the arguments
			FunctionArgumentsCollectingWriter functionArguments = (FunctionArgumentsCollectingWriter) writer;
			writer = outputStack.removeFirst();
			out( sqlFunction.render( functionType, functionArguments.getArgs(), sessionFactory ) );
		}
	}

	// --- Inner classes (moved here from sql-gen.g) ---

	/**
	 * Writes SQL fragments.
	 */
	interface SqlWriter {
		void clause(String clause);

		void betweenFunctionArguments();
	}

	interface FunctionArgumentsCollectingWriter extends SqlWriter {
		public List getArgs();
	}

	/**
	 * SQL function processing code redirects generated SQL output to an instance of this class
	 * which catches function arguments.
	 */
	static class StandardFunctionArguments implements FunctionArgumentsCollectingWriter {
		private int argInd;
		private final List<String> args = new ArrayList<String>( 3 );

		@Override
		public void clause(String clause) {
			if ( argInd == args.size() ) {
				args.add( clause );
			}
			else {
				args.set( argInd, args.get( argInd ) + clause );
			}
		}

		@Override
		public void betweenFunctionArguments() {
			++argInd;
		}

		public List getArgs() {
			return args;
		}
	}

	/**
	 * SQL function processing code redirects generated SQL output to an instance of this class
	 * which catches function arguments.
	 */
	static class CastFunctionArguments implements FunctionArgumentsCollectingWriter {
		private String castExpression;
		private String castTargetType;

		private boolean startedType;

		@Override
		public void clause(String clause) {
			if ( startedType ) {
				if ( castTargetType == null ) {
					castTargetType = clause;
				}
				else {
					castTargetType += clause;
				}
			}
			else {
				if ( castExpression == null ) {
					castExpression = clause;
				}
				else {
					castExpression += clause;
				}
			}
		}

		@Override
		public void betweenFunctionArguments() {
			if ( startedType ) {
				throw new QueryException( "CAST function should only have 2 arguments" );
			}
			startedType = true;
		}

		public List getArgs() {
			List<String> rtn = CollectionHelper.arrayList( 2 );
			rtn.add( castExpression );
			rtn.add( castTargetType );
			return rtn;
		}
	}

	/**
	 * The default SQL writer.
	 */
	class DefaultWriter implements SqlWriter {
		@Override
		public void clause(String clause) {
			getStringBuilder().append( clause );
		}

		@Override
		public void betweenFunctionArguments() {
			getStringBuilder().append( ", " );
		}
	}

	public static void panic() {
		throw new QueryException( "TreeWalker: panic" );
	}

	@Override
	protected void fromFragmentSeparator(AST a) {
		// check two "adjecent" nodes at the top of the from-clause tree
		AST next = a.getNextSibling();
		if ( next == null || !hasText( a ) ) {
			return;
		}

		FromElement left = (FromElement) a;
		FromElement right = (FromElement) next;

		///////////////////////////////////////////////////////////////////////
		// HACK ALERT !!!!!!!!!!!!!!!!!!!!!!!!!!!!
		// Attempt to work around "ghost" ImpliedFromElements that occasionally
		// show up between the actual things being joined.  This consistently
		// occurs from index nodes (at least against many-to-many).  Not sure
		// if there are other conditions
		//
		// Essentially, look-ahead to the next FromElement that actually
		// writes something to the SQL
		while ( right != null && !hasText( right ) ) {
			right = (FromElement) right.getNextSibling();
		}
		if ( right == null ) {
			return;
		}
		///////////////////////////////////////////////////////////////////////

		if ( !hasText( right ) ) {
			return;
		}

		if ( right.getType() == ENTITY_JOIN ) {
			out( " " );
		}
		else if ( right.getRealOrigin() == left ||
				( right.getRealOrigin() != null && right.getRealOrigin() == left.getRealOrigin() ) ) {
			// right represents a joins originating from left; or
			// both right and left reprersent joins originating from the same FromElement
			if ( right.getJoinSequence() != null && right.getJoinSequence().isThetaStyle() ) {
				writeCrossJoinSeparator();
			}
			else {
				out( " " );
			}
		}
		else {
			// these are just two unrelated table references
			writeCrossJoinSeparator();
		}
	}

	private void writeCrossJoinSeparator() {
		if ( REGRESSION_STYLE_CROSS_JOINS ) {
			out( ", " );
		}
		else {
			out( sessionFactory.getDialect().getCrossJoinSeparator() );
		}
	}

	@Override
	protected void nestedFromFragment(AST d, AST parent) {
		// check a set of parent/child nodes in the from-clause tree
		// to determine if a comma is required between them
		if ( d != null && hasText( d ) ) {
			if ( parent != null && hasText( parent ) ) {
				// again, both should be FromElements
				FromElement left = (FromElement) parent;
				FromElement right = (FromElement) d;
				if ( right.getRealOrigin() == left ) {
					// right represents a joins originating from left...
					if ( right.getJoinSequence() != null && right.getJoinSequence().isThetaStyle() ) {
						writeCrossJoinSeparator();
					}
					else {
						out( " " );
					}
				}
				else {
					// not so sure this is even valid subtree.  but if it was, it'd
					// represent two unrelated table references...
					writeCrossJoinSeparator();
				}
			}
			out( d );
		}
	}

	@Override
	protected String renderOrderByElement(String expression, String order, String nulls) {
		final NullPrecedence nullPrecedence = NullPrecedence.parse(
				nulls,
				sessionFactory.getSettings().getDefaultNullPrecedence()
		);
		return sessionFactory.getDialect().renderOrderByElement( expression, null, order, nullPrecedence );
	}

	@Override
	protected void renderCollectionSize(AST ast) {
		assert ast instanceof CollectionSizeNode;

		final CollectionSizeNode collectionSizeNode = (CollectionSizeNode) ast;

		try {
			writer.clause( collectionSizeNode.toSqlExpression() );
		}
		catch (QueryException qe) {
			throw qe;
		}
		catch (Exception e) {
			throw new QueryException( "Unable to render collection-size node" );
		}
	}
}
