//$Id: WhereParser.java,v 1.21 2004/06/19 18:45:02 oneovthafew Exp $
package net.sf.hibernate.hql;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import net.sf.hibernate.MappingException;
import net.sf.hibernate.QueryException;
import net.sf.hibernate.util.ReflectHelper;
import net.sf.hibernate.util.StringHelper;
import net.sf.hibernate.persister.Queryable;
import net.sf.hibernate.sql.InFragment;
import net.sf.hibernate.sql.JoinFragment;
import net.sf.hibernate.type.EntityType;
import net.sf.hibernate.type.LiteralType;
import net.sf.hibernate.type.Type;
import net.sf.hibernate.type.TypeFactory;

/**
 * Parses the where clause of a hibernate query and translates it to an
 * SQL where clause.
 */

// We should reengineer this class so that, rather than the current ad -
// hoc linear approach to processing a stream of tokens, we instead
// build up a tree of expressions.

// We would probably refactor to have LogicParser (builds a tree of simple
// expressions connected by and, or, not), ExpressionParser (translates
// from OO terms like foo, foo.Bar, foo.Bar.Baz to SQL terms like
// FOOS.ID, FOOS.BAR_ID, etc) and PathExpressionParser (which does much
// the same thing it does now)

public class WhereParser implements Parser {
	
	private final PathExpressionParser pathExpressionParser;
	{
		pathExpressionParser = new PathExpressionParser();
		pathExpressionParser.setUseThetaStyleJoin(true); //Need this, since join condition can appear inside parens!
	}
	
	private static final Set EXPRESSION_TERMINATORS = new HashSet();   //tokens that close a sub expression
	private static final Set EXPRESSION_OPENERS = new HashSet();       //tokens that open a sub expression
	private static final Set BOOLEAN_OPERATORS = new HashSet();        //tokens that would indicate a sub expression is a boolean expression
	private static final Map NEGATIONS = new HashMap();
	
	static {
		EXPRESSION_TERMINATORS.add("and");
		EXPRESSION_TERMINATORS.add("or");
		EXPRESSION_TERMINATORS.add(StringHelper.CLOSE_PAREN);
		//expressionTerminators.add(","); // deliberately excluded
		
		EXPRESSION_OPENERS.add("and");
		EXPRESSION_OPENERS.add("or");
		EXPRESSION_OPENERS.add(StringHelper.OPEN_PAREN);
		//expressionOpeners.add(","); // deliberately excluded
		
		BOOLEAN_OPERATORS.add("<");
		BOOLEAN_OPERATORS.add("=");
		BOOLEAN_OPERATORS.add(">");
		BOOLEAN_OPERATORS.add("#");
		BOOLEAN_OPERATORS.add("~");
		BOOLEAN_OPERATORS.add("like");
		BOOLEAN_OPERATORS.add("ilike");
		BOOLEAN_OPERATORS.add("is");
		BOOLEAN_OPERATORS.add("in");
		BOOLEAN_OPERATORS.add("any");
		BOOLEAN_OPERATORS.add("some");
		BOOLEAN_OPERATORS.add("all");
		BOOLEAN_OPERATORS.add("exists");
		BOOLEAN_OPERATORS.add("between");
		BOOLEAN_OPERATORS.add("<=");
		BOOLEAN_OPERATORS.add(">=");
		BOOLEAN_OPERATORS.add("=>");
		BOOLEAN_OPERATORS.add("=<");
		BOOLEAN_OPERATORS.add("!=");
		BOOLEAN_OPERATORS.add("<>");
		BOOLEAN_OPERATORS.add("!#");
		BOOLEAN_OPERATORS.add("!~");
		BOOLEAN_OPERATORS.add("!<");
		BOOLEAN_OPERATORS.add("!>");
		BOOLEAN_OPERATORS.add("is not");
		BOOLEAN_OPERATORS.add("not like");
		BOOLEAN_OPERATORS.add("not ilike");
		BOOLEAN_OPERATORS.add("not in");
		BOOLEAN_OPERATORS.add("not between");
		BOOLEAN_OPERATORS.add("not exists");
		
		NEGATIONS.put("and", "or");
		NEGATIONS.put("or", "and");
		NEGATIONS.put("<", ">=");
		NEGATIONS.put("=", "<>");
		NEGATIONS.put(">", "<=");
		NEGATIONS.put("#", "!#");
		NEGATIONS.put("~", "!~");
		NEGATIONS.put("like", "not like");
		NEGATIONS.put("ilike", "not ilike");
		NEGATIONS.put("is", "is not");
		NEGATIONS.put("in", "not in");
		NEGATIONS.put("exists", "not exists");
		NEGATIONS.put("between", "not between");
		NEGATIONS.put("<=", ">");
		NEGATIONS.put(">=", "<");
		NEGATIONS.put("=>", "<");
		NEGATIONS.put("=<", ">");
		NEGATIONS.put("!=", "=");
		NEGATIONS.put("<>", "=");
		NEGATIONS.put("!#", "#");
		NEGATIONS.put("!~", "~");
		NEGATIONS.put("!<", "<");
		NEGATIONS.put("!>", ">");
		NEGATIONS.put("is not", "is");
		NEGATIONS.put("not like", "like");
		NEGATIONS.put("not ilike", "ilike");
		NEGATIONS.put("not in", "in");
		NEGATIONS.put("not between", "between");
		NEGATIONS.put("not exists", "exists");
		
	}
	// Handles things like:
	// a and b or c
	// a and ( b or c )
	// not a and not b
	// not ( a and b )
	// x between y and z            (overloaded "and")
	// x in ( a, b, c )             (overloaded brackets)
	// not not a
	// a is not null                (overloaded "not")
	// etc......
	// and expressions like
	// foo = bar                    (maps to: foo.id = bar.id)
	// foo.Bar = 'foo'              (maps to: foo.bar = 'foo')
	// foo.Bar.Baz = 1.0            (maps to: foo.bar = bar.id and bar.baz = 1.0)
	// 1.0 = foo.Bar.Baz            (maps to: bar.baz = 1.0 and foo.Bar = bar.id)
	// foo.Bar.Baz = a.B.C          (maps to: bar.Baz = b.C and foo.Bar = bar.id and a.B = b.id)
	// foo.Bar.Baz + a.B.C          (maps to: bar.Baz + b.C and foo.Bar = bar.id and a.B = b.id)
	// ( foo.Bar.Baz + 1.0 ) < 2.0  (maps to: ( bar.Baz + 1.0 ) < 2.0 and foo.Bar = bar.id)
	
	private boolean betweenSpecialCase = false;       //Inside a BETWEEN ... AND ... expression
	private boolean negated = false;
	
	private boolean inSubselect = false;
	private int bracketsSinceSelect = 0;
	private StringBuffer subselect;
	
	private boolean expectingPathContinuation = false;
	private int expectingIndex = 0;
	
	// The following variables are stacks that keep information about each subexpression
	// in the list of nested subexpressions we are currently processing.
	
	private LinkedList nots = new LinkedList();           //were an odd or even number of NOTs encountered
	private LinkedList joins = new LinkedList();          //the join string built up by compound paths inside this expression
	private LinkedList booleanTests = new LinkedList();   //a flag indicating if the subexpression is known to be boolean
	
	private String getElementName(PathExpressionParser.CollectionElement element, QueryTranslator q) throws QueryException {
		String name;
		if ( element.isOneToMany ) {
			name = element.alias;
		}
		else {
			Type type = element.elementType;
			Class clazz;
			if ( type.isEntityType() ) { //ie. a many-to-many
				clazz = ( (EntityType) type ).getAssociatedClass();
				name = pathExpressionParser.continueFromManyToMany(clazz, element.elementColumns, q);
			}
			else {
				throw new QueryException("illegally dereferenced collection element");
			}
		}
		return name;
	}
	
	public void token(String token, QueryTranslator q) throws QueryException {
		
		String lcToken = token.toLowerCase();
		
		//Cope with [,]
		if ( token.equals("[") && !expectingPathContinuation ) {
			expectingPathContinuation = false;
			if (expectingIndex==0) throw new QueryException("unexpected [");
			return;
		}
		else if ( token.equals("]") ) {
			expectingIndex--;
			expectingPathContinuation = true;
			return;
		}
		
		//Cope with a continued path expression (ie. ].baz)
		if (expectingPathContinuation) {
			boolean pathExpressionContinuesFurther = continuePathExpression(token, q);
			if (pathExpressionContinuesFurther) return; //NOTE: early return
		}
		
		//Cope with a subselect
		if ( !inSubselect && ( lcToken.equals("select") || lcToken.equals("from") ) ) {
			inSubselect=true;
			subselect = new StringBuffer(20);
		}
		if ( inSubselect && token.equals(StringHelper.CLOSE_PAREN) ) {
			bracketsSinceSelect--;
			
			if (bracketsSinceSelect==-1) {
				QueryTranslator subq = new QueryTranslator( subselect.toString() );
				try {
					subq.compile(q);
				}
				catch (MappingException me) {
					throw new QueryException("MappingException occurred compiling subquery", me );
				}
				appendToken( q, subq.getSQLString() );
				inSubselect = false;
				bracketsSinceSelect=0;
			}
		}
		if (inSubselect) {
			if ( token.equals(StringHelper.OPEN_PAREN) ) bracketsSinceSelect++;
			subselect.append(token).append(' ');
			return;
		}
		
		//Cope with special cases of AND, NOT, ()
		specialCasesBefore(lcToken);
		
		//Close extra brackets we opened
		if ( !betweenSpecialCase && EXPRESSION_TERMINATORS.contains(lcToken) ) {
			closeExpression(q, lcToken);
		}
		
		//take note when this is a boolean expression
		if ( BOOLEAN_OPERATORS.contains(lcToken) ) {
			booleanTests.removeLast();
			booleanTests.addLast(Boolean.TRUE);
		}
		
		if ( lcToken.equals("not") ) {
			nots.addLast(
				new Boolean( !( (Boolean) nots.removeLast() ).booleanValue() )
			);
			negated = !negated;
			return; //NOTE: early return
		}
		
		//process a token, mapping OO path expressions to SQL expressions
		doToken(token, q);
		
		//Open any extra brackets we might need.
		if ( !betweenSpecialCase && EXPRESSION_OPENERS.contains(lcToken) ) {
			openExpression(q, lcToken);
		}
		
		//Cope with special cases of AND, NOT, )
		specialCasesAfter(lcToken);
		
	}
	
	public void start(QueryTranslator q) throws QueryException {
		token(StringHelper.OPEN_PAREN, q);
	}
	
	public void end(QueryTranslator q) throws QueryException {
		if (expectingPathContinuation) {
			expectingPathContinuation=false;
			PathExpressionParser.CollectionElement element = pathExpressionParser.lastCollectionElement();
			if ( element.elementColumns.length!=1 ) throw new QueryException("path expression ended in composite collection element");
			appendToken( q, element.elementColumns[0] );
			addToCurrentJoin(element);
		}
		token(StringHelper.CLOSE_PAREN, q);
	}
	
	private void closeExpression(QueryTranslator q, String lcToken) {
		if ( ( (Boolean) booleanTests.removeLast() ).booleanValue() ) { //it was a boolean expression
			
			if ( booleanTests.size()>0 ) {
				// the next one up must also be
				booleanTests.removeLast();
				booleanTests.addLast(Boolean.TRUE);
			}
			
			// Add any joins
			appendToken(q, ( joins.removeLast() ).toString() );
			
		}
		else {
			StringBuffer join = (StringBuffer) joins.removeLast();
			( (StringBuffer) joins.getLast() ).append( join.toString() );
		}
		
		if ( ( (Boolean) nots.removeLast() ).booleanValue() ) negated = !negated;
		
		if ( !StringHelper.CLOSE_PAREN.equals(lcToken) ) appendToken(q, StringHelper.CLOSE_PAREN);
	}
	
	private void openExpression(QueryTranslator q, String lcToken) {
		nots.addLast(Boolean.FALSE);
		booleanTests.addLast(Boolean.FALSE);
		joins.addLast( new StringBuffer() );
		if ( !StringHelper.OPEN_PAREN.equals(lcToken) ) appendToken(q, StringHelper.OPEN_PAREN);
	}
		
	private void preprocess(String token, QueryTranslator q) throws QueryException {
		// ugly hack for cases like "foo.bar.collection.elements" 
		// (multi-part path expression ending in elements or indices)
		String[] tokens = StringHelper.split(".", token, true);
		if ( 
			tokens.length>5 &&
			( "elements".equals( tokens[tokens.length-1] ) || "indices".equals( tokens[tokens.length-1] ) )
		) {
			pathExpressionParser.start(q);
			for( int i=0; i<tokens.length-3; i++ ) {
				pathExpressionParser.token( tokens[i], q);
			}
			pathExpressionParser.token(null, q);
			pathExpressionParser.end(q);
			addJoin( pathExpressionParser.getWhereJoin(), q );
			pathExpressionParser.ignoreInitialJoin();
		}
	}
	
	private void doPathExpression(String token, QueryTranslator q) throws QueryException {
		
		preprocess(token, q);
		
		StringTokenizer tokens = new StringTokenizer(token, ".", true);
		pathExpressionParser.start(q);
		while( tokens.hasMoreTokens() ) {
			pathExpressionParser.token( tokens.nextToken(), q);
		}
		pathExpressionParser.end(q);
		if ( pathExpressionParser.isCollectionValued() ) {
			openExpression(q, StringHelper.EMPTY_STRING);
			appendToken( q, pathExpressionParser.getCollectionSubquery() );
			closeExpression(q, StringHelper.EMPTY_STRING);
			// this is ugly here, but needed because its a subquery
			q.addQuerySpace( 
				q.getCollectionPersister( pathExpressionParser.getCollectionRole() ).getCollectionSpace() 
			);
		}
		else {
			if ( pathExpressionParser.isExpectingCollectionIndex() ) {
				expectingIndex++;
			}
			else {
				addJoin( pathExpressionParser.getWhereJoin(), q );
				appendToken( q, pathExpressionParser.getWhereColumn() );
			}
		}
	}
	
	private void addJoin(JoinFragment ojf, QueryTranslator q) {
		JoinFragment fromClause = q.createJoinFragment(true);
		fromClause.addJoins( ojf.toFromFragmentString(), StringHelper.EMPTY_STRING );
		q.addJoin( pathExpressionParser.getName(), fromClause );
		addToCurrentJoin( ojf.toWhereFragmentString() );
	}
	
	private void doToken(String token, QueryTranslator q) throws QueryException {
		if ( q.isName( StringHelper.root(token) ) ) { //path expression
			doPathExpression( q.unalias(token), q);
		}
		else if ( token.startsWith(ParserHelper.HQL_VARIABLE_PREFIX) ) { //named query parameter
			q.addNamedParameter( token.substring(1) );
			appendToken(q, "?");
		}
		else {
			Queryable persister = q.getPersisterUsingImports(token);
			if ( persister!=null ) { // the name of a class
				final Object discrim = persister.getDiscriminatorSQLValue();
				if ( InFragment.NULL==discrim || InFragment.NOT_NULL==discrim ) {
					throw new QueryException("subclass test not allowed for null or not null discriminator");
				}
				appendToken( q, discrim.toString() );
			}
			else {
				Object constant;
				if (
					token.indexOf(StringHelper.DOT)>-1 &&
					( constant=ReflectHelper.getConstantValue(token) )!=null
				) {
					Type type;
					try {
						type = TypeFactory.heuristicType( constant.getClass().getName() );
					}
					catch (MappingException me) {
						throw new QueryException(me);
					}
					if (type==null) throw new QueryException("Could not determine type of: " + token);
					try {
						appendToken(q, ( (LiteralType) type ).objectToSQLString(constant) );
					}
					catch (Exception e) {
						throw new QueryException("Could not format constant value to SQL literal: " + token, e);
					}
				}
				else { //anything else
					
					String negatedToken = negated ? (String) NEGATIONS.get( token.toLowerCase() ) : null;
					if ( negatedToken!=null && ( !betweenSpecialCase || !"or".equals(negatedToken) ) ) {
						appendToken(q, negatedToken);
					}
					else {
						appendToken(q, token);
					}
				}
			}
		}
	}
	
	private void addToCurrentJoin(String sql) {
		( (StringBuffer) joins.getLast() ).append(sql);
	}
	
	private void addToCurrentJoin(PathExpressionParser.CollectionElement ce) {
		addToCurrentJoin( ce.joinFragment.toWhereFragmentString() + ce.indexValue.toString() );
	}
	
	private void specialCasesBefore(String lcToken) {
		if ( lcToken.equals("between") || lcToken.equals("not between") ) {
			betweenSpecialCase = true;
		}
	}
	
	private void specialCasesAfter(String lcToken) {
		if ( betweenSpecialCase && lcToken.equals("and") ) {
			betweenSpecialCase = false;
		}
	}
	
	void appendToken(QueryTranslator q, String token) {
		if (expectingIndex>0) {
			pathExpressionParser.setLastCollectionElementIndexValue(token);
		}
		else {
			q.appendWhereToken(token);
		}
	}
	
	private boolean continuePathExpression(String token, QueryTranslator q) throws QueryException {
		
		expectingPathContinuation = false;
			
		PathExpressionParser.CollectionElement element = pathExpressionParser.lastCollectionElement();
			
		if ( token.startsWith(".") ) { // the path expression continues after a ]
				
			doPathExpression( getElementName(element, q) + token, q ); // careful with this!
				
			addToCurrentJoin(element);
			return true; //NOTE: EARLY EXIT!
				
		}

		else { // the path expression ends at the ]
			if ( element.elementColumns.length!=1 ) {
				throw new QueryException("path expression ended in composite collection element");
			}
			appendToken( q, element.elementColumns[0] );
			addToCurrentJoin(element);
			return false;
		}
	}
}
