/*
 * 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.tree;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.hql.antlr.HqlSqlTokenTypes;
import org.hibernate.hql.ast.util.ASTIterator;
import org.hibernate.hql.ast.util.ASTUtil;
import org.hibernate.hql.ast.util.DisplayableNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * Represents the 'FROM' part of a query or subquery, containing all mapped class references.
 *
 * @author josh
 */
public class FromClause extends HqlSqlWalkerNode implements HqlSqlTokenTypes, DisplayableNode {
	private static Logger log = LoggerFactory.getLogger( FromClause.class );
	public static final int ROOT_LEVEL = 1;

	private int level = ROOT_LEVEL;
	private Set fromElements = new HashSet();
	private Map fromElementByClassAlias = new HashMap();
	private Map fromElementByTableAlias = new HashMap();
	private Map fromElementsByPath = new HashMap();

	/**
	 * All of the implicit FROM xxx JOIN yyy elements that are the destination of a collection.  These are created from
	 * index operators on collection property references.
	 */
	private Map collectionJoinFromElementsByPath = new HashMap();
	/**
	 * Pointer to the parent FROM clause, if there is one.
	 */
	private FromClause parentFromClause;
	/**
	 * Collection of FROM clauses of which this is the parent.
	 */
	private Set childFromClauses;
	/**
	 * Counts the from elements as they are added.
	 */
	private int fromElementCounter = 0;
	/**
	 * Implied FROM elements to add onto the end of the FROM clause.
	 */
	private List impliedElements = new LinkedList();

	/**
	 * Adds a new from element to the from node.
	 *
	 * @param path  The reference to the class.
	 * @param alias The alias AST.
	 * @return FromElement - The new FROM element.
	 */
	public FromElement addFromElement(String path, AST alias) throws SemanticException {
		// The path may be a reference to an alias defined in the parent query.
		String classAlias = ( alias == null ) ? null : alias.getText();
		checkForDuplicateClassAlias( classAlias );
		FromElementFactory factory = new FromElementFactory( this, null, path, classAlias, null, false );
		return factory.addFromElement();
	}

	void registerFromElement(FromElement element) {
		fromElements.add( element );
		String classAlias = element.getClassAlias();
		if ( classAlias != null ) {
			// The HQL class alias refers to the class name.
			fromElementByClassAlias.put( classAlias, element );
		}
		// Associate the table alias with the element.
		String tableAlias = element.getTableAlias();
		if ( tableAlias != null ) {
			fromElementByTableAlias.put( tableAlias, element );
		}
	}

	void addDuplicateAlias(String alias, FromElement element) {
		if ( alias != null ) {
			fromElementByClassAlias.put( alias, element );
		}
	}

	private void checkForDuplicateClassAlias(String classAlias) throws SemanticException {
		if ( classAlias != null && fromElementByClassAlias.containsKey( classAlias ) ) {
			throw new SemanticException( "Duplicate definition of alias '" + classAlias + "'" );
		}
	}

	/**
	 * Retreives the from-element represented by the given alias.
	 *
	 * @param aliasOrClassName The alias by which to locate the from-element.
	 * @return The from-element assigned the given alias, or null if none.
	 */
	public FromElement getFromElement(String aliasOrClassName) {
		FromElement fromElement = ( FromElement ) fromElementByClassAlias.get( aliasOrClassName );
		if ( fromElement == null && getSessionFactoryHelper().isStrictJPAQLComplianceEnabled() ) {
			fromElement = findIntendedAliasedFromElementBasedOnCrazyJPARequirements( aliasOrClassName );
		}
		if ( fromElement == null && parentFromClause != null ) {
			fromElement = parentFromClause.getFromElement( aliasOrClassName );
		}
		return fromElement;
	}

	private FromElement findIntendedAliasedFromElementBasedOnCrazyJPARequirements(String specifiedAlias) {
		Iterator itr = fromElementByClassAlias.entrySet().iterator();
		while ( itr.hasNext() ) {
			Map.Entry entry = ( Map.Entry ) itr.next();
			String alias = ( String ) entry.getKey();
			if ( alias.equalsIgnoreCase( specifiedAlias ) ) {
				return ( FromElement ) entry.getValue();
			}
		}
		return null;
	}

	/**
	 * Convenience method to check whether a given token represents a from-element alias.
	 *
	 * @param possibleAlias The potential from-element alias to check.
	 * @return True if the possibleAlias is an alias to a from-element visible
	 *         from this point in the query graph.
	 */
	public boolean isFromElementAlias(String possibleAlias) {
		boolean isAlias = containsClassAlias( possibleAlias );
		if ( !isAlias && parentFromClause != null ) {
			// try the parent FromClause...
			isAlias = parentFromClause.isFromElementAlias( possibleAlias );
		}
		return isAlias;
	}

	/**
	 * Returns the list of from elements in order.
	 *
	 * @return the list of from elements (instances of FromElement).
	 */
	public List getFromElements() {
		return ASTUtil.collectChildren( this, fromElementPredicate );
	}
	
	public FromElement getFromElement() {
		// TODO: not sure about this one
//		List fromElements = getFromElements();
//		if ( fromElements == null || fromElements.isEmpty() ) {
//			throw new QueryException( "Unable to locate from element" );
//		}
		return (FromElement) getFromElements().get(0);
	}

	/**
	 * Returns the list of from elements that will be part of the result set.
	 *
	 * @return the list of from elements that will be part of the result set.
	 */
	public List getProjectionList() {
		return ASTUtil.collectChildren( this, projectionListPredicate );
	}

	public List getCollectionFetches() {
		return ASTUtil.collectChildren( this, collectionFetchPredicate );
	}

	public boolean hasCollectionFecthes() {
		return getCollectionFetches().size() > 0;
	}

	public List getExplicitFromElements() {
		return ASTUtil.collectChildren( this, explicitFromPredicate );
	}

	private static ASTUtil.FilterPredicate fromElementPredicate = new ASTUtil.IncludePredicate() {
		public boolean include(AST node) {
			FromElement fromElement = ( FromElement ) node;
			return fromElement.isFromOrJoinFragment();
		}
	};

	private static ASTUtil.FilterPredicate projectionListPredicate = new ASTUtil.IncludePredicate() {
		public boolean include(AST node) {
			FromElement fromElement = ( FromElement ) node;
			return fromElement.inProjectionList();
		}
	};

	private static ASTUtil.FilterPredicate collectionFetchPredicate = new ASTUtil.IncludePredicate() {
		public boolean include(AST node) {
			FromElement fromElement = ( FromElement ) node;
			return fromElement.isFetch() && fromElement.getQueryableCollection() != null;
		}
	};

	private static ASTUtil.FilterPredicate explicitFromPredicate = new ASTUtil.IncludePredicate() {
		public boolean include(AST node) {
			final FromElement fromElement = ( FromElement ) node;
			return !fromElement.isImplied();
		}
	};

	FromElement findCollectionJoin(String path) {
		return ( FromElement ) collectionJoinFromElementsByPath.get( path );
	}

	/**
	 * Look for an existing implicit or explicit join by the
	 * given path.
	 */
	FromElement findJoinByPath(String path) {
		FromElement elem = findJoinByPathLocal( path );
		if ( elem == null && parentFromClause != null ) {
			elem = parentFromClause.findJoinByPath( path );
		}
		return elem;
	}

	FromElement findJoinByPathLocal(String path) {
		Map joinsByPath = fromElementsByPath;
		return ( FromElement ) joinsByPath.get( path );
	}

	void addJoinByPathMap(String path, FromElement destination) {
		if ( log.isDebugEnabled() ) {
			log.debug( "addJoinByPathMap() : " + path + " -> " + destination );
		}
		fromElementsByPath.put( path, destination );
	}

	/**
	 * Returns true if the from node contains the class alias name.
	 *
	 * @param alias The HQL class alias name.
	 * @return true if the from node contains the class alias name.
	 */
	public boolean containsClassAlias(String alias) {
		boolean isAlias = fromElementByClassAlias.containsKey( alias );
		if ( !isAlias && getSessionFactoryHelper().isStrictJPAQLComplianceEnabled() ) {
			isAlias = findIntendedAliasedFromElementBasedOnCrazyJPARequirements( alias ) != null;
		}
		return isAlias;
	}

	/**
	 * Returns true if the from node contains the table alias name.
	 *
	 * @param alias The SQL table alias name.
	 * @return true if the from node contains the table alias name.
	 */
	public boolean containsTableAlias(String alias) {
		return fromElementByTableAlias.keySet().contains( alias );
	}

	public String getDisplayText() {
		return "FromClause{" +
				"level=" + level +
				", fromElementCounter=" + fromElementCounter +
				", fromElements=" + fromElements.size() +
				", fromElementByClassAlias=" + fromElementByClassAlias.keySet() +
				", fromElementByTableAlias=" + fromElementByTableAlias.keySet() +
				", fromElementsByPath=" + fromElementsByPath.keySet() +
				", collectionJoinFromElementsByPath=" + collectionJoinFromElementsByPath.keySet() +
				", impliedElements=" + impliedElements +
				"}";
	}

	public void setParentFromClause(FromClause parentFromClause) {
		this.parentFromClause = parentFromClause;
		if ( parentFromClause != null ) {
			level = parentFromClause.getLevel() + 1;
			parentFromClause.addChild( this );
		}
	}

	private void addChild(FromClause fromClause) {
		if ( childFromClauses == null ) {
			childFromClauses = new HashSet();
		}
		childFromClauses.add( fromClause );
	}

	public FromClause locateChildFromClauseWithJoinByPath(String path) {
		if ( childFromClauses != null && !childFromClauses.isEmpty() ) {
			Iterator children = childFromClauses.iterator();
			while ( children.hasNext() ) {
				FromClause child = ( FromClause ) children.next();
				if ( child.findJoinByPathLocal( path ) != null ) {
					return child;
				}
			}
		}
		return null;
	}

	public void promoteJoin(FromElement elem) {
		if ( log.isDebugEnabled() ) {
			log.debug( "Promoting [" + elem + "] to [" + this + "]" );
		}
		//TODO: implement functionality
		//  this might be painful to do here, as the "join post processing" for
		//  the subquery has already been performed (meaning that for
		//  theta-join dialects, the join conditions have already been moved
		//  over to the where clause).  A "simple" solution here might to
		//  perform "join post processing" once for the entire query (including
		//  any subqueries) at one fell swoop
	}

	public boolean isSubQuery() {
		// TODO : this is broke for subqueries in statements other than selects...
		return parentFromClause != null;
	}

	void addCollectionJoinFromElementByPath(String path, FromElement destination) {
		if ( log.isDebugEnabled() ) {
			log.debug( "addCollectionJoinFromElementByPath() : " + path + " -> " + destination );
		}
		collectionJoinFromElementsByPath.put( path, destination );	// Add the new node to the map so that we don't create it twice.
	}

	public FromClause getParentFromClause() {
		return parentFromClause;
	}

	public int getLevel() {
		return level;
	}

	public int nextFromElementCounter() {
		return fromElementCounter++;
	}

	public void resolve() {
		// Make sure that all from elements registered with this FROM clause are actually in the AST.
		ASTIterator iter = new ASTIterator( this.getFirstChild() );
		Set childrenInTree = new HashSet();
		while ( iter.hasNext() ) {
			childrenInTree.add( iter.next() );
		}
		for ( Iterator iterator = fromElements.iterator(); iterator.hasNext(); ) {
			FromElement fromElement = ( FromElement ) iterator.next();
			if ( !childrenInTree.contains( fromElement ) ) {
				throw new IllegalStateException( "Element not in AST: " + fromElement );
			}
		}
	}

	public void addImpliedFromElement(FromElement element) {
		impliedElements.add( element );
	}

	public String toString() {
		return "FromClause{" +
				"level=" + level +
				"}";
	}
}
