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

import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.id.Assigned;
import org.hibernate.intercept.LazyPropertyInitializer;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.property.Getter;
import org.hibernate.property.Setter;
import org.hibernate.proxy.ProxyFactory;
import org.hibernate.tuple.Instantiator;
import org.hibernate.tuple.StandardProperty;
import org.hibernate.tuple.VersionProperty;
import org.hibernate.type.AbstractComponentType;
import org.hibernate.type.ComponentType;


/**
 * Support for tuplizers relating to entities.
 *
 * @author Steve Ebersole
 * @author Gavin King
 */
public abstract class AbstractEntityTuplizer implements EntityTuplizer {

	//TODO: currently keeps Getters and Setters (instead of PropertyAccessors) because of the way getGetter() and getSetter() are implemented currently; yuck!

	private final EntityMetamodel entityMetamodel;

	private final Getter idGetter;
	private final Setter idSetter;

	protected final Getter[] getters;
	protected final Setter[] setters;
	protected final int propertySpan;
	protected final boolean hasCustomAccessors;
	private final Instantiator instantiator;
	private final ProxyFactory proxyFactory;
	private final AbstractComponentType identifierMapperType;


	/**
	 * Build an appropriate Getter for the given property.
	 *
	 * @param mappedProperty The property to be accessed via the built Getter.
	 * @param mappedEntity The entity information regarding the mapped entity owning this property.
	 * @return An appropriate Getter instance.
	 */
	protected abstract Getter buildPropertyGetter(Property mappedProperty, PersistentClass mappedEntity);

	/**
	 * Build an appropriate Setter for the given property.
	 *
	 * @param mappedProperty The property to be accessed via the built Setter.
	 * @param mappedEntity The entity information regarding the mapped entity owning this property.
	 * @return An appropriate Setter instance.
	 */
	protected abstract Setter buildPropertySetter(Property mappedProperty, PersistentClass mappedEntity);

	/**
	 * Build an appropriate Instantiator for the given mapped entity.
	 *
	 * @param mappingInfo The mapping information regarding the mapped entity.
	 * @return An appropriate Instantiator instance.
	 */
	protected abstract Instantiator buildInstantiator(PersistentClass mappingInfo);

	/**
	 * Build an appropriate ProxyFactory for the given mapped entity.
	 *
	 * @param mappingInfo The mapping information regarding the mapped entity.
	 * @param idGetter The constructed Getter relating to the entity's id property.
	 * @param idSetter The constructed Setter relating to the entity's id property.
	 * @return An appropriate ProxyFactory instance.
	 */
	protected abstract ProxyFactory buildProxyFactory(PersistentClass mappingInfo, Getter idGetter, Setter idSetter);

	/**
	 * Constructs a new AbstractEntityTuplizer instance.
	 *
	 * @param entityMetamodel The "interpreted" information relating to the mapped entity.
	 * @param mappingInfo The parsed "raw" mapping data relating to the given entity.
	 */
	public AbstractEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappingInfo) {
		this.entityMetamodel = entityMetamodel;

		if ( !entityMetamodel.getIdentifierProperty().isVirtual() ) {
			idGetter = buildPropertyGetter( mappingInfo.getIdentifierProperty(), mappingInfo );
			idSetter = buildPropertySetter( mappingInfo.getIdentifierProperty(), mappingInfo );
		}
		else {
			idGetter = null;
			idSetter = null;
		}

		propertySpan = entityMetamodel.getPropertySpan();

        getters = new Getter[propertySpan];
		setters = new Setter[propertySpan];

		Iterator iter = mappingInfo.getPropertyClosureIterator();
		boolean foundCustomAccessor=false;
		int i=0;
		while ( iter.hasNext() ) {
			//TODO: redesign how PropertyAccessors are acquired...
			Property property = (Property) iter.next();
			getters[i] = buildPropertyGetter(property, mappingInfo);
			setters[i] = buildPropertySetter(property, mappingInfo);
			if ( !property.isBasicPropertyAccessor() ) foundCustomAccessor = true;
			i++;
		}
		hasCustomAccessors = foundCustomAccessor;

        instantiator = buildInstantiator( mappingInfo );

		if ( entityMetamodel.isLazy() ) {
			proxyFactory = buildProxyFactory( mappingInfo, idGetter, idSetter );
			if (proxyFactory == null) {
				entityMetamodel.setLazy( false );
			}
		}
		else {
			proxyFactory = null;
		}
		
		Component mapper = mappingInfo.getIdentifierMapper();
		identifierMapperType = mapper==null ? null : (AbstractComponentType) mapper.getType();
	}

	/** Retreives the defined entity-name for the tuplized entity.
	 *
	 * @return The entity-name.
	 */
	protected String getEntityName() {
		return entityMetamodel.getName();
	}

	/**
	 * Retreives the defined entity-names for any subclasses defined for this
	 * entity.
	 *
	 * @return Any subclass entity-names.
	 */
	protected Set getSubclassEntityNames() {
		return entityMetamodel.getSubclassEntityNames();
	}

	public Serializable getIdentifier(Object entity) throws HibernateException {
		final Object id;
		if ( entityMetamodel.getIdentifierProperty().isEmbedded() ) {
			id = entity;
		}
		else {
			if ( idGetter == null ) {
				if (identifierMapperType==null) {
					throw new HibernateException( "The class has no identifier property: " + getEntityName() );
				}
				else {
					ComponentType copier = (ComponentType) entityMetamodel.getIdentifierProperty().getType();
					id = copier.instantiate( getEntityMode() );
					copier.setPropertyValues( id, identifierMapperType.getPropertyValues( entity, getEntityMode() ), getEntityMode() );
				}
			}
			else {
				id = idGetter.get( entity );
			}
		}

		try {
			return (Serializable) id;
		}
		catch ( ClassCastException cce ) {
			StringBuffer msg = new StringBuffer( "Identifier classes must be serializable. " );
			if ( id != null ) {
				msg.append( id.getClass().getName() + " is not serializable. " );
			}
			if ( cce.getMessage() != null ) {
				msg.append( cce.getMessage() );
			}
			throw new ClassCastException( msg.toString() );
		}
	}


	public void setIdentifier(Object entity, Serializable id) throws HibernateException {
		if ( entityMetamodel.getIdentifierProperty().isEmbedded() ) {
			if ( entity != id ) {
				AbstractComponentType copier = (AbstractComponentType) entityMetamodel.getIdentifierProperty().getType();
				copier.setPropertyValues( entity, copier.getPropertyValues( id, getEntityMode() ), getEntityMode() );
			}
		}
		else if ( idSetter != null ) {
			idSetter.set( entity, id, getFactory() );
		}
	}

	public void resetIdentifier(Object entity, Serializable currentId, Object currentVersion) {
		if ( entityMetamodel.getIdentifierProperty().getIdentifierGenerator() instanceof Assigned ) {
			//return currentId;
		}
		else {
			//reset the id
			Serializable result = entityMetamodel.getIdentifierProperty()
					.getUnsavedValue()
					.getDefaultValue( currentId );
			setIdentifier( entity, result );
			//reset the version
			VersionProperty versionProperty = entityMetamodel.getVersionProperty();
			if ( entityMetamodel.isVersioned() ) {
				setPropertyValue(
				        entity,
				        entityMetamodel.getVersionPropertyIndex(),
						versionProperty.getUnsavedValue().getDefaultValue( currentVersion )
					);
			}
			//return the id, so we can use it to reset the proxy id
			//return result;
		}
	}

	public Object getVersion(Object entity) throws HibernateException {
		if ( !entityMetamodel.isVersioned() ) return null;
		return getters[ entityMetamodel.getVersionPropertyIndex() ].get( entity );
	}

	protected boolean shouldGetAllProperties(Object entity) {
		return !hasUninitializedLazyProperties( entity );
	}

	public Object[] getPropertyValues(Object entity) throws HibernateException {
		boolean getAll = shouldGetAllProperties( entity );
		final int span = entityMetamodel.getPropertySpan();
		final Object[] result = new Object[span];

		for ( int j = 0; j < span; j++ ) {
			StandardProperty property = entityMetamodel.getProperties()[j];
			if ( getAll || !property.isLazy() ) {
				result[j] = getters[j].get( entity );
			}
			else {
				result[j] = LazyPropertyInitializer.UNFETCHED_PROPERTY;
			}
		}
		return result;
	}

	public Object[] getPropertyValuesToInsert(Object entity, Map mergeMap, SessionImplementor session) 
	throws HibernateException {
		final int span = entityMetamodel.getPropertySpan();
		final Object[] result = new Object[span];

		for ( int j = 0; j < span; j++ ) {
			result[j] = getters[j].getForInsert( entity, mergeMap, session );
		}
		return result;
	}

	public Object getPropertyValue(Object entity, int i) throws HibernateException {
		return getters[i].get( entity );
	}

	public Object getPropertyValue(Object entity, String propertyPath) throws HibernateException {
		
		int loc = propertyPath.indexOf('.');
		String basePropertyName = loc>0 ?
			propertyPath.substring(0, loc) : propertyPath;
			
		int index = entityMetamodel.getPropertyIndex( basePropertyName );
		Object baseValue = getPropertyValue( entity, index );
		if ( loc>0 ) {
			ComponentType type = (ComponentType) entityMetamodel.getPropertyTypes()[index];
			return getComponentValue( type, baseValue, propertyPath.substring(loc+1) );
		}
		else {
			return baseValue;
		}
	}

	/**
	 * Extract a component property value.
	 *
	 * @param type The component property types.
	 * @param component The component instance itself.
	 * @param propertyPath The property path for the property to be extracted.
	 * @return The property value extracted.
	 */
	protected Object getComponentValue(ComponentType type, Object component, String propertyPath) {
		int loc = propertyPath.indexOf('.');
		String basePropertyName = loc>0 ?
			propertyPath.substring(0, loc) : propertyPath;
		
		String[] propertyNames = type.getPropertyNames();
		int index=0;
		for ( ; index<propertyNames.length; index++ ) {
			if ( basePropertyName.equals( propertyNames[index] ) ) break;
		}
		if (index==propertyNames.length) {
			throw new MappingException( "component property not found: " + basePropertyName );
		}
		
		Object baseValue = type.getPropertyValue( component, index, getEntityMode() );
		
		if ( loc>0 ) {
			ComponentType subtype = (ComponentType) type.getSubtypes()[index];
			return getComponentValue( subtype, baseValue, propertyPath.substring(loc+1) );
		}
		else {
			return baseValue;
		}
		
	}

	public void setPropertyValues(Object entity, Object[] values) throws HibernateException {
		boolean setAll = !entityMetamodel.hasLazyProperties();

		for ( int j = 0; j < entityMetamodel.getPropertySpan(); j++ ) {
			if ( setAll || values[j] != LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
				setters[j].set( entity, values[j], getFactory() );
			}
		}
	}

	public void setPropertyValue(Object entity, int i, Object value) throws HibernateException {
		setters[i].set( entity, value, getFactory() );
	}

	public void setPropertyValue(Object entity, String propertyName, Object value) throws HibernateException {
		setters[ entityMetamodel.getPropertyIndex( propertyName ) ].set( entity, value, getFactory() );
	}

	public final Object instantiate(Serializable id) throws HibernateException {
		Object result = getInstantiator().instantiate( id );
		if ( id != null ) {
			setIdentifier( result, id );
		}
		return result;
	}

	public final Object instantiate() throws HibernateException {
		return instantiate( null );
	}

	public void afterInitialize(Object entity, boolean lazyPropertiesAreUnfetched, SessionImplementor session) {}

	public boolean hasUninitializedLazyProperties(Object entity) {
		// the default is to simply not lazy fetch properties for now...
		return false;
	}

	public final boolean isInstance(Object object) {
        return getInstantiator().isInstance( object );
	}

	public boolean hasProxy() {
		return entityMetamodel.isLazy();
	}

	public final Object createProxy(Serializable id, SessionImplementor session)
	throws HibernateException {
		return getProxyFactory().getProxy( id, session );
	}

	public boolean isLifecycleImplementor() {
		return false;
	}

	public boolean isValidatableImplementor() {
		return false;
	}
	
	protected final EntityMetamodel getEntityMetamodel() {
		return entityMetamodel;
	}

	protected final SessionFactoryImplementor getFactory() {
		return entityMetamodel.getSessionFactory();
	}

	protected final Instantiator getInstantiator() {
		return instantiator;
	}

	protected final ProxyFactory getProxyFactory() {
		return proxyFactory;
	}
	
	public String toString() {
		return getClass().getName() + '(' + getEntityMetamodel().getName() + ')';
	}

}
