/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat Inc. 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 Inc.
 *
 * 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.type;

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

import org.dom4j.Node;
import org.hibernate.EntityMode;
import org.hibernate.FetchMode;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.TransientObjectException;
import org.hibernate.engine.CascadeStyle;
import org.hibernate.engine.ForeignKeys;
import org.hibernate.engine.Mapping;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.proxy.HibernateProxyHelper;
import org.hibernate.util.ArrayHelper;

/**
 * Handles "any" mappings
 * 
 * @author Gavin King
 */
public class AnyType extends AbstractType implements CompositeType, AssociationType {
	private final Type identifierType;
	private final Type metaType;

	public AnyType(Type metaType, Type identifierType) {
		this.identifierType = identifierType;
		this.metaType = metaType;
	}

	public Object deepCopy(Object value, EntityMode entityMode, SessionFactoryImplementor factory)
	throws HibernateException {
		return value;
	}
	
	public boolean isMethodOf(Method method) {
		return false;
	}

	public boolean isSame(Object x, Object y, EntityMode entityMode) throws HibernateException {
		return x==y;
	}

	public int compare(Object x, Object y, EntityMode entityMode) {
		return 0; //TODO: entities CAN be compared, by PK and entity name, fix this!
	}

	public int getColumnSpan(Mapping session)
	throws MappingException {
		return 2;
	}

	public String getName() {
		return "object";
	}

	public boolean isMutable() {
		return false;
	}

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

		throw new UnsupportedOperationException("object is a multicolumn type");
	}

	public Object nullSafeGet(ResultSet rs,	String[] names,	SessionImplementor session,	Object owner)
	throws HibernateException, SQLException {
		return resolveAny(
				(String) metaType.nullSafeGet(rs, names[0], session, owner),
				(Serializable) identifierType.nullSafeGet(rs, names[1], session, owner),
				session
			);
	}

	public Object hydrate(ResultSet rs,	String[] names,	SessionImplementor session,	Object owner)
	throws HibernateException, SQLException {
		String entityName = (String) metaType.nullSafeGet(rs, names[0], session, owner);
		Serializable id = (Serializable) identifierType.nullSafeGet(rs, names[1], session, owner);
		return new ObjectTypeCacheEntry(entityName, id);
	}

	public Object resolve(Object value, SessionImplementor session, Object owner)
	throws HibernateException {
		ObjectTypeCacheEntry holder = (ObjectTypeCacheEntry) value;
		return resolveAny(holder.entityName, holder.id, session);
	}

	public Object semiResolve(Object value, SessionImplementor session, Object owner)
	throws HibernateException {
		throw new UnsupportedOperationException("any mappings may not form part of a property-ref");
	}
	
	private Object resolveAny(String entityName, Serializable id, SessionImplementor session)
	throws HibernateException {
		return entityName==null || id==null ?
				null : session.internalLoad( entityName, id, false, false );
	}

	public void nullSafeSet(PreparedStatement st, Object value,	int index, SessionImplementor session)
	throws HibernateException, SQLException {
		nullSafeSet(st, value, index, null, session);
	}
	
	public void nullSafeSet(PreparedStatement st, Object value,	int index, boolean[] settable, SessionImplementor session)
	throws HibernateException, SQLException {

		Serializable id;
		String entityName;
		if (value==null) {
			id=null;
			entityName=null;
		}
		else {
			entityName = session.bestGuessEntityName(value);
			id = ForeignKeys.getEntityIdentifierIfNotUnsaved(entityName, value, session);
		}
		
		// metaType is assumed to be single-column type
		if ( settable==null || settable[0] ) {
			metaType.nullSafeSet(st, entityName, index, session);
		}
		if (settable==null) {
			identifierType.nullSafeSet(st, id, index+1, session);
		}
		else {
			boolean[] idsettable = new boolean[ settable.length-1 ];
			System.arraycopy(settable, 1, idsettable, 0, idsettable.length);
			identifierType.nullSafeSet(st, id, index+1, idsettable, session);
		}
	}

	public Class getReturnedClass() {
		return Object.class;
	}

	public int[] sqlTypes(Mapping mapping) throws MappingException {
		return ArrayHelper.join(
				metaType.sqlTypes(mapping),
				identifierType.sqlTypes(mapping)
			);
	}

	public void setToXMLNode(Node xml, Object value, SessionFactoryImplementor factory) {
		throw new UnsupportedOperationException("any types cannot be stringified");
	}

	public String toLoggableString(Object value, SessionFactoryImplementor factory) 
	throws HibernateException {
		//TODO: terrible implementation!
		return value==null ?
				"null" :
				Hibernate.entity( HibernateProxyHelper.getClassWithoutInitializingProxy(value) )
						.toLoggableString(value, factory);
	}

	public Object fromXMLNode(Node xml, Mapping factory) throws HibernateException {
		throw new UnsupportedOperationException(); //TODO: is this right??
	}

	public static final class ObjectTypeCacheEntry implements Serializable {
		String entityName;
		Serializable id;
		ObjectTypeCacheEntry(String entityName, Serializable id) {
			this.entityName = entityName;
			this.id = id;
		}
	}

	public Object assemble(
		Serializable cached,
		SessionImplementor session,
		Object owner)
	throws HibernateException {

		ObjectTypeCacheEntry e = (ObjectTypeCacheEntry) cached;
		return e==null ? null : session.internalLoad(e.entityName, e.id, false, false);
	}

	public Serializable disassemble(Object value, SessionImplementor session, Object owner)
	throws HibernateException {
		return value==null ?
			null :
			new ObjectTypeCacheEntry(
						session.bestGuessEntityName(value),
						ForeignKeys.getEntityIdentifierIfNotUnsaved( 
								session.bestGuessEntityName(value), value, session 
							)
					);
	}

	public boolean isAnyType() {
		return true;
	}

	public Object replace(
			Object original, 
			Object target,
			SessionImplementor session, 
			Object owner, 
			Map copyCache)
	throws HibernateException {
		if (original==null) {
			return null;
		}
		else {
			String entityName = session.bestGuessEntityName(original);
			Serializable id = ForeignKeys.getEntityIdentifierIfNotUnsaved( 
					entityName, 
					original, 
					session 
				);
			return session.internalLoad( 
					entityName, 
					id, 
					false, 
					false
				);
		}
	}
	public CascadeStyle getCascadeStyle(int i) {
		return CascadeStyle.NONE;
	}

	public FetchMode getFetchMode(int i) {
		return FetchMode.SELECT;
	}

	private static final String[] PROPERTY_NAMES = new String[] { "class", "id" };

	public String[] getPropertyNames() {
		return PROPERTY_NAMES;
	}

	public Object getPropertyValue(Object component, int i, SessionImplementor session)
		throws HibernateException {

		return i==0 ?
				session.bestGuessEntityName(component) :
				getIdentifier(component, session);
	}

	public Object[] getPropertyValues(Object component, SessionImplementor session)
		throws HibernateException {

		return new Object[] { session.bestGuessEntityName(component), getIdentifier(component, session) };
	}

	private Serializable getIdentifier(Object value, SessionImplementor session) throws HibernateException {
		try {
			return ForeignKeys.getEntityIdentifierIfNotUnsaved( session.bestGuessEntityName(value), value, session );
		}
		catch (TransientObjectException toe) {
			return null;
		}
	}

	public Type[] getSubtypes() {
		return new Type[] { metaType, identifierType };
	}

	public void setPropertyValues(Object component, Object[] values, EntityMode entityMode)
		throws HibernateException {

		throw new UnsupportedOperationException();

	}

	public Object[] getPropertyValues(Object component, EntityMode entityMode) {
		throw new UnsupportedOperationException();
	}

	public boolean isComponentType() {
		return true;
	}

	public ForeignKeyDirection getForeignKeyDirection() {
		//return AssociationType.FOREIGN_KEY_TO_PARENT; //this is better but causes a transient object exception...
		return ForeignKeyDirection.FOREIGN_KEY_FROM_PARENT;
	}

	public boolean isAssociationType() {
		return true;
	}

	public boolean useLHSPrimaryKey() {
		return false;
	}

	public Joinable getAssociatedJoinable(SessionFactoryImplementor factory) {
		throw new UnsupportedOperationException("any types do not have a unique referenced persister");
	}

	public boolean isModified(Object old, Object current, boolean[] checkable, SessionImplementor session)
	throws HibernateException {
		if (current==null) return old!=null;
		if (old==null) return current!=null;
		ObjectTypeCacheEntry holder = (ObjectTypeCacheEntry) old;
		boolean[] idcheckable = new boolean[checkable.length-1];
		System.arraycopy(checkable, 1, idcheckable, 0, idcheckable.length);
		return ( checkable[0] && !holder.entityName.equals( session.bestGuessEntityName(current) ) ) ||
				identifierType.isModified(holder.id, getIdentifier(current, session), idcheckable, session);
	}

	public String getAssociatedEntityName(SessionFactoryImplementor factory)
		throws MappingException {
		throw new UnsupportedOperationException("any types do not have a unique referenced persister");
	}
	
	public boolean[] getPropertyNullability() {
		return null;
	}

	public String getOnCondition(String alias, SessionFactoryImplementor factory, Map enabledFilters)
	throws MappingException {
		throw new UnsupportedOperationException();
	}
	
	public boolean isReferenceToPrimaryKey() {
		return true;
	}
	
	public String getRHSUniqueKeyPropertyName() {
		return null;
	}

	public String getLHSPropertyName() {
		return null;
	}

	public boolean isAlwaysDirtyChecked() {
		return false;
	}

	public boolean isEmbeddedInXML() {
		return false;
	}
	
	public boolean[] toColumnNullness(Object value, Mapping mapping) {
		boolean[] result = new boolean[ getColumnSpan(mapping) ];
		if (value!=null) Arrays.fill(result, true);
		return result;
	}

	public boolean isDirty(Object old, Object current, boolean[] checkable, SessionImplementor session) 
	throws HibernateException {
		//TODO!!!
		return isDirty(old, current, session);
	}

	public boolean isEmbedded() {
		return false;
	}
}
