//$Id: Cascade.java 7652 2005-07-26 05:51:47Z oneovthafew $
package brn.distsim.ormapper.util;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.engine.CascadeStyle;
import org.hibernate.engine.CascadingAction;
import org.hibernate.engine.CollectionEntry;
import org.hibernate.event.EventSource;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.AbstractComponentType;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.EntityType;
import org.hibernate.type.SetType;
import org.hibernate.type.Type;
import org.hibernate.util.CollectionHelper;

/**
 * Implements cascaded save / delete / update / lock / evict / replicate /
 * persist / merge
 *
 * @see org.hibernate.type.AssociationType
 * @author Gavin King
 */
public final class CascadeMap {

	private int cascadeTo;

	private EventSource eventSource;

	private CascadingAction action;

	public CascadeMap(final CascadingAction action, final int cascadeTo,
			final EventSource eventSource) {
		this.cascadeTo = cascadeTo;
		this.eventSource = eventSource;
		this.action = action;
	}

	private static final Log log = LogFactory.getLog(CascadeMap.class);

	// The available cascade actions:

	/**
	 * A cascade point that occurs just after the insertion of the parent entity
	 * and just before deletion
	 */
	public static final int AFTER_INSERT_BEFORE_DELETE = 1;

	/**
	 * A cascade point that occurs just before the insertion of the parent
	 * entity and just after deletion
	 */
	public static final int BEFORE_INSERT_AFTER_DELETE = 2;

	/**
	 * A cascade point that occurs just after the insertion of the parent entity
	 * and just before deletion, inside a collection
	 */
	public static final int AFTER_INSERT_BEFORE_DELETE_VIA_COLLECTION = 3;

	/**
	 * A cascade point that occurs just after update of the parent entity
	 */
	public static final int AFTER_UPDATE = 0;

	/**
	 * A cascade point that occurs just before the session is flushed
	 */
	public static final int BEFORE_FLUSH = 0;

	/**
	 * A cascade point that occurs just after eviction of the parent entity from
	 * the session cache
	 */
	public static final int AFTER_EVICT = 0;

	/**
	 * A cascade point that occurs just after locking a transient parent entity
	 * into the session cache
	 */
	public static final int BEFORE_REFRESH = 0;

	/**
	 * A cascade point that occurs just after refreshing a parent entity
	 */
	public static final int AFTER_LOCK = 0;

	/**
	 * A cascade point that occurs just before merging from a transient parent
	 * entity into the object in the session cache
	 */
	public static final int BEFORE_MERGE = 0;

	// The allowable cascade styles for a property:

	/**
	 * Cascade an action to the child or children
	 */
	private void cascadeProperty(final Object child, final Type type,
			final CascadeStyle style, final Object anything,
			final boolean isCascadeDeleteEnabled) throws HibernateException {

		if (child != null) {
			if (type.isAssociationType()) {
				AssociationType associationType = (AssociationType) type;
				if (cascadeAssociationNow(associationType)) {
					cascadeAssociation(child, type, style, anything,
							isCascadeDeleteEnabled);
				}
			} else if (type.isComponentType()) {
				cascadeComponent(child, (AbstractComponentType) type, anything);
			}
		}
	}

	private boolean cascadeAssociationNow(AssociationType associationType) {
		return associationType.getForeignKeyDirection().cascadeNow(cascadeTo)
				&& (eventSource.getEntityMode() != EntityMode.DOM4J || associationType
						.isEmbeddedInXML());
	}

	private void cascadeComponent(final Object child,
			final AbstractComponentType componentType, final Object anything) {
		Object[] children = componentType.getPropertyValues(child, eventSource);
		Type[] types = componentType.getSubtypes();
		for (int i = 0; i < types.length; i++) {
			CascadeStyle componentPropertyStyle = componentType
					.getCascadeStyle(i);
			if (componentPropertyStyle.doCascade(action)) {
				cascadeProperty(children[i], types[i], componentPropertyStyle,
						anything, false);
			}
		}
	}

	private void cascadeAssociation(final Object child, final Type type,
			final CascadeStyle style, final Object anything,
			final boolean isCascadeDeleteEnabled) {
		if (type.isEntityType() || type.isAnyType()) {
			cascadeToOne(child, type, style, anything, isCascadeDeleteEnabled);
		} else if (type.isCollectionType()) {
			cascadeCollection(child, style, anything, (CollectionType) type);
		}
	}

	/**
	 * Cascade an action to a collection
	 */
	private void cascadeCollection(final Object child,
			final CascadeStyle style, final Object anything,
			final CollectionType type) {

		CollectionPersister persister = eventSource.getFactory()
				.getCollectionPersister(type.getRole());
		Type elemType = persister.getElementType();

		final int oldCascadeTo = cascadeTo;
		if (cascadeTo == AFTER_INSERT_BEFORE_DELETE) {
			cascadeTo = AFTER_INSERT_BEFORE_DELETE_VIA_COLLECTION;
		}

		// cascade to current collection elements
		if (elemType.isEntityType() || elemType.isAnyType()
				|| elemType.isComponentType()) {
			cascadeCollectionElements(child, type, style, elemType, anything,
					persister.isCascadeDeleteEnabled());
		}

		cascadeTo = oldCascadeTo;
	}

	/**
	 * Cascade an action to a to-one association or any type
	 */
	private void cascadeToOne(final Object child, final Type type,
			final CascadeStyle style, final Object anything,
			final boolean isCascadeDeleteEnabled) {

		final String entityName = type.isEntityType() ? ((EntityType) type)
				.getAssociatedEntityName() : null;

		if (style.reallyDoCascade(action)) { // not really necessary, but
												// good for consistency...
			action.cascade(eventSource, child, entityName, anything,
					isCascadeDeleteEnabled);
		}

	}

	/**
	 * Cascade an action from the parent entity instance to all its children
	 */
	public void cascade(final EntityPersister persister, final Object parent)
			throws HibernateException {
		cascade(persister, parent, null);
	}

	/**
	 * Cascade an action from the parent entity instance to all its children
	 */
	public void cascade(final EntityPersister persister, final Object parent,
			final Object anything) throws HibernateException {

		if (persister.hasCascades()) { // performance opt
			if (log.isTraceEnabled()) {
				log.trace("processing cascade " + action + " for: "
						+ persister.getEntityName());
			}

			Type[] types = persister.getPropertyTypes();
			CascadeStyle[] cascadeStyles = persister.getPropertyCascadeStyles();
			for (int i = 0; i < types.length; i++) {
				CascadeStyle style = cascadeStyles[i];
				if (style.doCascade(action)) {
					// associations cannot be field-level lazy="true", so don't
					// need to check that the field is fetched (laziness for
					// associations is always done by proxying currently)
					cascadeProperty(persister.getPropertyValue(parent, i,
							eventSource.getEntityMode()), types[i], style,
							anything, false);
				}
			}

			if (log.isTraceEnabled()) {
				log.trace("done processing cascade " + action + " for: "
						+ persister.getEntityName());
			}
		}
	}

	/**
	 * Cascade to the collection elements
	 */
	private void cascadeCollectionElements(final Object child,
			final CollectionType collectionType, final CascadeStyle style,
			final Type elemType, final Object anything,
			final boolean isCascadeDeleteEnabled) throws HibernateException {

		// we can't cascade to non-embedded elements
		boolean embeddedElements = eventSource.getEntityMode() != EntityMode.DOM4J
				|| ((EntityType) collectionType.getElementType(eventSource
						.getFactory())).isEmbeddedInXML();

		boolean reallyDoCascade = style.reallyDoCascade(action)
				&& embeddedElements
				&& child != CollectionType.UNFETCHED_COLLECTION;

		if (reallyDoCascade) {
			if (log.isTraceEnabled()) {
				log.trace("cascade " + action + " for collection: "
						+ collectionType.getRole());
			}

			Iterator iter = action.getCascadableChildrenIterator(eventSource,
					collectionType, child);
			while (iter.hasNext()) {
				cascadeProperty(iter.next(), elemType, style, anything,
						isCascadeDeleteEnabled);
			}

			// Hack to cascade into keys of maps
			if (child instanceof Map) {
				Set keys = ((Map) child).keySet();
				SetType setType = new SetType(collectionType.getRole(),
						collectionType.getLHSPropertyName(), true);
				Iterator keyIter = action.getCascadableChildrenIterator(
						eventSource, setType, keys);
				while (keyIter.hasNext()) {
					cascadeProperty(keyIter.next(), elemType, style, anything,
							isCascadeDeleteEnabled);
				}
			}
			// End of hack

			if (log.isTraceEnabled()) {
				log.trace("done cascade " + action + " for collection: "
						+ collectionType.getRole());
			}
		}

		final boolean deleteOrphans = style.hasOrphanDelete()
				&& action.deleteOrphans() && elemType.isEntityType()
				&& child instanceof PersistentCollection; // a newly
															// instantiated
															// collection can't
															// have orphans

		if (deleteOrphans) { // handle orphaned entities!!
			if (log.isTraceEnabled()) {
				log.trace("deleting orphans for collection: "
						+ collectionType.getRole());
			}

			// we can do the cast since orphan-delete does not apply to:
			// 1. newly instantiated collections
			// 2. arrays (we can't track orphans for detached arrays)
			final String entityName = collectionType
					.getAssociatedEntityName(eventSource.getFactory());
			deleteOrphans(entityName, (PersistentCollection) child);

			if (log.isTraceEnabled()) {
				log.trace("done deleting orphans for collection: "
						+ collectionType.getRole());
			}
		}
	}

	/**
	 * Delete any entities that were removed from the collection
	 */
	private void deleteOrphans(String entityName, PersistentCollection pc)
			throws HibernateException {

		final Collection orphans;
		// TODO: suck this logic into the collection!
		if (pc.wasInitialized()) {
			CollectionEntry ce = eventSource.getPersistenceContext()
					.getCollectionEntry(pc);
			orphans = ce == null ? CollectionHelper.EMPTY_COLLECTION : ce
					.getOrphans(entityName, pc);
		} else {
			orphans = pc.getQueuedOrphans(entityName);
		}

		final Iterator orphanIter = orphans.iterator();
		while (orphanIter.hasNext()) {
			Object orphan = orphanIter.next();
			if (orphan != null) {
				if (log.isTraceEnabled()) {
					log.trace("deleting orphaned entity instance: "
							+ entityName);
				}
        // TODO is this right?
        eventSource.delete(entityName, orphan, false, null);
//        eventSource.delete(entityName, orphan);
			}
		}

	}

}
