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

import org.hibernate.engine.internal.JoinSequence;
import org.hibernate.hql.internal.antlr.SqlTokenTypes;
import org.hibernate.hql.internal.ast.util.ASTUtil;
import org.hibernate.hql.internal.ast.util.AliasGenerator;
import org.hibernate.hql.internal.ast.util.PathHelper;
import org.hibernate.hql.internal.ast.util.SessionFactoryHelper;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.persister.collection.QueryableCollection;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.sql.JoinType;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;

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

/**
 * Encapsulates the creation of FromElements and JoinSequences.
 *
 * @author josh
 */
public class FromElementFactory implements SqlTokenTypes {
	private static final CoreMessageLogger LOG = CoreLogging.messageLogger( FromElementFactory.class );

	private FromClause fromClause;
	private FromElement origin;
	private String path;

	private String classAlias;
	private String[] columns;
	private boolean implied;
	private boolean inElementsFunction;
	private boolean collection;
	private QueryableCollection queryableCollection;
	private CollectionType collectionType;

	/**
	 * Creates entity from elements.
	 */
	public FromElementFactory(FromClause fromClause, FromElement origin, String path) {
		this.fromClause = fromClause;
		this.origin = origin;
		this.path = path;
		collection = false;
	}

	/**
	 * Creates collection from elements.
	 */
	public FromElementFactory(
			FromClause fromClause,
			FromElement origin,
			String path,
			String classAlias,
			String[] columns,
			boolean implied) {
		this( fromClause, origin, path );
		this.classAlias = classAlias;
		this.columns = columns;
		this.implied = implied;
		collection = true;
	}

	FromElement addFromElement() throws SemanticException {
		final FromClause parentFromClause = fromClause.getParentFromClause();
		if ( parentFromClause != null ) {
			// Look up class name using the first identifier in the path.
			final String pathAlias = PathHelper.getAlias( path );
			final FromElement parentFromElement = parentFromClause.getFromElement( pathAlias );
			if ( parentFromElement != null ) {
				return createFromElementInSubselect( path, pathAlias, parentFromElement, classAlias );
			}
		}

		final EntityPersister entityPersister = fromClause.getSessionFactoryHelper().requireClassPersister( path );

		final FromElement elem = createAndAddFromElement(
				path,
				classAlias,
				entityPersister,
				(EntityType) ( (Queryable) entityPersister ).getType(),
				null
		);

		// Add to the query spaces.
		fromClause.getWalker().addQuerySpaces( entityPersister.getQuerySpaces() );

		return elem;
	}

	private FromElement createFromElementInSubselect(
			String path,
			String pathAlias,
			FromElement parentFromElement,
			String classAlias) throws SemanticException {
		LOG.debugf( "createFromElementInSubselect() : path = %s", path );

		// Create an DotNode AST for the path and resolve it.
		FromElement fromElement = evaluateFromElementPath( path, classAlias );
		EntityPersister entityPersister = fromElement.getEntityPersister();

		// If the first identifier in the path refers to the class alias (not the class name), then this
		// is a correlated subselect.  If it's a correlated sub-select, use the existing table alias.  Otherwise
		// generate a new one.
		String tableAlias = null;
		boolean correlatedSubselect = pathAlias.equals( parentFromElement.getClassAlias() );
		if ( correlatedSubselect ) {
			tableAlias = fromElement.getTableAlias();
		}
		else {
			tableAlias = null;
		}

		// If the from element isn't in the same clause, create a new from element.
		if ( fromElement.getFromClause() != fromClause ) {
			LOG.debug( "createFromElementInSubselect() : creating a new FROM element..." );
			fromElement = createFromElement( entityPersister );
			initializeAndAddFromElement(
					fromElement,
					path,
					classAlias,
					entityPersister,
					(EntityType) ( (Queryable) entityPersister ).getType(),
					tableAlias
			);
		}
		LOG.debugf( "createFromElementInSubselect() : %s -> %s", path, fromElement );
		return fromElement;
	}

	private FromElement evaluateFromElementPath(String path, String classAlias) throws SemanticException {
		ASTFactory factory = fromClause.getASTFactory();
		FromReferenceNode pathNode = (FromReferenceNode) PathHelper.parsePath( path, factory );
		pathNode.recursiveResolve(
				// This is the root level node.
				FromReferenceNode.ROOT_LEVEL,
				// Generate an explicit from clause at the root.
				false,
				classAlias,
				null
		);
		if ( pathNode.getImpliedJoin() != null ) {
			return pathNode.getImpliedJoin();
		}
		return pathNode.getFromElement();
	}

	FromElement createCollectionElementsJoin(
			QueryableCollection queryableCollection,
			String collectionName) throws SemanticException {
		JoinSequence collectionJoinSequence = fromClause.getSessionFactoryHelper()
				.createCollectionJoinSequence( queryableCollection, collectionName );
		this.queryableCollection = queryableCollection;
		return createCollectionJoin( collectionJoinSequence, null );
	}

	public FromElement createCollection(
			QueryableCollection queryableCollection,
			String role,
			JoinType joinType,
			boolean fetchFlag,
			boolean indexed)
			throws SemanticException {
		if ( !collection ) {
			throw new IllegalStateException( "FromElementFactory not initialized for collections!" );
		}
		this.inElementsFunction = indexed;
		FromElement elem;
		this.queryableCollection = queryableCollection;
		collectionType = queryableCollection.getCollectionType();
		String roleAlias = fromClause.getAliasGenerator().createName( role );

		// Correlated subqueries create 'special' implied from nodes
		// because correlated subselects can't use an ANSI-style join
		boolean explicitSubqueryFromElement = fromClause.isSubQuery() && !implied;
		if ( explicitSubqueryFromElement ) {
			String pathRoot = StringHelper.root( path );
			FromElement origin = fromClause.getFromElement( pathRoot );
			if ( origin == null || origin.getFromClause() != fromClause ) {
				implied = true;
			}
		}

		// super-duper-classic-parser-regression-testing-mojo-magic...
		if ( explicitSubqueryFromElement && DotNode.useThetaStyleImplicitJoins ) {
			implied = true;
		}

		Type elementType = queryableCollection.getElementType();
		if ( elementType.isEntityType() ) {
			// A collection of entities...
			elem = createEntityAssociation( role, roleAlias, joinType );
		}
		else if ( elementType.isComponentType() ) {
			// A collection of components...
			JoinSequence joinSequence = createJoinSequence( roleAlias, joinType );
			elem = createCollectionJoin( joinSequence, roleAlias );
		}
		else {
			// A collection of scalar elements...
			JoinSequence joinSequence = createJoinSequence( roleAlias, joinType );
			elem = createCollectionJoin( joinSequence, roleAlias );
		}

		elem.setRole( role );
		elem.setQueryableCollection( queryableCollection );
		// Don't include sub-classes for implied collection joins or subquery joins.
		if ( implied ) {
			elem.setIncludeSubclasses( false );
		}

		if ( explicitSubqueryFromElement ) {
			// Treat explict from elements in sub-queries properly.
			elem.setInProjectionList( true );
		}

		if ( fetchFlag ) {
			elem.setFetch( true );
		}
		return elem;
	}

	public FromElement createEntityJoin(
			String entityClass,
			String tableAlias,
			JoinSequence joinSequence,
			boolean fetchFlag,
			boolean inFrom,
			EntityType type,
			String role,
			String joinPath) throws SemanticException {
		FromElement elem = createJoin( entityClass, tableAlias, joinSequence, type, false );
		elem.setFetch( fetchFlag );

		if ( joinPath != null ) {
			elem.applyTreatAsDeclarations( fromClause.getWalker().getTreatAsDeclarationsByPath( joinPath ) );
		}

		EntityPersister entityPersister = elem.getEntityPersister();
		int numberOfTables = entityPersister.getQuerySpaces().length;
		if ( numberOfTables > 1 && implied && !elem.useFromFragment() ) {
			LOG.debug( "createEntityJoin() : Implied multi-table entity join" );
			elem.setUseFromFragment( true );
		}

		// If this is an implied join in a FROM clause, then use ANSI-style joining, and set the
		// flag on the FromElement that indicates that it was implied in the FROM clause itself.
		if ( implied && inFrom ) {
			joinSequence.setUseThetaStyle( false );
			elem.setUseFromFragment( true );
			elem.setImpliedInFromClause( true );
		}
		if ( elem.getWalker().isSubQuery() ) {
			// two conditions where we need to transform this to a theta-join syntax:
			//      1) 'elem' is the "root from-element" in correlated subqueries
			//      2) The DotNode.useThetaStyleImplicitJoins has been set to true
			//          and 'elem' represents an implicit join
			if ( elem.getFromClause() != elem.getOrigin().getFromClause() ||
//			        ( implied && DotNode.useThetaStyleImplicitJoins ) ) {
					DotNode.useThetaStyleImplicitJoins ) {
				// the "root from-element" in correlated subqueries do need this piece
				elem.setType( FROM_FRAGMENT );
				joinSequence.setUseThetaStyle( true );
				elem.setUseFromFragment( false );
			}
		}

		elem.setRole( role );

		return elem;
	}

	public FromElement createComponentJoin(CompositeType type) {

		// need to create a "place holder" from-element that can store the component/alias for this
		// 		component join
		return new ComponentJoin( fromClause, origin, classAlias, path, type );
	}

	FromElement createElementJoin(QueryableCollection queryableCollection) throws SemanticException {
		FromElement elem;

		implied = true; //TODO: always true for now, but not if we later decide to support elements() in the from clause
		inElementsFunction = true;
		Type elementType = queryableCollection.getElementType();
		if ( !elementType.isEntityType() ) {
			throw new IllegalArgumentException( "Cannot create element join for a collection of non-entities!" );
		}
		this.queryableCollection = queryableCollection;
		SessionFactoryHelper sfh = fromClause.getSessionFactoryHelper();
		FromElement destination = null;
		String tableAlias = null;
		EntityPersister entityPersister = queryableCollection.getElementPersister();
		tableAlias = fromClause.getAliasGenerator().createName( entityPersister.getEntityName() );
		String associatedEntityName = entityPersister.getEntityName();
		EntityPersister targetEntityPersister = sfh.requireClassPersister( associatedEntityName );
		// Create the FROM element for the target (the elements of the collection).
		destination = createAndAddFromElement(
				associatedEntityName,
				classAlias,
				targetEntityPersister,
				(EntityType) queryableCollection.getElementType(),
				tableAlias
		);
		// If the join is implied, then don't include sub-classes on the element.
		if ( implied ) {
			destination.setIncludeSubclasses( false );
		}
		fromClause.addCollectionJoinFromElementByPath( path, destination );
//		origin.addDestination(destination);
		// Add the query spaces.
		fromClause.getWalker().addQuerySpaces( entityPersister.getQuerySpaces() );

		CollectionType type = queryableCollection.getCollectionType();
		String role = type.getRole();
		String roleAlias = origin.getTableAlias();

		String[] targetColumns = sfh.getCollectionElementColumns( role, roleAlias );
		AssociationType elementAssociationType = sfh.getElementAssociationType( type );

		// Create the join element under the from element.
		JoinType joinType = JoinType.INNER_JOIN;
		JoinSequence joinSequence = sfh.createJoinSequence(
				implied,
				elementAssociationType,
				tableAlias,
				joinType,
				targetColumns
		);
		elem = initializeJoin( path, destination, joinSequence, targetColumns, origin, false );
		elem.setUseFromFragment( true );    // The associated entity is implied, but it must be included in the FROM.
		elem.setCollectionTableAlias( roleAlias );    // The collection alias is the role.
		return elem;
	}

	private FromElement createCollectionJoin(JoinSequence collectionJoinSequence, String tableAlias)
			throws SemanticException {
		String text = queryableCollection.getTableName();
		AST ast = createFromElement( text );
		FromElement destination = (FromElement) ast;
		Type elementType = queryableCollection.getElementType();
		if ( elementType.isCollectionType() ) {
			throw new SemanticException( "Collections of collections are not supported!" );
		}
		destination.initializeCollection( fromClause, classAlias, tableAlias );
		destination.setType( JOIN_FRAGMENT );        // Tag this node as a JOIN.
		destination.setIncludeSubclasses( false );    // Don't include subclasses in the join.
		destination.setCollectionJoin( true );        // This is a clollection join.
		destination.setJoinSequence( collectionJoinSequence );
		destination.setOrigin( origin, false );
		destination.setCollectionTableAlias( tableAlias );
//		origin.addDestination( destination );
// This was the cause of HHH-242
//		origin.setType( FROM_FRAGMENT );			// Set the parent node type so that the AST is properly formed.
		origin.setText( "" );                        // The destination node will have all the FROM text.
		origin.setCollectionJoin( true );            // The parent node is a collection join too (voodoo - see JoinProcessor)
		fromClause.addCollectionJoinFromElementByPath( path, destination );
		fromClause.getWalker().addQuerySpaces( queryableCollection.getCollectionSpaces() );
		return destination;
	}

	private FromElement createEntityAssociation(
			String role,
			String roleAlias,
			JoinType joinType) throws SemanticException {
		FromElement elem;
		Queryable entityPersister = (Queryable) queryableCollection.getElementPersister();
		String associatedEntityName = entityPersister.getEntityName();
		// Get the class name of the associated entity.
		if ( queryableCollection.isOneToMany() ) {
			LOG.debugf(
					"createEntityAssociation() : One to many - path = %s role = %s associatedEntityName = %s",
					path,
					role,
					associatedEntityName
			);
			JoinSequence joinSequence = createJoinSequence( roleAlias, joinType );

			elem = createJoin(
					associatedEntityName,
					roleAlias,
					joinSequence,
					(EntityType) queryableCollection.getElementType(),
					false
			);
		}
		else {
			LOG.debugf(
					"createManyToMany() : path = %s role = %s associatedEntityName = %s",
					path,
					role,
					associatedEntityName
			);
			elem = createManyToMany(
					role, associatedEntityName,
					roleAlias, entityPersister, (EntityType) queryableCollection.getElementType(), joinType
			);
			fromClause.getWalker().addQuerySpaces( queryableCollection.getCollectionSpaces() );
		}
		elem.setCollectionTableAlias( roleAlias );
		return elem;
	}

	private FromElement createJoin(
			String entityClass,
			String tableAlias,
			JoinSequence joinSequence,
			EntityType type,
			boolean manyToMany) throws SemanticException {
		//  origin, path, implied, columns, classAlias,
		EntityPersister entityPersister = fromClause.getSessionFactoryHelper().requireClassPersister( entityClass );
		FromElement destination = createAndAddFromElement(
				entityClass,
				classAlias,
				entityPersister,
				type,
				tableAlias
		);
		return initializeJoin( path, destination, joinSequence, getColumns(), origin, manyToMany );
	}

	private FromElement createManyToMany(
			String role,
			String associatedEntityName,
			String roleAlias,
			Queryable entityPersister,
			EntityType type,
			JoinType joinType) throws SemanticException {
		FromElement elem;
		SessionFactoryHelper sfh = fromClause.getSessionFactoryHelper();
		if ( inElementsFunction /*implied*/ ) {
			// For implied many-to-many, just add the end join.
			JoinSequence joinSequence = createJoinSequence( roleAlias, joinType );
			elem = createJoin( associatedEntityName, roleAlias, joinSequence, type, true );
		}
		else {
			// For an explicit many-to-many relationship, add a second join from the intermediate
			// (many-to-many) table to the destination table.  Also, make sure that the from element's
			// idea of the destination is the destination table.
			String tableAlias = fromClause.getAliasGenerator().createName( entityPersister.getEntityName() );
			String[] secondJoinColumns = sfh.getCollectionElementColumns( role, roleAlias );
			// Add the second join, the one that ends in the destination table.
			JoinSequence joinSequence = createJoinSequence( roleAlias, joinType );
			joinSequence.addJoin(
					sfh.getElementAssociationType( collectionType ),
					tableAlias,
					joinType,
					secondJoinColumns
			);
			elem = createJoin( associatedEntityName, tableAlias, joinSequence, type, false );
			elem.setUseFromFragment( true );
		}
		return elem;
	}

	private JoinSequence createJoinSequence(String roleAlias, JoinType joinType) {
		SessionFactoryHelper sessionFactoryHelper = fromClause.getSessionFactoryHelper();
		String[] joinColumns = getColumns();
		if ( collectionType == null ) {
			throw new IllegalStateException( "collectionType is null!" );
		}
		return sessionFactoryHelper.createJoinSequence( implied, collectionType, roleAlias, joinType, joinColumns );
	}

	private FromElement createAndAddFromElement(
			String className,
			String classAlias,
			EntityPersister entityPersister,
			EntityType type,
			String tableAlias) {
		if ( !( entityPersister instanceof Joinable ) ) {
			throw new IllegalArgumentException( "EntityPersister " + entityPersister + " does not implement Joinable!" );
		}
		FromElement element = createFromElement( entityPersister );
		initializeAndAddFromElement( element, className, classAlias, entityPersister, type, tableAlias );
		return element;
	}

	private void initializeAndAddFromElement(
			FromElement element,
			String className,
			String classAlias,
			EntityPersister entityPersister,
			EntityType type,
			String tableAlias) {
		if ( tableAlias == null ) {
			AliasGenerator aliasGenerator = fromClause.getAliasGenerator();
			tableAlias = aliasGenerator.createName( entityPersister.getEntityName() );
		}
		element.initializeEntity( fromClause, className, entityPersister, type, classAlias, tableAlias );
	}

	private FromElement createFromElement(EntityPersister entityPersister) {
		Joinable joinable = (Joinable) entityPersister;
		String text = joinable.getTableName();
		AST ast = createFromElement( text );
		FromElement element = (FromElement) ast;
		return element;
	}

	private AST createFromElement(String text) {
		AST ast = ASTUtil.create(
				fromClause.getASTFactory(),
				implied ? IMPLIED_FROM : FROM_FRAGMENT, // This causes the factory to instantiate the desired class.
				text
		);
		// Reset the node type, because the rest of the system is expecting FROM_FRAGMENT, all we wanted was
		// for the factory to create the right sub-class.  This might get reset again later on anyway to make the
		// SQL generation simpler.
		ast.setType( FROM_FRAGMENT );
		return ast;
	}

	private FromElement initializeJoin(
			String path,
			FromElement destination,
			JoinSequence joinSequence,
			String[] columns,
			FromElement origin,
			boolean manyToMany) {
		destination.setType( JOIN_FRAGMENT );
		destination.setJoinSequence( joinSequence );
		destination.setColumns( columns );
		destination.setOrigin( origin, manyToMany );
		fromClause.addJoinByPathMap( path, destination );
		return destination;
	}

	private String[] getColumns() {
		if ( columns == null ) {
			throw new IllegalStateException( "No foriegn key columns were supplied!" );
		}
		return columns;
	}
}
