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

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.EntityMode;
import org.hibernate.FetchMode;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.PropertyNotFoundException;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.engine.jdbc.Size;
import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.Mapping;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.tuple.StandardProperty;
import org.hibernate.tuple.ValueGeneration;
import org.hibernate.tuple.component.ComponentMetamodel;
import org.hibernate.tuple.component.ComponentTuplizer;

/**
 * Handles "component" mappings
 *
 * @author Gavin King
 */
public class ComponentType extends AbstractType implements CompositeType, ProcedureParameterExtractionAware {

	private final TypeFactory.TypeScope typeScope;
	private final String[] propertyNames;
	private final Type[] propertyTypes;
	private final ValueGeneration[] propertyValueGenerationStrategies;
	private final boolean[] propertyNullability;
	protected final int propertySpan;
	private final CascadeStyle[] cascade;
	private final FetchMode[] joinedFetch;
	private final boolean isKey;
	private boolean hasNotNullProperty;
	private final boolean createEmptyCompositesEnabled;

	protected final EntityMode entityMode;
	protected final ComponentTuplizer componentTuplizer;

	public ComponentType(TypeFactory.TypeScope typeScope, ComponentMetamodel metamodel) {
		this.typeScope = typeScope;
		// for now, just "re-flatten" the metamodel since this is temporary stuff anyway (HHH-1907)
		this.isKey = metamodel.isKey();
		this.propertySpan = metamodel.getPropertySpan();
		this.propertyNames = new String[propertySpan];
		this.propertyTypes = new Type[propertySpan];
		this.propertyValueGenerationStrategies = new ValueGeneration[propertySpan];
		this.propertyNullability = new boolean[propertySpan];
		this.cascade = new CascadeStyle[propertySpan];
		this.joinedFetch = new FetchMode[propertySpan];

		for ( int i = 0; i < propertySpan; i++ ) {
			StandardProperty prop = metamodel.getProperty( i );
			this.propertyNames[i] = prop.getName();
			this.propertyTypes[i] = prop.getType();
			this.propertyNullability[i] = prop.isNullable();
			this.cascade[i] = prop.getCascadeStyle();
			this.joinedFetch[i] = prop.getFetchMode();
			if ( !prop.isNullable() ) {
				hasNotNullProperty = true;
			}
			this.propertyValueGenerationStrategies[i] = prop.getValueGenerationStrategy();
		}

		this.entityMode = metamodel.getEntityMode();
		this.componentTuplizer = metamodel.getComponentTuplizer();
		this.createEmptyCompositesEnabled = metamodel.isCreateEmptyCompositesEnabled();
	}

	public boolean isKey() {
		return isKey;
	}

	public EntityMode getEntityMode() {
		return entityMode;
	}

	public ComponentTuplizer getComponentTuplizer() {
		return componentTuplizer;
	}

	@Override
	public int getColumnSpan(Mapping mapping) throws MappingException {
		int span = 0;
		for ( int i = 0; i < propertySpan; i++ ) {
			span += propertyTypes[i].getColumnSpan( mapping );
		}
		return span;
	}

	@Override
	public int[] sqlTypes(Mapping mapping) throws MappingException {
		//Not called at runtime so doesn't matter if its slow :)
		int[] sqlTypes = new int[getColumnSpan( mapping )];
		int n = 0;
		for ( int i = 0; i < propertySpan; i++ ) {
			int[] subtypes = propertyTypes[i].sqlTypes( mapping );
			for ( int subtype : subtypes ) {
				sqlTypes[n++] = subtype;
			}
		}
		return sqlTypes;
	}

	@Override
	public Size[] dictatedSizes(Mapping mapping) throws MappingException {
		//Not called at runtime so doesn't matter if its slow :)
		final Size[] sizes = new Size[getColumnSpan( mapping )];
		int soFar = 0;
		for ( Type propertyType : propertyTypes ) {
			final Size[] propertySizes = propertyType.dictatedSizes( mapping );
			System.arraycopy( propertySizes, 0, sizes, soFar, propertySizes.length );
			soFar += propertySizes.length;
		}
		return sizes;
	}

	@Override
	public Size[] defaultSizes(Mapping mapping) throws MappingException {
		//Not called at runtime so doesn't matter if its slow :)
		final Size[] sizes = new Size[getColumnSpan( mapping )];
		int soFar = 0;
		for ( Type propertyType : propertyTypes ) {
			final Size[] propertySizes = propertyType.defaultSizes( mapping );
			System.arraycopy( propertySizes, 0, sizes, soFar, propertySizes.length );
			soFar += propertySizes.length;
		}
		return sizes;
	}


	@Override
	public final boolean isComponentType() {
		return true;
	}

	public Class getReturnedClass() {
		return componentTuplizer.getMappedClass();
	}

	@Override
	public boolean isSame(Object x, Object y) throws HibernateException {
		if ( x == y ) {
			return true;
		}
		// null value and empty component are considered equivalent
		Object[] xvalues = getPropertyValues( x, entityMode );
		Object[] yvalues = getPropertyValues( y, entityMode );
		for ( int i = 0; i < propertySpan; i++ ) {
			if ( !propertyTypes[i].isSame( xvalues[i], yvalues[i] ) ) {
				return false;
			}
		}
		return true;
	}

	@Override
	public boolean isEqual(final Object x, final Object y) throws HibernateException {
		if ( x == y ) {
			return true;
		}
		// null value and empty component are considered equivalent
		for ( int i = 0; i < propertySpan; i++ ) {
			if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ) ) ) {
				return false;
			}
		}
		return true;
	}

	@Override
	public boolean isEqual(final Object x, final Object y, final SessionFactoryImplementor factory)
			throws HibernateException {
		if ( x == y ) {
			return true;
		}
		// null value and empty component are considered equivalent
		for ( int i = 0; i < propertySpan; i++ ) {
			if ( !propertyTypes[i].isEqual( getPropertyValue( x, i ), getPropertyValue( y, i ), factory ) ) {
				return false;
			}
		}
		return true;
	}

	@Override
	public int compare(final Object x, final Object y) {
		if ( x == y ) {
			return 0;
		}
		for ( int i = 0; i < propertySpan; i++ ) {
			int propertyCompare = propertyTypes[i].compare( getPropertyValue( x, i ), getPropertyValue( y, i ) );
			if ( propertyCompare != 0 ) {
				return propertyCompare;
			}
		}
		return 0;
	}

	public boolean isMethodOf(Method method) {
		return false;
	}

	@Override
	public int getHashCode(final Object x) {
		int result = 17;
		for ( int i = 0; i < propertySpan; i++ ) {
			Object y = getPropertyValue( x, i );
			result *= 37;
			if ( y != null ) {
				result += propertyTypes[i].getHashCode( y );
			}
		}
		return result;
	}

	@Override
	public int getHashCode(final Object x, final SessionFactoryImplementor factory) {
		int result = 17;
		for ( int i = 0; i < propertySpan; i++ ) {
			Object y = getPropertyValue( x, i );
			result *= 37;
			if ( y != null ) {
				result += propertyTypes[i].getHashCode( y, factory );
			}
		}
		return result;
	}

	@Override
	public boolean isDirty(final Object x, final Object y, final SharedSessionContractImplementor session) throws HibernateException {
		if ( x == y ) {
			return false;
		}
		// null value and empty component are considered equivalent
		for ( int i = 0; i < propertySpan; i++ ) {
			if ( propertyTypes[i].isDirty( getPropertyValue( x, i ), getPropertyValue( y, i ), session ) ) {
				return true;
			}
		}
		return false;
	}

	public boolean isDirty(final Object x, final Object y, final boolean[] checkable, final SharedSessionContractImplementor session)
			throws HibernateException {
		if ( x == y ) {
			return false;
		}
		// null value and empty component are considered equivalent
		int loc = 0;
		for ( int i = 0; i < propertySpan; i++ ) {
			int len = propertyTypes[i].getColumnSpan( session.getFactory() );
			if ( len <= 1 ) {
				final boolean dirty = ( len == 0 || checkable[loc] ) &&
						propertyTypes[i].isDirty( getPropertyValue( x, i ), getPropertyValue( y, i ), session );
				if ( dirty ) {
					return true;
				}
			}
			else {
				boolean[] subcheckable = new boolean[len];
				System.arraycopy( checkable, loc, subcheckable, 0, len );
				final boolean dirty = propertyTypes[i].isDirty(
						getPropertyValue( x, i ),
						getPropertyValue( y, i ),
						subcheckable,
						session
				);
				if ( dirty ) {
					return true;
				}
			}
			loc += len;
		}
		return false;
	}

	@Override
	public boolean isModified(
			final Object old,
			final Object current,
			final boolean[] checkable,
			final SharedSessionContractImplementor session) throws HibernateException {
		if ( old == current ) {
			return false;
		}
		// null value and empty components are considered equivalent
		int loc = 0;
		for ( int i = 0; i < propertySpan; i++ ) {
			int len = propertyTypes[i].getColumnSpan( session.getFactory() );
			boolean[] subcheckable = new boolean[len];
			System.arraycopy( checkable, loc, subcheckable, 0, len );
			if ( propertyTypes[i].isModified( getPropertyValue( old, i ), getPropertyValue( current, i ), subcheckable, session ) ) {
				return true;
			}
			loc += len;
		}
		return false;

	}

	@Override
	public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
			throws HibernateException, SQLException {
		return resolve( hydrate( rs, names, session, owner ), session, owner );
	}

	@Override
	public void nullSafeSet(PreparedStatement st, Object value, int begin, SharedSessionContractImplementor session)
			throws HibernateException, SQLException {

		Object[] subvalues = nullSafeGetValues( value, entityMode );

		for ( int i = 0; i < propertySpan; i++ ) {
			propertyTypes[i].nullSafeSet( st, subvalues[i], begin, session );
			begin += propertyTypes[i].getColumnSpan( session.getFactory() );
		}
	}

	@Override
	public void nullSafeSet(
			PreparedStatement st,
			Object value,
			int begin,
			boolean[] settable,
			SharedSessionContractImplementor session)
			throws HibernateException, SQLException {

		Object[] subvalues = nullSafeGetValues( value, entityMode );

		int loc = 0;
		for ( int i = 0; i < propertySpan; i++ ) {
			int len = propertyTypes[i].getColumnSpan( session.getFactory() );
			//noinspection StatementWithEmptyBody
			if ( len == 0 ) {
				//noop
			}
			else if ( len == 1 ) {
				if ( settable[loc] ) {
					propertyTypes[i].nullSafeSet( st, subvalues[i], begin, session );
					begin++;
				}
			}
			else {
				boolean[] subsettable = new boolean[len];
				System.arraycopy( settable, loc, subsettable, 0, len );
				propertyTypes[i].nullSafeSet( st, subvalues[i], begin, subsettable, session );
				begin += ArrayHelper.countTrue( subsettable );
			}
			loc += len;
		}
	}

	private Object[] nullSafeGetValues(Object value, EntityMode entityMode) throws HibernateException {
		if ( value == null ) {
			return new Object[propertySpan];
		}
		else {
			return getPropertyValues( value, entityMode );
		}
	}

	@Override
	public Object nullSafeGet(ResultSet rs, String name, SharedSessionContractImplementor session, Object owner)
			throws HibernateException, SQLException {

		return nullSafeGet( rs, new String[] {name}, session, owner );
	}

	@Override
	public Object getPropertyValue(Object component, int i, SharedSessionContractImplementor session)
			throws HibernateException {
		return getPropertyValue( component, i );
	}

	public Object getPropertyValue(Object component, int i, EntityMode entityMode)
			throws HibernateException {
		return getPropertyValue( component, i );
	}

	public Object getPropertyValue(Object component, int i)
			throws HibernateException {
		if (component == null) {
			component = new Object[propertySpan];
		}
		if ( component instanceof Object[] ) {
			// A few calls to hashCode pass the property values already in an
			// Object[] (ex: QueryKey hash codes for cached queries).
			// It's easiest to just check for the condition here prior to
			// trying reflection.
			return ( (Object[]) component )[i];
		}
		else {
			return componentTuplizer.getPropertyValue( component, i );
		}
	}

	@Override
	public Object[] getPropertyValues(Object component, SharedSessionContractImplementor session)
			throws HibernateException {
		return getPropertyValues( component, entityMode );
	}

	@Override
	public Object[] getPropertyValues(Object component, EntityMode entityMode)
			throws HibernateException {
		if (component == null) {
			component = new Object[propertySpan];
		}
		if ( component instanceof Object[] ) {
			// A few calls to hashCode pass the property values already in an 
			// Object[] (ex: QueryKey hash codes for cached queries).
			// It's easiest to just check for the condition here prior to
			// trying reflection.
			return (Object[]) component;
		}
		else {
			return componentTuplizer.getPropertyValues( component );
		}
	}

	@Override
	public void setPropertyValues(Object component, Object[] values, EntityMode entityMode)
			throws HibernateException {
		componentTuplizer.setPropertyValues( component, values );
	}

	@Override
	public Type[] getSubtypes() {
		return propertyTypes;
	}

	public ValueGeneration[] getPropertyValueGenerationStrategies() {
		return propertyValueGenerationStrategies;
	}

	@Override
	public String getName() {
		return "component" + ArrayHelper.toString( propertyNames );
	}

	@Override
	public String toLoggableString(Object value, SessionFactoryImplementor factory)
			throws HibernateException {
		if ( value == null ) {
			return "null";
		}

		if ( entityMode == null ) {
			throw new ClassCastException( value.getClass().getName() );
		}
		Map<String, String> result = new HashMap<>();
		Object[] values = getPropertyValues( value, entityMode );
		for ( int i = 0; i < propertyTypes.length; i++ ) {
			if ( values[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
				result.put( propertyNames[i], "<uninitialized>" );
			}
			else {
				result.put( propertyNames[i], propertyTypes[i].toLoggableString( values[i], factory ) );
			}
		}
		return StringHelper.unqualify( getName() ) + result.toString();
	}

	@Override
	public String[] getPropertyNames() {
		return propertyNames;
	}

	@Override
	public Object deepCopy(Object component, SessionFactoryImplementor factory)
			throws HibernateException {
		if ( component == null ) {
			return null;
		}

		Object[] values = getPropertyValues( component, entityMode );
		for ( int i = 0; i < propertySpan; i++ ) {
			values[i] = propertyTypes[i].deepCopy( values[i], factory );
		}

		Object result = instantiate( entityMode );
		setPropertyValues( result, values, entityMode );

		//not absolutely necessary, but helps for some
		//equals()/hashCode() implementations
		if ( componentTuplizer.hasParentProperty() ) {
			componentTuplizer.setParent( result, componentTuplizer.getParent( component ), factory );
		}

		return result;
	}

	@Override
	public Object replace(
			Object original,
			Object target,
			SharedSessionContractImplementor session,
			Object owner,
			Map copyCache)
			throws HibernateException {

		if ( original == null ) {
			return null;
		}
		//if ( original == target ) return target;

		final Object result = target == null
				? instantiate( owner, session )
				: target;

		Object[] values = TypeHelper.replace(
				getPropertyValues( original, entityMode ),
				getPropertyValues( result, entityMode ),
				propertyTypes,
				session,
				owner,
				copyCache
		);

		setPropertyValues( result, values, entityMode );
		return result;
	}

	@Override
	public Object replace(
			Object original,
			Object target,
			SharedSessionContractImplementor session,
			Object owner,
			Map copyCache,
			ForeignKeyDirection foreignKeyDirection)
			throws HibernateException {

		if ( original == null ) {
			return null;
		}
		//if ( original == target ) return target;

		final Object result = target == null ?
				instantiate( owner, session ) :
				target;

		Object[] values = TypeHelper.replace(
				getPropertyValues( original, entityMode ),
				getPropertyValues( result, entityMode ),
				propertyTypes,
				session,
				owner,
				copyCache,
				foreignKeyDirection
		);

		setPropertyValues( result, values, entityMode );
		return result;
	}

	/**
	 * This method does not populate the component parent
	 */
	public Object instantiate(EntityMode entityMode) throws HibernateException {
		return componentTuplizer.instantiate();
	}

	public Object instantiate(Object parent, SharedSessionContractImplementor session)
			throws HibernateException {

		Object result = instantiate( entityMode );

		if ( componentTuplizer.hasParentProperty() && parent != null ) {
			componentTuplizer.setParent(
					result,
					session.getPersistenceContext().proxyFor( parent ),
					session.getFactory()
			);
		}

		return result;
	}

	@Override
	public CascadeStyle getCascadeStyle(int i) {
		return cascade[i];
	}

	@Override
	public boolean isMutable() {
		return true;
	}

	@Override
	public Serializable disassemble(Object value, SharedSessionContractImplementor session, Object owner)
			throws HibernateException {

		if ( value == null ) {
			return null;
		}
		else {
			Object[] values = getPropertyValues( value, entityMode );
			for ( int i = 0; i < propertyTypes.length; i++ ) {
				values[i] = propertyTypes[i].disassemble( values[i], session, owner );
			}
			return values;
		}
	}

	@Override
	public Object assemble(Serializable object, SharedSessionContractImplementor session, Object owner)
			throws HibernateException {

		if ( object == null ) {
			return null;
		}
		else {
			Object[] values = (Object[]) object;
			Object[] assembled = new Object[values.length];
			for ( int i = 0; i < propertyTypes.length; i++ ) {
				assembled[i] = propertyTypes[i].assemble( (Serializable) values[i], session, owner );
			}
			Object result = instantiate( owner, session );
			setPropertyValues( result, assembled, entityMode );
			return result;
		}
	}

	@Override
	public FetchMode getFetchMode(int i) {
		return joinedFetch[i];
	}

	@Override
	public Object hydrate(
			final ResultSet rs,
			final String[] names,
			final SharedSessionContractImplementor session,
			final Object owner)
			throws HibernateException, SQLException {

		int begin = 0;
		boolean notNull = false;
		Object[] values = new Object[propertySpan];
		for ( int i = 0; i < propertySpan; i++ ) {
			int length = propertyTypes[i].getColumnSpan( session.getFactory() );
			String[] range = ArrayHelper.slice( names, begin, length ); //cache this
			Object val = propertyTypes[i].hydrate( rs, range, session, owner );
			if ( val == null ) {
				if ( isKey ) {
					return null; //different nullability rules for pk/fk
				}
			}
			else {
				notNull = true;
			}
			values[i] = val;
			begin += length;
		}

		return notNull ? values : null;
	}

	@Override
	public Object resolve(Object value, SharedSessionContractImplementor session, Object owner)
			throws HibernateException {

		if ( value != null ) {
			Object result = instantiate( owner, session );
			Object[] values = (Object[]) value;
			Object[] resolvedValues = new Object[values.length]; //only really need new array during semiresolve!
			for ( int i = 0; i < values.length; i++ ) {
				resolvedValues[i] = propertyTypes[i].resolve( values[i], session, owner );
			}
			setPropertyValues( result, resolvedValues, entityMode );
			return result;
		}
		else if ( isCreateEmptyCompositesEnabled() ) {
			return instantiate( owner, session );
		}
		else {
			return null;
		}
	}

	@Override
	public Object semiResolve(Object value, SharedSessionContractImplementor session, Object owner)
			throws HibernateException {
		//note that this implementation is kinda broken
		//for components with many-to-one associations
		return resolve( value, session, owner );
	}

	@Override
	public boolean[] getPropertyNullability() {
		return propertyNullability;
	}

	@Override
	public boolean[] toColumnNullness(Object value, Mapping mapping) {
		boolean[] result = new boolean[getColumnSpan( mapping )];
		if ( value == null ) {
			return result;
		}
		Object[] values = getPropertyValues( value, EntityMode.POJO ); //TODO!!!!!!!
		int loc = 0;
		for ( int i = 0; i < propertyTypes.length; i++ ) {
			boolean[] propertyNullness = propertyTypes[i].toColumnNullness( values[i], mapping );
			System.arraycopy( propertyNullness, 0, result, loc, propertyNullness.length );
			loc += propertyNullness.length;
		}
		return result;
	}

	@Override
	public boolean isEmbedded() {
		return false;
	}

	@Override
	public int getPropertyIndex(String name) {
		String[] names = getPropertyNames();
		for ( int i = 0, max = names.length; i < max; i++ ) {
			if ( names[i].equals( name ) ) {
				return i;
			}
		}
		throw new PropertyNotFoundException(
				"Unable to locate property named " + name + " on " + getReturnedClass().getName()
		);
	}

	private Boolean canDoExtraction;

	@Override
	public boolean canDoExtraction() {
		if ( canDoExtraction == null ) {
			canDoExtraction = determineIfProcedureParamExtractionCanBePerformed();
		}
		return canDoExtraction;
	}

	private boolean determineIfProcedureParamExtractionCanBePerformed() {
		for ( Type propertyType : propertyTypes ) {
			if ( !ProcedureParameterExtractionAware.class.isInstance( propertyType ) ) {
				return false;
			}
			if ( !( (ProcedureParameterExtractionAware) propertyType ).canDoExtraction() ) {
				return false;
			}
		}
		return true;
	}

	@Override
	public Object extract(CallableStatement statement, int startIndex, SharedSessionContractImplementor session) throws SQLException {
		Object[] values = new Object[propertySpan];

		int currentIndex = startIndex;
		boolean notNull = false;
		for ( int i = 0; i < propertySpan; i++ ) {
			// we know this cast is safe from canDoExtraction
			final Type propertyType = propertyTypes[i];
			final Object value = ((ProcedureParameterExtractionAware) propertyType).extract(
					statement,
					currentIndex,
					session
			);
			if ( value == null ) {
				if ( isKey ) {
					return null; //different nullability rules for pk/fk
				}
			}
			else {
				notNull = true;
			}
			values[i] = value;
			currentIndex += propertyType.getColumnSpan( session.getFactory() );
		}

		if ( !notNull ) {
			values = null;
		}

		return resolve( values, session, null );
	}

	@Override
	public Object extract(CallableStatement statement, String[] paramNames, SharedSessionContractImplementor session)
			throws SQLException {
		// for this form to work all sub-property spans must be one (1)...

		Object[] values = new Object[propertySpan];

		int indx = 0;
		boolean notNull = false;
		for ( String paramName : paramNames ) {
			// we know this cast is safe from canDoExtraction
			final ProcedureParameterExtractionAware propertyType = (ProcedureParameterExtractionAware) propertyTypes[indx];
			final Object value = propertyType.extract( statement, new String[] {paramName}, session );
			if ( value == null ) {
				if ( isKey ) {
					return null; //different nullability rules for pk/fk
				}
			}
			else {
				notNull = true;
			}
			values[indx] = value;
		}

		if ( !notNull ) {
			values = null;
		}

		return resolve( values, session, null );
	}

	@Override
	public boolean hasNotNullProperty() {
		return hasNotNullProperty;
	}

	private boolean isCreateEmptyCompositesEnabled() {
		return createEmptyCompositesEnabled;
	}
}