/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 *
 */
package org.hibernate.hql.ast;

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

import antlr.RecognitionException;
import antlr.collections.AST;
import org.hibernate.QueryException;
import org.hibernate.hql.ast.tree.FunctionNode;
import org.hibernate.type.Type;
import org.hibernate.util.StringHelper;
import org.hibernate.param.ParameterSpecification;
import org.hibernate.dialect.function.SQLFunction;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.hql.antlr.SqlGeneratorBase;
import org.hibernate.hql.antlr.SqlTokenTypes;
import org.hibernate.hql.ast.tree.FromElement;
import org.hibernate.hql.ast.tree.Node;
import org.hibernate.hql.ast.tree.ParameterNode;
import org.hibernate.hql.ast.tree.ParameterContainer;
import org.hibernate.hql.ast.util.ASTPrinter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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 Logger log = LoggerFactory.getLogger( SqlGenerator.class );
	private final boolean trace = log.isTraceEnabled();

	public static boolean REGRESSION_STYLE_CROSS_JOINS = false;

	/**
	 * 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 final ASTPrinter printer = new ASTPrinter( SqlTokenTypes.class );
	private List collectedParameters = new ArrayList();


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

	private int traceDepth = 0;

	public void traceIn(String ruleName, AST tree) {
		if (trace) {
			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() + " [" + printer.getTokenTypeName( tree.getType() ) + "]";
	}

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

	public List getCollectedParameters() {
		return collectedParameters;
	}

	protected void out(String s) {
		writer.clause( s );
	}

	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 ) );
				}
			}
		}
	}

	protected void commaBetweenParameters(String comma) {
		writer.commaBetweenParameters( comma );
	}

	public void reportError(RecognitionException e) {
		parseErrorHandler.reportError( e ); // Use the delegate.
	}

	public void reportError(String s) {
		parseErrorHandler.reportError( s ); // Use the delegate.
	}

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

	public ParseErrorHandler getParseErrorHandler() {
		return parseErrorHandler;
	}

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

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

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

	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 );
			writer = new FunctionArguments();
		}
	}

	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
			FunctionArguments functionArguments = ( FunctionArguments ) 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);

		/**
		 * todo remove this hack
		 * The parameter is either ", " or " , ". This is needed to pass sql generating tests as the old
		 * sql generator uses " , " in the WHERE and ", " in SELECT.
		 *
		 * @param comma either " , " or ", "
		 */
		void commaBetweenParameters(String comma);
	}

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

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

		public void commaBetweenParameters(String comma) {
			++argInd;
		}

		public List getArgs() {
			return args;
		}
	}

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

		public void commaBetweenParameters(String comma) {
			getStringBuffer().append( comma );
		}
	}

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

	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.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() );
		}
	}

	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() ) {
						out( ", " );
					}
					else {
						out( " " );
					}
				}
				else {
					// not so sure this is even valid subtree.  but if it was, it'd
					// represent two unrelated table references...
					out( ", " );
				}
			}
			out( d );
		}
	}

}
