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

import java.util.HashMap;
import java.util.Map;

import org.hibernate.MappingException;
import org.hibernate.QueryException;
import org.hibernate.dialect.function.SQLFunction;
import org.hibernate.engine.JoinSequence;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.hql.NameGenerator;
import org.hibernate.hql.ast.DetailedSemanticException;
import org.hibernate.hql.ast.QuerySyntaxException;
import org.hibernate.hql.ast.tree.SqlNode;
import org.hibernate.persister.collection.CollectionPropertyMapping;
import org.hibernate.persister.collection.CollectionPropertyNames;
import org.hibernate.persister.collection.QueryableCollection;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.PropertyMapping;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;

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

/**
 * Helper for performing common and/or complex operations with the
 * {@link SessionFactoryImplementor} during translation of an HQL query.
 *
 * @author Joshua Davis
 */
public class SessionFactoryHelper {

	private SessionFactoryImplementor sfi;
	private Map collectionPropertyMappingByRole;

	/**
	 * Construct a new SessionFactoryHelper instance.
	 *
	 * @param sfi The SessionFactory impl to be encapsulated.
	 */
	public SessionFactoryHelper(SessionFactoryImplementor sfi) {
		this.sfi = sfi;
		collectionPropertyMappingByRole = new HashMap();
	}

	/**
	 * Get a handle to the encapsulated SessionFactory.
	 *
	 * @return The encapsulated SessionFactory.
	 */
	public SessionFactoryImplementor getFactory() {
		return sfi;
	}

	/**
	 * Does the given persister define a physical discriminator column
	 * for the purpose of inheritance discrimination?
	 *
	 * @param persister The persister to be checked.
	 * @return True if the persister does define an actual discriminator column.
	 */
	public boolean hasPhysicalDiscriminatorColumn(Queryable persister) {
		if ( persister.getDiscriminatorType() != null ) {
			String discrimColumnName = persister.getDiscriminatorColumnName();
			// Needed the "clazz_" check to work around union-subclasses
			// TODO : is there a way to tell whether a persister is truly discrim-column based inheritence?
			if ( discrimColumnName != null && !"clazz_".equals( discrimColumnName ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Given a (potentially unqualified) class name, locate its imported qualified name.
	 *
	 * @param className The potentially unqualified class name
	 * @return The qualified class name.
	 */
	public String getImportedClassName(String className) {
		return sfi.getImportedClassName( className );
	}

	/**
	 * Given a (potentially unqualified) class name, locate its persister.
	 *
	 * @param className The (potentially unqualified) class name.
	 * @return The defined persister for this class, or null if none found.
	 */
	public Queryable findQueryableUsingImports(String className) {
		return findQueryableUsingImports( sfi, className );
	}


	/**
	 * Given a (potentially unqualified) class name, locate its persister.
	 *
	 * @param sfi The session factory implementor.
	 * @param className The (potentially unqualified) class name.
	 * @return The defined persister for this class, or null if none found.
	 */
	public static Queryable findQueryableUsingImports(SessionFactoryImplementor sfi, String className) {
		final String importedClassName = sfi.getImportedClassName( className );
		if ( importedClassName == null ) {
			return null;
		}
		try {
			return ( Queryable ) sfi.getEntityPersister( importedClassName );
		}
		catch ( MappingException me ) {
			return null;
		}
	}

	/**
	 * Locate the persister by class or entity name.
	 *
	 * @param name The class or entity name
	 * @return The defined persister for this entity, or null if none found.
	 * @throws MappingException
	 */
	private EntityPersister findEntityPersisterByName(String name) throws MappingException {
		// First, try to get the persister using the given name directly.
		try {
			return sfi.getEntityPersister( name );
		}
		catch ( MappingException ignore ) {
			// unable to locate it using this name
		}

		// If that didn't work, try using the 'import' name.
		String importedClassName = sfi.getImportedClassName( name );
		if ( importedClassName == null ) {
			return null;
		}
		return sfi.getEntityPersister( importedClassName );
	}

	/**
	 * Locate the persister by class or entity name, requiring that such a persister
	 * exist.
	 *
	 * @param name The class or entity name
	 * @return The defined persister for this entity
	 * @throws SemanticException Indicates the persister could not be found
	 */
	public EntityPersister requireClassPersister(String name) throws SemanticException {
		EntityPersister cp;
		try {
			cp = findEntityPersisterByName( name );
			if ( cp == null ) {
				throw new QuerySyntaxException( name + " is not mapped" );
			}
		}
		catch ( MappingException e ) {
			throw new DetailedSemanticException( e.getMessage(), e );
		}
		return cp;
	}

	/**
	 * Locate the collection persister by the collection role.
	 *
	 * @param role The collection role name.
	 * @return The defined CollectionPersister for this collection role, or null.
	 */
	public QueryableCollection getCollectionPersister(String role) {
		try {
			return ( QueryableCollection ) sfi.getCollectionPersister( role );
		}
		catch ( ClassCastException cce ) {
			throw new QueryException( "collection is not queryable: " + role );
		}
		catch ( Exception e ) {
			throw new QueryException( "collection not found: " + role );
		}
	}

	/**
	 * Locate the collection persister by the collection role, requiring that
	 * such a persister exist.
	 *
	 * @param role The collection role name.
	 * @return The defined CollectionPersister for this collection role.
	 * @throws QueryException Indicates that the collection persister could not be found.
	 */
	public QueryableCollection requireQueryableCollection(String role) throws QueryException {
		try {
			QueryableCollection queryableCollection = ( QueryableCollection ) sfi.getCollectionPersister( role );
			if ( queryableCollection != null ) {
				collectionPropertyMappingByRole.put( role, new CollectionPropertyMapping( queryableCollection ) );
			}
			return queryableCollection;
		}
		catch ( ClassCastException cce ) {
			throw new QueryException( "collection role is not queryable: " + role );
		}
		catch ( Exception e ) {
			throw new QueryException( "collection role not found: " + role );
		}
	}

	/**
	 * Retrieve a PropertyMapping describing the given collection role.
	 *
	 * @param role The collection role for which to retrieve the property mapping.
	 * @return The property mapping.
	 */
	private PropertyMapping getCollectionPropertyMapping(String role) {
		return ( PropertyMapping ) collectionPropertyMappingByRole.get( role );
	}

	/**
	 * Retrieves the column names corresponding to the collection elements for the given
	 * collection role.
	 *
	 * @param role The collection role
	 * @param roleAlias The sql column-qualification alias (i.e., the table alias)
	 * @return the collection element columns
	 */
	public String[] getCollectionElementColumns(String role, String roleAlias) {
		return getCollectionPropertyMapping( role ).toColumns( roleAlias, CollectionPropertyNames.COLLECTION_ELEMENTS );
	}

	/**
	 * Generate an empty join sequence instance.
	 *
	 * @return The generate join sequence.
	 */
	public JoinSequence createJoinSequence() {
		return new JoinSequence( sfi );
	}

	/**
	 * Generate a join sequence representing the given association type.
	 *
	 * @param implicit Should implicit joins (theta-style) or explicit joins (ANSI-style) be rendered
	 * @param associationType The type representing the thing to be joined into.
	 * @param tableAlias The table alias to use in qualifying the join conditions
	 * @param joinType The type of join to render (inner, outer, etc);  see {@link org.hibernate.sql.JoinFragment}
	 * @param columns The columns making up the condition of the join.
	 * @return The generated join sequence.
	 */
	public JoinSequence createJoinSequence(boolean implicit, AssociationType associationType, String tableAlias, int joinType, String[] columns) {
		JoinSequence joinSequence = createJoinSequence();
		joinSequence.setUseThetaStyle( implicit );	// Implicit joins use theta style (WHERE pk = fk), explicit joins use JOIN (after from)
		joinSequence.addJoin( associationType, tableAlias, joinType, columns );
		return joinSequence;
	}

	/**
	 * Create a join sequence rooted at the given collection.
	 *
	 * @param collPersister The persister for the collection at which the join should be rooted.
	 * @param collectionName The alias to use for qualifying column references.
	 * @return The generated join sequence.
	 */
	public JoinSequence createCollectionJoinSequence(QueryableCollection collPersister, String collectionName) {
		JoinSequence joinSequence = createJoinSequence();
		joinSequence.setRoot( collPersister, collectionName );
		joinSequence.setUseThetaStyle( true );		// TODO: figure out how this should be set.
///////////////////////////////////////////////////////////////////////////////
// This was the reason for failures regarding INDEX_OP and subclass joins on
// theta-join dialects; not sure what behavior we were trying to emulate ;)
//		joinSequence = joinSequence.getFromPart();	// Emulate the old addFromOnly behavior.
		return joinSequence;
	}

	/**
	 * Determine the name of the property for the entity encapsulated by the
	 * given type which represents the id or unique-key.
	 *
	 * @param entityType The type representing the entity.
	 * @return The corresponding property name
	 * @throws QueryException Indicates such a property could not be found.
	 */
	public String getIdentifierOrUniqueKeyPropertyName(EntityType entityType) {
		try {
			return entityType.getIdentifierOrUniqueKeyPropertyName( sfi );
		}
		catch ( MappingException me ) {
			throw new QueryException( me );
		}
	}

	/**
	 * Retrieve the number of columns represented by this type.
	 *
	 * @param type The type.
	 * @return The number of columns.
	 */
	public int getColumnSpan(Type type) {
		return type.getColumnSpan( sfi );
	}

	/**
	 * Given a collection type, determine the entity name of the elements
	 * contained within instance of that collection.
	 *
	 * @param collectionType The collection type to check.
	 * @return The entity name of the elements of this collection.
	 */
	public String getAssociatedEntityName(CollectionType collectionType) {
		return collectionType.getAssociatedEntityName( sfi );
	}

	/**
	 * Given a collection type, determine the Type representing elements
	 * within instances of that collection.
	 *
	 * @param collectionType The collection type to be checked.
	 * @return The Type of the elements of the collection.
	 */
	private Type getElementType(CollectionType collectionType) {
		return collectionType.getElementType( sfi );
	}

	/**
	 * Essentially the same as {@link #getElementType}, but requiring that the
	 * element type be an association type.
	 *
	 * @param collectionType The collection type to be checked.
	 * @return The AssociationType of the elements of the collection.
	 */
	public AssociationType getElementAssociationType(CollectionType collectionType) {
		return ( AssociationType ) getElementType( collectionType );
	}

	/**
	 * Locate a registered sql function by name.
	 *
	 * @param functionName The name of the function to locate
	 * @return The sql function, or null if not found.
	 */
	public SQLFunction findSQLFunction(String functionName) {
		return sfi.getSqlFunctionRegistry().findSQLFunction( functionName.toLowerCase() );
	}

	/**
	 * Locate a registered sql function by name, requiring that such a registered function exist.
	 *
	 * @param functionName The name of the function to locate
	 * @return The sql function.
	 * @throws QueryException Indicates no matching sql functions could be found.
	 */
	private SQLFunction requireSQLFunction(String functionName) {
		SQLFunction f = findSQLFunction( functionName );
		if ( f == null ) {
			throw new QueryException( "Unable to find SQL function: " + functionName );
		}
		return f;
	}

	/**
	 * Find the function return type given the function name and the first argument expression node.
	 *
	 * @param functionName The function name.
	 * @param first        The first argument expression.
	 * @return the function return type given the function name and the first argument expression node.
	 */
	public Type findFunctionReturnType(String functionName, AST first) {
		SQLFunction sqlFunction = requireSQLFunction( functionName );
		return findFunctionReturnType( functionName, sqlFunction, first );
	}

	public Type findFunctionReturnType(String functionName, SQLFunction sqlFunction, AST firstArgument) {
		// determine the type of the first argument...
		Type argumentType = null;
		if ( firstArgument != null ) {
			if ( "cast".equals(functionName) ) {
				argumentType = sfi.getTypeResolver().heuristicType( firstArgument.getNextSibling().getText() );
			}
			else if ( SqlNode.class.isInstance( firstArgument ) ) {
				argumentType = ( (SqlNode) firstArgument ).getDataType();
			}
		}

		return sqlFunction.getReturnType( argumentType, sfi );
	}

	public String[][] generateColumnNames(Type[] sqlResultTypes) {
		return NameGenerator.generateColumnNames( sqlResultTypes, sfi );
	}

	public boolean isStrictJPAQLComplianceEnabled() {
		return sfi.getSettings().isStrictJPAQLCompliance();
	}
}
