/*
 * 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 java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.hibernate.AssertionFailure;
import org.hibernate.MappingException;
import org.hibernate.engine.internal.JoinSequence;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes;
import org.hibernate.hql.internal.ast.HqlSqlWalker;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.persister.entity.Queryable;
import org.hibernate.sql.JoinFragment;
import org.hibernate.sql.JoinType;
import org.hibernate.type.EntityType;

/**
 * @author Steve Ebersole
 */
public class EntityJoinFromElement extends FromElement {
	public EntityJoinFromElement(
			HqlSqlWalker walker,
			FromClause fromClause,
			EntityPersister entityPersister,
			JoinType joinType,
			boolean fetchProperties,
			String alias) {
		initialize( walker );

		final String tableName = ( (Joinable) entityPersister ).getTableName();
		final String tableAlias = fromClause.getAliasGenerator().createName( entityPersister.getEntityName() );

		final EntityType entityType = (EntityType) ( (Queryable) entityPersister ).getType();

		initializeEntity(
				fromClause,
				entityPersister.getEntityName(),
				entityPersister,
				entityType,
				alias,
				tableAlias
		);

		EntityJoinJoinSequenceImpl joinSequence = new EntityJoinJoinSequenceImpl(
				getSessionFactoryHelper().getFactory(),
				entityType,
				tableName,
				tableAlias,
				joinType

		);
		setJoinSequence( joinSequence );

		setAllPropertyFetch( fetchProperties );

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

		setType( HqlSqlTokenTypes.ENTITY_JOIN );
//		setType( HqlSqlTokenTypes.FROM_FRAGMENT );
		setText( tableName );
	}

	@Override
	public void setText(String s) {
		super.setText( s );
	}


	private static class EntityJoinJoinSequenceImpl extends JoinSequence {
		private final SessionFactoryImplementor factory;
		private final String entityTableText;
		private final String entityTableAlias;
		private final EntityType entityType;
		private final JoinType joinType;

		public EntityJoinJoinSequenceImpl(
				SessionFactoryImplementor factory,
				EntityType entityType,
				String entityTableText,
				String entityTableAlias,
				JoinType joinType) {
			super( factory );
			this.factory = factory;
			this.entityType = entityType;
			this.entityTableText = entityTableText;
			this.entityTableAlias = entityTableAlias;
			this.joinType = joinType;

			setUseThetaStyle( false );
		}

		/**
		 * Ugh!
		 */
		@Override
		public JoinFragment toJoinFragment(
				Map enabledFilters,
				boolean includeAllSubclassJoins,
				String withClauseFragment) throws MappingException {
			final String joinString;
			switch ( joinType ) {
				case INNER_JOIN:
					joinString = " inner join ";
					break;
				case LEFT_OUTER_JOIN:
					joinString = " left outer join ";
					break;
				case RIGHT_OUTER_JOIN:
					joinString = " right outer join ";
					break;
				case FULL_JOIN:
					joinString = " full outer join ";
					break;
				default:
					throw new AssertionFailure( "undefined join type" );
			}

			final StringBuilder buffer = new StringBuilder();
			final AbstractEntityPersister joinable = (AbstractEntityPersister) entityType.getAssociatedJoinable(factory);

			buffer.append( joinString );

			Set<String> treatAsDeclarations = getTreatAsDeclarations();
			final boolean include = includeAllSubclassJoins && isIncluded( entityTableAlias );
			String fromFragment = joinable.fromJoinFragment( entityTableAlias, true, include, treatAsDeclarations );
			String whereFragment = joinable.whereJoinFragment( entityTableAlias, true, include, treatAsDeclarations );

			// We need a table group only when having e.g. a left join of a polymorphic entity
			// fromFragment is empty if the entity is non-polymorphic
			// Inner joined entity joins can avoid using the table grouping since the condition can be moved to the where clause
			boolean renderTableGroup = !fromFragment.isEmpty() && joinType != JoinType.INNER_JOIN;

			if ( renderTableGroup ) {
				buffer.append( '(' );
			}

			buffer.append( entityTableText )
					.append( ' ' )
					.append( entityTableAlias );

			if ( renderTableGroup ) {
				buffer.append( fromFragment )
						.append( ')' );
			}

			buffer.append( " on " );

			final String filters = 	entityType.getOnCondition(
					entityTableAlias,
					factory,
					enabledFilters,
					Collections.<String>emptySet()
			);

			if ( fromFragment.isEmpty() || renderTableGroup ) {
				buffer.append( filters );
				if ( withClauseFragment != null ) {
					if ( StringHelper.isNotEmpty( filters ) ) {
						buffer.append( " and " );
					}
					buffer.append( withClauseFragment );
				}
			}
			else {
				// We know there is a fromFragment and that we shouldn't render a table group
				// This means the entity is polymorphic and the entity join is an inner join
				// We move the with clause stuff to the where clause but still need to have a valid condition
				buffer.append( "1=1" );
				buffer.append( fromFragment );

				// Proper capacity to avoid resizing
				StringBuilder whereBuffer = new StringBuilder(
						10
							+ whereFragment.length()
							+ filters.length()
							+ withClauseFragment.length()
				);
				whereBuffer.append(whereFragment);
				if ( !filters.isEmpty() ) {
					whereBuffer.append( " and " );
					whereBuffer.append( filters );
				}
				if ( !withClauseFragment.isEmpty() ) {
					whereBuffer.append( " and " );
					whereBuffer.append( withClauseFragment );
				}

				whereFragment = whereBuffer.toString();
			}

			return new EntityJoinJoinFragment( buffer.toString(), whereFragment );
		}

	}

	private static class EntityJoinJoinFragment extends JoinFragment {
		private final String fragmentString;
		private final String whereFragment;

		public EntityJoinJoinFragment(String fragmentString, String whereFragment) {
			this.fragmentString = fragmentString;
			this.whereFragment = whereFragment;
		}

		@Override
		public void addJoin(
				String tableName,
				String alias,
				String[] fkColumns,
				String[] pkColumns,
				JoinType joinType) {
		}

		@Override
		public void addJoin(
				String tableName,
				String alias,
				String[] fkColumns,
				String[] pkColumns,
				JoinType joinType,
				String on) {
		}

		@Override
		public void addCrossJoin(String tableName, String alias) {
		}

		@Override
		public void addJoins(String fromFragment, String whereFragment) {
		}

		@Override
		public String toFromFragmentString() {
			return fragmentString;
		}

		@Override
		public String toWhereFragmentString() {
			return whereFragment;
		}

		@Override
		public void addCondition(String alias, String[] fkColumns, String[] pkColumns) {

		}

		@Override
		public boolean addCondition(String condition) {
			return false;
		}

		@Override
		public JoinFragment copy() {
			return null;
		}

	}

	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	// n/a


	@Override
	protected void initializeComponentJoin(FromElementType elementType) {
	}

	@Override
	public void initializeCollection(FromClause fromClause, String classAlias, String tableAlias) {
	}

	@Override
	public String getCollectionSuffix() {
		return null;
	}

	@Override
	public void setCollectionSuffix(String suffix) {
	}
}
