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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.LinkedHashMap;

import org.hibernate.Criteria;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.MappingException;
import org.hibernate.QueryException;
import org.hibernate.LockOptions;
import org.hibernate.criterion.EnhancedProjection;
import org.hibernate.hql.ast.util.SessionFactoryHelper;
import org.hibernate.criterion.CriteriaQuery;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Projection;
import org.hibernate.engine.QueryParameters;
import org.hibernate.engine.RowSelection;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.engine.TypedValue;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.persister.entity.Loadable;
import org.hibernate.persister.entity.PropertyMapping;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.type.AssociationType;
import org.hibernate.type.StringRepresentableType;
import org.hibernate.type.Type;
import org.hibernate.util.ArrayHelper;
import org.hibernate.util.StringHelper;

/**
 * @author Gavin King
 */
public class CriteriaQueryTranslator implements CriteriaQuery {

	public static final String ROOT_SQL_ALIAS = Criteria.ROOT_ALIAS + '_';

	private CriteriaQuery outerQueryTranslator;

	private final CriteriaImpl rootCriteria;
	private final String rootEntityName;
	private final String rootSQLAlias;
	private int aliasCount = 0;

	private final Map criteriaEntityNames = new LinkedHashMap();
	private final Map criteriaSQLAliasMap = new HashMap();
	private final Map aliasCriteriaMap = new HashMap();
	private final Map associationPathCriteriaMap = new LinkedHashMap();
	private final Map associationPathJoinTypesMap = new LinkedHashMap();
	private final Map withClauseMap = new HashMap();
	
	private final SessionFactoryImplementor sessionFactory;

	public CriteriaQueryTranslator(
			final SessionFactoryImplementor factory,
	        final CriteriaImpl criteria,
	        final String rootEntityName,
	        final String rootSQLAlias,
	        CriteriaQuery outerQuery) throws HibernateException {
		this( factory, criteria, rootEntityName, rootSQLAlias );
		outerQueryTranslator = outerQuery;
	}

	public CriteriaQueryTranslator(
			final SessionFactoryImplementor factory,
	        final CriteriaImpl criteria,
	        final String rootEntityName,
	        final String rootSQLAlias) throws HibernateException {
		this.rootCriteria = criteria;
		this.rootEntityName = rootEntityName;
		this.sessionFactory = factory;
		this.rootSQLAlias = rootSQLAlias;
		createAliasCriteriaMap();
		createAssociationPathCriteriaMap();
		createCriteriaEntityNameMap();
		createCriteriaSQLAliasMap();
	}

	public String generateSQLAlias() {
		return StringHelper.generateAlias( Criteria.ROOT_ALIAS, aliasCount ) + '_';
	}

	public String getRootSQLALias() {
		return rootSQLAlias;
	}

	private Criteria getAliasedCriteria(String alias) {
		return ( Criteria ) aliasCriteriaMap.get( alias );
	}

	public boolean isJoin(String path) {
		return associationPathCriteriaMap.containsKey( path );
	}

	public int getJoinType(String path) {
		Integer result = ( Integer ) associationPathJoinTypesMap.get( path );
		return ( result == null ? Criteria.INNER_JOIN : result.intValue() );
	}

	public Criteria getCriteria(String path) {
		return ( Criteria ) associationPathCriteriaMap.get( path );
	}

	public Set getQuerySpaces() {
		Set result = new HashSet();
		Iterator iter = criteriaEntityNames.values().iterator();
		while ( iter.hasNext() ) {
			String entityName = ( String ) iter.next();
			result.addAll( Arrays.asList( getFactory().getEntityPersister( entityName ).getQuerySpaces() ) );
		}
		return result;
	}

	private void createAliasCriteriaMap() {
		aliasCriteriaMap.put( rootCriteria.getAlias(), rootCriteria );
		Iterator iter = rootCriteria.iterateSubcriteria();
		while ( iter.hasNext() ) {
			Criteria subcriteria = ( Criteria ) iter.next();
			if ( subcriteria.getAlias() != null ) {
				Object old = aliasCriteriaMap.put( subcriteria.getAlias(), subcriteria );
				if ( old != null ) {
					throw new QueryException( "duplicate alias: " + subcriteria.getAlias() );
				}
			}
		}
	}

	private void createAssociationPathCriteriaMap() {
		Iterator iter = rootCriteria.iterateSubcriteria();
		while ( iter.hasNext() ) {
			CriteriaImpl.Subcriteria crit = ( CriteriaImpl.Subcriteria ) iter.next();
			String wholeAssociationPath = getWholeAssociationPath( crit );
			Object old = associationPathCriteriaMap.put( wholeAssociationPath, crit );
			if ( old != null ) {
				throw new QueryException( "duplicate association path: " + wholeAssociationPath );
			}
			int joinType = crit.getJoinType();
			old = associationPathJoinTypesMap.put( wholeAssociationPath, new Integer( joinType ) );
			if ( old != null ) {
				// TODO : not so sure this is needed...
				throw new QueryException( "duplicate association path: " + wholeAssociationPath );
			}
			if ( crit.getWithClause() != null )
			{
				this.withClauseMap.put(wholeAssociationPath, crit.getWithClause());
			}
		}
	}

	private String getWholeAssociationPath(CriteriaImpl.Subcriteria subcriteria) {
		String path = subcriteria.getPath();

		// some messy, complex stuff here, since createCriteria() can take an
		// aliased path, or a path rooted at the creating criteria instance
		Criteria parent = null;
		if ( path.indexOf( '.' ) > 0 ) {
			// if it is a compound path
			String testAlias = StringHelper.root( path );
			if ( !testAlias.equals( subcriteria.getAlias() ) ) {
				// and the qualifier is not the alias of this criteria
				//      -> check to see if we belong to some criteria other
				//          than the one that created us
				parent = ( Criteria ) aliasCriteriaMap.get( testAlias );
			}
		}
		if ( parent == null ) {
			// otherwise assume the parent is the the criteria that created us
			parent = subcriteria.getParent();
		}
		else {
			path = StringHelper.unroot( path );
		}

		if ( parent.equals( rootCriteria ) ) {
			// if its the root criteria, we are done
			return path;
		}
		else {
			// otherwise, recurse
			return getWholeAssociationPath( ( CriteriaImpl.Subcriteria ) parent ) + '.' + path;
		}
	}

	private void createCriteriaEntityNameMap() {
		criteriaEntityNames.put( rootCriteria, rootEntityName );
		Iterator iter = associationPathCriteriaMap.entrySet().iterator();
		while ( iter.hasNext() ) {
			Map.Entry me = ( Map.Entry ) iter.next();
			criteriaEntityNames.put(
					me.getValue(), //the criteria instance
			        getPathEntityName( ( String ) me.getKey() )
			);
		}
	}

	private String getPathEntityName(String path) {
		Queryable persister = ( Queryable ) sessionFactory.getEntityPersister( rootEntityName );
		StringTokenizer tokens = new StringTokenizer( path, "." );
		String componentPath = "";
		while ( tokens.hasMoreTokens() ) {
			componentPath += tokens.nextToken();
			Type type = persister.toType( componentPath );
			if ( type.isAssociationType() ) {
				AssociationType atype = ( AssociationType ) type;
				persister = ( Queryable ) sessionFactory.getEntityPersister(
						atype.getAssociatedEntityName( sessionFactory )
				);
				componentPath = "";
			}
			else if ( type.isComponentType() ) {
				componentPath += '.';
			}
			else {
				throw new QueryException( "not an association: " + componentPath );
			}
		}
		return persister.getEntityName();
	}

	public int getSQLAliasCount() {
		return criteriaSQLAliasMap.size();
	}

	private void createCriteriaSQLAliasMap() {
		int i = 0;
		Iterator criteriaIterator = criteriaEntityNames.entrySet().iterator();
		while ( criteriaIterator.hasNext() ) {
			Map.Entry me = ( Map.Entry ) criteriaIterator.next();
			Criteria crit = ( Criteria ) me.getKey();
			String alias = crit.getAlias();
			if ( alias == null ) {
				alias = ( String ) me.getValue(); // the entity name
			}
			criteriaSQLAliasMap.put( crit, StringHelper.generateAlias( alias, i++ ) );
		}
		criteriaSQLAliasMap.put( rootCriteria, rootSQLAlias );
	}

	public CriteriaImpl getRootCriteria() {
		return rootCriteria;
	}

	public QueryParameters getQueryParameters() {
		LockOptions lockOptions = new LockOptions();
		RowSelection selection = new RowSelection();
		selection.setFirstRow( rootCriteria.getFirstResult() );
		selection.setMaxRows( rootCriteria.getMaxResults() );
		selection.setTimeout( rootCriteria.getTimeout() );
		selection.setFetchSize( rootCriteria.getFetchSize() );

		Iterator iter = rootCriteria.getLockModes().entrySet().iterator();
		while ( iter.hasNext() ) {
			Map.Entry me = ( Map.Entry ) iter.next();
			final Criteria subcriteria = getAliasedCriteria( ( String ) me.getKey() );
			lockOptions.setAliasSpecificLockMode( getSQLAlias( subcriteria ), (LockMode)me.getValue() );
		}
		List values = new ArrayList();
		List types = new ArrayList();
		iter = rootCriteria.iterateSubcriteria();
		while ( iter.hasNext() ) {
			CriteriaImpl.Subcriteria subcriteria = ( CriteriaImpl.Subcriteria ) iter.next();
			LockMode lm = subcriteria.getLockMode();
			if ( lm != null ) {
				lockOptions.setAliasSpecificLockMode( getSQLAlias( subcriteria ), lm );
			}
			if ( subcriteria.getWithClause() != null )
			{
				TypedValue[] tv = subcriteria.getWithClause().getTypedValues( subcriteria, this );
				for ( int i = 0; i < tv.length; i++ ) {
					values.add( tv[i].getValue() );
					types.add( tv[i].getType() );
				}
			}
		}

		// Type and value gathering for the WHERE clause needs to come AFTER lock mode gathering,
		// because the lock mode gathering loop now contains join clauses which can contain
		// parameter bindings (as in the HQL WITH clause).
		iter = rootCriteria.iterateExpressionEntries();
		while ( iter.hasNext() ) {
			CriteriaImpl.CriterionEntry ce = ( CriteriaImpl.CriterionEntry ) iter.next();
			TypedValue[] tv = ce.getCriterion().getTypedValues( ce.getCriteria(), this );
			for ( int i = 0; i < tv.length; i++ ) {
				values.add( tv[i].getValue() );
				types.add( tv[i].getType() );
			}
		}

		Object[] valueArray = values.toArray();
		Type[] typeArray = ArrayHelper.toTypeArray( types );
		return new QueryParameters(
				typeArray,
		        valueArray,
		        lockOptions,
		        selection,
		        rootCriteria.isReadOnlyInitialized(),
		        ( rootCriteria.isReadOnlyInitialized() ? rootCriteria.isReadOnly() : false ),
		        rootCriteria.getCacheable(),
		        rootCriteria.getCacheRegion(),
		        rootCriteria.getComment(),
		        rootCriteria.isLookupByNaturalKey(),
		        rootCriteria.getResultTransformer()
		);
	}

	public boolean hasProjection() {
		return rootCriteria.getProjection() != null;
	}

	public String getGroupBy() {
		if ( rootCriteria.getProjection().isGrouped() ) {
			return rootCriteria.getProjection()
					.toGroupSqlString( rootCriteria.getProjectionCriteria(), this );
		}
		else {
			return "";
		}
	}

	public String getSelect() {
		return rootCriteria.getProjection().toSqlString(
				rootCriteria.getProjectionCriteria(),
		        0,
		        this
		);
	}

	public Type[] getProjectedTypes() {
		return rootCriteria.getProjection().getTypes( rootCriteria, this );
	}

	public String[] getProjectedColumnAliases() {
		return rootCriteria.getProjection() instanceof EnhancedProjection ?
				( ( EnhancedProjection ) rootCriteria.getProjection() ).getColumnAliases( 0, rootCriteria, this ) :
				rootCriteria.getProjection().getColumnAliases( 0 );
	}

	public String[] getProjectedAliases() {
		return rootCriteria.getProjection().getAliases();
	}

	public String getWhereCondition() {
		StringBuffer condition = new StringBuffer( 30 );
		Iterator criterionIterator = rootCriteria.iterateExpressionEntries();
		while ( criterionIterator.hasNext() ) {
			CriteriaImpl.CriterionEntry entry = ( CriteriaImpl.CriterionEntry ) criterionIterator.next();
			String sqlString = entry.getCriterion().toSqlString( entry.getCriteria(), this );
			condition.append( sqlString );
			if ( criterionIterator.hasNext() ) {
				condition.append( " and " );
			}
		}
		return condition.toString();
	}

	public String getOrderBy() {
		StringBuffer orderBy = new StringBuffer( 30 );
		Iterator criterionIterator = rootCriteria.iterateOrderings();
		while ( criterionIterator.hasNext() ) {
			CriteriaImpl.OrderEntry oe = ( CriteriaImpl.OrderEntry ) criterionIterator.next();
			orderBy.append( oe.getOrder().toSqlString( oe.getCriteria(), this ) );
			if ( criterionIterator.hasNext() ) {
				orderBy.append( ", " );
			}
		}
		return orderBy.toString();
	}

	public SessionFactoryImplementor getFactory() {
		return sessionFactory;
	}

	public String getSQLAlias(Criteria criteria) {
		return ( String ) criteriaSQLAliasMap.get( criteria );
	}

	public String getEntityName(Criteria criteria) {
		return ( String ) criteriaEntityNames.get( criteria );
	}

	public String getColumn(Criteria criteria, String propertyName) {
		String[] cols = getColumns( propertyName, criteria );
		if ( cols.length != 1 ) {
			throw new QueryException( "property does not map to a single column: " + propertyName );
		}
		return cols[0];
	}

	/**
	 * Get the names of the columns constrained
	 * by this criterion.
	 */
	public String[] getColumnsUsingProjection(
			Criteria subcriteria,
	        String propertyName) throws HibernateException {

		//first look for a reference to a projection alias
		final Projection projection = rootCriteria.getProjection();
		String[] projectionColumns = null;
		if ( projection != null ) {
			projectionColumns = ( projection instanceof EnhancedProjection ?
					( ( EnhancedProjection ) projection ).getColumnAliases( propertyName, 0, rootCriteria, this ) :
					projection.getColumnAliases( propertyName, 0 )
			);
		}
		if ( projectionColumns == null ) {
			//it does not refer to an alias of a projection,
			//look for a property
			try {
				return getColumns( propertyName, subcriteria );
			}
			catch ( HibernateException he ) {
				//not found in inner query , try the outer query
				if ( outerQueryTranslator != null ) {
					return outerQueryTranslator.getColumnsUsingProjection( subcriteria, propertyName );
				}
				else {
					throw he;
				}
			}
		}
		else {
			//it refers to an alias of a projection
			return projectionColumns;
		}
	}

	public String[] getIdentifierColumns(Criteria subcriteria) {
		String[] idcols =
				( ( Loadable ) getPropertyMapping( getEntityName( subcriteria ) ) ).getIdentifierColumnNames();
		return StringHelper.qualify( getSQLAlias( subcriteria ), idcols );
	}

	public Type getIdentifierType(Criteria subcriteria) {
		return ( ( Loadable ) getPropertyMapping( getEntityName( subcriteria ) ) ).getIdentifierType();
	}

	public TypedValue getTypedIdentifierValue(Criteria subcriteria, Object value) {
		final Loadable loadable = ( Loadable ) getPropertyMapping( getEntityName( subcriteria ) );
		return new TypedValue(
				loadable.getIdentifierType(),
		        value,
		        EntityMode.POJO
		);
	}

	public String[] getColumns(
			String propertyName,
	        Criteria subcriteria) throws HibernateException {
		return getPropertyMapping( getEntityName( subcriteria, propertyName ) )
				.toColumns(
						getSQLAlias( subcriteria, propertyName ),
				        getPropertyName( propertyName )
				);
	}

	/**
	 * Get the names of the columns mapped by a property path; if the
	 * property path is not found in subcriteria, try the "outer" query.
	 * Projection aliases are ignored.
	 */
	public String[] findColumns(String propertyName, Criteria subcriteria )
	throws HibernateException {
		try {
			return getColumns( propertyName, subcriteria );
		}
		catch ( HibernateException he ) {
			//not found in inner query, try the outer query
			if ( outerQueryTranslator != null ) {
				return outerQueryTranslator.findColumns( propertyName, subcriteria );
			}
			else {
				throw he;
			}
		}
	}

	public Type getTypeUsingProjection(Criteria subcriteria, String propertyName)
			throws HibernateException {

		//first look for a reference to a projection alias
		final Projection projection = rootCriteria.getProjection();
		Type[] projectionTypes = projection == null ?
		                         null :
		                         projection.getTypes( propertyName, subcriteria, this );

		if ( projectionTypes == null ) {
			try {
				//it does not refer to an alias of a projection,
				//look for a property
				return getType( subcriteria, propertyName );
			}
			catch ( HibernateException he ) {
				//not found in inner query , try the outer query
				if ( outerQueryTranslator != null ) {
					return outerQueryTranslator.getType( subcriteria, propertyName );
				}
				else {
					throw he;
				}
			}
		}
		else {
			if ( projectionTypes.length != 1 ) {
				//should never happen, i think
				throw new QueryException( "not a single-length projection: " + propertyName );
			}
			return projectionTypes[0];
		}
	}

	public Type getType(Criteria subcriteria, String propertyName)
			throws HibernateException {
		return getPropertyMapping( getEntityName( subcriteria, propertyName ) )
				.toType( getPropertyName( propertyName ) );
	}

	/**
	 * Get the a typed value for the given property value.
	 */
	public TypedValue getTypedValue(Criteria subcriteria, String propertyName, Object value)
			throws HibernateException {
		// Detect discriminator values...
		if ( value instanceof Class ) {
			Class entityClass = ( Class ) value;
			Queryable q = SessionFactoryHelper.findQueryableUsingImports( sessionFactory, entityClass.getName() );
			if ( q != null ) {
				Type type = q.getDiscriminatorType();
				String stringValue = q.getDiscriminatorSQLValue();
				if (stringValue != null && stringValue.length() > 2
						&& stringValue.startsWith("'")
						&& stringValue.endsWith("'")) { // remove the single
														// quotes
					stringValue = stringValue.substring(1,
							stringValue.length() - 1);
				}
				
				// Convert the string value into the proper type.
				if ( type instanceof StringRepresentableType ) {
					StringRepresentableType nullableType = (StringRepresentableType) type;
					value = nullableType.fromStringValue( stringValue );
				}
				else {
					throw new QueryException( "Unsupported discriminator type " + type );
				}
				return new TypedValue(
						type,
				        value,
				        EntityMode.POJO
				);
			}
		}
		// Otherwise, this is an ordinary value.
		return new TypedValue(
				getTypeUsingProjection( subcriteria, propertyName ),
		        value,
		        EntityMode.POJO
		);
	}

	private PropertyMapping getPropertyMapping(String entityName)
			throws MappingException {
		return ( PropertyMapping ) sessionFactory.getEntityPersister( entityName );
	}

	//TODO: use these in methods above

	public String getEntityName(Criteria subcriteria, String propertyName) {
		if ( propertyName.indexOf( '.' ) > 0 ) {
			String root = StringHelper.root( propertyName );
			Criteria crit = getAliasedCriteria( root );
			if ( crit != null ) {
				return getEntityName( crit );
			}
		}
		return getEntityName( subcriteria );
	}

	public String getSQLAlias(Criteria criteria, String propertyName) {
		if ( propertyName.indexOf( '.' ) > 0 ) {
			String root = StringHelper.root( propertyName );
			Criteria subcriteria = getAliasedCriteria( root );
			if ( subcriteria != null ) {
				return getSQLAlias( subcriteria );
			}
		}
		return getSQLAlias( criteria );
	}

	public String getPropertyName(String propertyName) {
		if ( propertyName.indexOf( '.' ) > 0 ) {
			String root = StringHelper.root( propertyName );
			Criteria crit = getAliasedCriteria( root );
			if ( crit != null ) {
				return propertyName.substring( root.length() + 1 );
			}
		}
		return propertyName;
	}

	public String getWithClause(String path)
	{
		final Criterion crit = (Criterion)this.withClauseMap.get(path);
		return crit == null ? null : crit.toSqlString(getCriteria(path), this);
	}

	public boolean hasRestriction(String path)
	{
		final CriteriaImpl.Subcriteria crit = ( CriteriaImpl.Subcriteria ) getCriteria( path );
		return crit == null ? false : crit.hasRestriction();
	}


}
