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

import java.util.Iterator;

import org.hibernate.engine.internal.JoinHelper;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.persister.collection.QueryableCollection;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.persister.entity.OuterJoinLoadable;
import org.hibernate.persister.walking.spi.AssociationKey;
import org.hibernate.persister.walking.spi.AttributeDefinition;
import org.hibernate.persister.walking.spi.AttributeSource;
import org.hibernate.persister.walking.spi.CompositionDefinition;
import org.hibernate.tuple.AbstractNonIdentifierAttribute;
import org.hibernate.tuple.BaselineAttributeInformation;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.ForeignKeyDirection;
import org.hibernate.type.Type;

import static org.hibernate.engine.internal.JoinHelper.getLHSColumnNames;
import static org.hibernate.engine.internal.JoinHelper.getLHSTableName;
import static org.hibernate.engine.internal.JoinHelper.getRHSColumnNames;

/**
 * A base class for a composite, non-identifier attribute.
 *
 * @author Steve Ebersole
 */
public abstract class AbstractCompositionAttribute
		extends AbstractNonIdentifierAttribute
		implements CompositionDefinition {

	private final int columnStartPosition;

	protected AbstractCompositionAttribute(
			AttributeSource source,
			SessionFactoryImplementor sessionFactory,
			int entityBasedAttributeNumber,
			String attributeName,
			CompositeType attributeType,
			int columnStartPosition,
			BaselineAttributeInformation baselineInfo) {
		super( source, sessionFactory, entityBasedAttributeNumber, attributeName, attributeType, baselineInfo );
		this.columnStartPosition = columnStartPosition;
	}

	@Override
	public CompositeType getType() {
		return (CompositeType) super.getType();
	}

	@Override
	public Iterable<AttributeDefinition> getAttributes() {
		return new Iterable<AttributeDefinition>() {
			@Override
			public Iterator<AttributeDefinition> iterator() {
				return new Iterator<AttributeDefinition>() {
					private final int numberOfAttributes = getType().getSubtypes().length;
					private int currentSubAttributeNumber;
					private int currentColumnPosition = columnStartPosition;

					@Override
					public boolean hasNext() {
						return currentSubAttributeNumber < numberOfAttributes;
					}

					@Override
					public AttributeDefinition next() {
						final int subAttributeNumber = currentSubAttributeNumber;
						currentSubAttributeNumber++;

						final String name = getType().getPropertyNames()[subAttributeNumber];
						final Type type = getType().getSubtypes()[subAttributeNumber];

						int columnPosition = currentColumnPosition;
						currentColumnPosition += type.getColumnSpan( sessionFactory() );

						if ( type.isAssociationType() ) {
							// we build the association-key here because of the "goofiness" with 'currentColumnPosition'
							final AssociationKey associationKey;
							final AssociationType aType = (AssociationType) type;
							final Joinable joinable = aType.getAssociatedJoinable( sessionFactory() );

							if ( aType.isAnyType() ) {
								associationKey = new AssociationKey(
										JoinHelper.getLHSTableName(
												aType,
												attributeNumber(),
												(OuterJoinLoadable) locateOwningPersister()
										),
										JoinHelper.getLHSColumnNames(
												aType,
												attributeNumber(),
												columnPosition,
												(OuterJoinLoadable) locateOwningPersister(),
												sessionFactory()
										)
								);
							}
							else if ( aType.getForeignKeyDirection() == ForeignKeyDirection.FROM_PARENT ) {
								final String lhsTableName;
								final String[] lhsColumnNames;

								if ( joinable.isCollection() ) {
									final QueryableCollection collectionPersister = (QueryableCollection) joinable;
									lhsTableName = collectionPersister.getTableName();
									lhsColumnNames = collectionPersister.getElementColumnNames();
								}
								else {
									final OuterJoinLoadable entityPersister = (OuterJoinLoadable) locateOwningPersister();
									lhsTableName = getLHSTableName( aType, attributeNumber(), entityPersister );
									lhsColumnNames = getLHSColumnNames(
											aType,
											attributeNumber(),
											columnPosition,
											entityPersister,
											sessionFactory()
									);
								}
								associationKey = new AssociationKey( lhsTableName, lhsColumnNames );
							}
							else {
								associationKey = new AssociationKey(
										joinable.getTableName(),
										getRHSColumnNames( aType, sessionFactory() )
								);
							}

							final CompositeType cType = getType();
							final boolean nullable =
									cType.getPropertyNullability() == null ||
											cType.getPropertyNullability()[subAttributeNumber];

							return new CompositeBasedAssociationAttribute(
									AbstractCompositionAttribute.this,
									sessionFactory(),
									attributeNumber(),
									name,
									(AssociationType) type,
									new BaselineAttributeInformation.Builder()
											.setInsertable( AbstractCompositionAttribute.this.isInsertable() )
											.setUpdateable( AbstractCompositionAttribute.this.isUpdateable() )
											// todo : handle nested ValueGeneration strategies...
											//		disallow if our strategy != NEVER
											.setNullable( nullable )
											.setDirtyCheckable( true )
											.setVersionable( AbstractCompositionAttribute.this.isVersionable() )
											.setCascadeStyle( getType().getCascadeStyle( subAttributeNumber ) )
											.setFetchMode( getType().getFetchMode( subAttributeNumber ) )
											.createInformation(),
									subAttributeNumber,
									associationKey
							);
						}
						else if ( type.isComponentType() ) {
							return new CompositionBasedCompositionAttribute(
									AbstractCompositionAttribute.this,
									sessionFactory(),
									attributeNumber(),
									name,
									(CompositeType) type,
									columnPosition,
									new BaselineAttributeInformation.Builder()
											.setInsertable( AbstractCompositionAttribute.this.isInsertable() )
											.setUpdateable( AbstractCompositionAttribute.this.isUpdateable() )
											// todo : handle nested ValueGeneration strategies...
											//		disallow if our strategy != NEVER
											.setNullable( getType().getPropertyNullability()[subAttributeNumber] )
											.setDirtyCheckable( true )
											.setVersionable( AbstractCompositionAttribute.this.isVersionable() )
											.setCascadeStyle( getType().getCascadeStyle( subAttributeNumber ) )
											.setFetchMode( getType().getFetchMode( subAttributeNumber ) )
											.createInformation()
							);
						}
						else {
							final CompositeType cType = getType();
							final boolean nullable = cType.getPropertyNullability() == null || cType.getPropertyNullability()[subAttributeNumber];

							return new CompositeBasedBasicAttribute(
									AbstractCompositionAttribute.this,
									sessionFactory(),
									subAttributeNumber,
									name,
									type,
									new BaselineAttributeInformation.Builder()
											.setInsertable( AbstractCompositionAttribute.this.isInsertable() )
											.setUpdateable( AbstractCompositionAttribute.this.isUpdateable() )
											// todo : handle nested ValueGeneration strategies...
											//		disallow if our strategy != NEVER
											.setNullable( nullable )
											.setDirtyCheckable( true )
											.setVersionable( AbstractCompositionAttribute.this.isVersionable() )
											.setCascadeStyle( getType().getCascadeStyle( subAttributeNumber ) )
											.setFetchMode( getType().getFetchMode( subAttributeNumber ) )
											.createInformation()
							);
						}
					}

					@Override
					public void remove() {
						throw new UnsupportedOperationException( "Remove operation not supported here" );
					}
				};
			}
		};
	}

	protected abstract EntityPersister locateOwningPersister();

	@Override
	protected String loggableMetadata() {
		return super.loggableMetadata() + ",composition";
	}
}
