/********************************************************************
 * Copyright © 2006 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.esr.common.persistent.history;

// Java imports
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.Filter;
import org.hibernate.Session;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;

import gov.va.med.esr.service.impl.ChangeEvent;
import gov.va.med.fw.model.AbstractKeyedEntity;
import gov.va.med.fw.model.AbstractVersionedEntity;
import gov.va.med.fw.model.EntityKey;
import gov.va.med.fw.persistent.DAOException;
import gov.va.med.fw.persistent.hibernate.AbstractDAOAction;

/**
 * @author DNS   CHENJ2
 * @version 1.0
 */
public abstract class AbstractSingleAssocHistoryDAOImpl extends HistoryDAOImpl {

	
    private static final long serialVersionUID = -1370346746640850300L;
    private String singleEndPropertyName = null;
	private String manyEndParentClassName = null;
	
	/**
	 * A default constructor
	 */
	public AbstractSingleAssocHistoryDAOImpl() {
		super();
	}

	/**
	 * Retrieves the entity given a timestamp (filter should have been preset
	 * for each of current or previous version.
	 * 
	 * @param changeEvent
	 *            provides the key to the root object, and timestamp for history
	 *            retrieval
	 * @see gov.va.med.esr.common.persistent.history.HistoryDAO#getHistoryByChangeTime(gov.va.med.esr.service.impl.ChangeEvent)
	 */
	protected AbstractVersionedEntity getHistoricalEntity(Session session, ChangeEvent changeEvent) throws DAOException {

		AbstractVersionedEntity rootEntity = null;
		AbstractVersionedEntity p = null;

		try {
			rootEntity = super.getHistoricalEntity(session, changeEvent);
		
			if (rootEntity != null) {
				p = getManyEndParent(rootEntity);
				if (p != null) {
					// manually attach the single ended association subgraph to
					// the main graph
					EntityKey key = ((AbstractKeyedEntity)p).getEntityKey();
					AbstractVersionedEntity c = getHistoricalSingleEndedChild(
							session, changeEvent, key);
					if (c != null) {
						setSingleEndChild(p, c);
					}
					// explicit evict at this level, apparently for this
					// proxy/child
					// calling evict(r) at parent's getCurrentVersion and
					// getPreviousVersion didn't work
					evict(c);
				}
			}
		} catch (Exception e) {
			throw new DAOException(
					"Failed to get Historical Entity for changeEvent: "	+ changeEvent, e);
		}
		return rootEntity;
	}


	/**
	 * Given the root of the tree, return the object at the many end of the relationship
	 * By default, this is the same as the root.
	 * @param rootEntity
	 * @return
	 */
	protected AbstractVersionedEntity getManyEndParent(AbstractVersionedEntity rootEntity)
	{
		return rootEntity;
	}
	
	/**
	 * Manually access child via a Criteria query instead of the base class's
	 * access() funtion, since many-to-one relationships cannot be accessed
	 * using the base implementation. (The base implementation rely on the
	 * relationships to be a one-to-many, using a set, so that we can use
	 * order-by, and then using the cache to get the max for each unique id).
	 * 
	 * @param changeEvent
	 * @param registryId
	 * @return
	 * @throws DAOException
	 */
	private AbstractVersionedEntity getHistoricalSingleEndedChild(
			Session session, ChangeEvent changeEvent, EntityKey key) throws Exception 
	{
		/* target query:
		 * 
		 * SELECT rt 
		 * FROM PrisonerOfWar as r 
		 * 		join r.registryTrait as rt 
		 * WHERE r.identifier = :Id 
		 * 	AND rt.modifiedOn <= :q_date
		 *  AND r.modifiedOn <= :q_date 
		 * 	ORDER BY rt.modifiedOn desc, rt.historyId desc
		 */	
		Criteria criteria = session.createCriteria(Class.forName(getManyEndParentClassName()));
		criteria.add(Restrictions.eq("identifier", key.getKeyValue()));
		Criteria rtCriteria = criteria.createCriteria(getSingleEndPropertyName(), "child");
		rtCriteria.addOrder(Order.desc("modifiedOn"));
		rtCriteria.addOrder(Order.desc("historyId"));
		
		// add date filters manually accordingly, since the mapping only works
		// with one-to-many sets, and not for many-to-one single ended
		// associations
		Filter f = session.getEnabledFilter(FILTER_AS_OF_DATE);
		if (f != null) {
			rtCriteria.add(Expression.le("modifiedOn", changeEvent
					.getTimeStamp()));
		} else {
			f = session.getEnabledFilter(FILTER_PRIOR_TO_DATE);
			if (f != null) {
				rtCriteria.add(Expression.lt("modifiedOn", changeEvent
						.getTimeStamp()));
			}
		}

		//criteria.setProjection(Projections.property("registryTrait")); --> this hits a bug, and fetch is not done eagerly.
		
		// eager fetch, otherwise we get the proxy back, wrapping only the
		// identifier, rather than the historyId (real primary key)
		criteria.setFetchMode(getSingleEndPropertyName(), FetchMode.JOIN);
		
		criteria.setMaxResults(1);
		
		// bring back both objects, since otherwise eager fetch doesn't work.
		criteria.setResultTransformer(Criteria.ALIAS_TO_ENTITY_MAP);
		
		AbstractVersionedEntity c = null;

		List results = criteria.list();
		
		if (results != null) {
			Iterator iter = results.iterator();
			if (iter.hasNext()) {
				Map map = (Map) iter.next();
				c = (AbstractVersionedEntity) map.get("child");
			}
		}
		return c;
	}

	/**
	 * This method is specifically used for lazily-initialized many-to-one relationships.
	 * For deleting children (in a set) to the single end object (acutally now a proxy).
	 * Since the lazy many-to-one uses a CGLIB proxy, the above reflection to access
	 * private fields no longer works (cannot access private field of a proxy, it's not declared as public).  
	 * In this case we have to manually loop through and remove the item. 
	 * 
	 * @param set
	 * @throws DAOException
	 */
	protected void processDeleteSimpleSetNoReflection(Set set, AbstractVersionedEntity proxiedObject)
			throws DAOException {
		
		AbstractVersionedEntity obj = null;
		Set toBeDeletedSet = null;

		for (Iterator i = set.iterator(); i.hasNext();) {
			obj = (AbstractVersionedEntity) i.next();

			if (obj.isDeleted().booleanValue()) {
				if (toBeDeletedSet == null) {
					toBeDeletedSet = new HashSet();

				}
				toBeDeletedSet.add(obj);
				// We cannot call i.remove, since it's an unmodifieable set;
				// we cannot call parentObj.remove...(obj); it would cause
				// concurrentModificationException
			}
		}

		if (toBeDeletedSet != null) {
			for (Iterator i = toBeDeletedSet.iterator(); i.hasNext();) {
				removeChildOfProxiedObject(proxiedObject, (AbstractVersionedEntity)i.next());
			}
		}
	}

	/**
	 * Invoked by processDeleteSimpleSetNoReflection.  Child class to override.
	 * Only needed if the single ended object has a set of children that needs to be
	 * retrieved as well.
	 * @param parent
	 * @param child
	 */
	protected void removeChildOfProxiedObject(AbstractVersionedEntity parent, AbstractVersionedEntity child) 
	{}		

	/**
	 * Subclass should implement -- invokes the get...() method on the parent
	 * @param p
	 * @param c
	 */
	abstract protected AbstractVersionedEntity getSingleEndChild(AbstractVersionedEntity p);

	/**
	 * Subclass should implement -- invokes the set...(child) method on the parent
	 * @param p
	 * @param c
	 */
	abstract protected void setSingleEndChild(AbstractVersionedEntity p, AbstractVersionedEntity c);
	
	/**
	 * Overrides base class implementation.
	 * Programatically remove delete records. Since we are dealing with a proxy,
	 * the reflection to access the private fields no longer works.
	 * Subclass can override for any of the single end object (proxy'ed) children.
	 */
	protected void removeDeletedRecords(AbstractVersionedEntity entity)
			throws DAOException {

		super.removeDeletedRecords(entity);
		
		AbstractVersionedEntity p = getManyEndParent(entity);
		if (p != null) {
			AbstractVersionedEntity c = getSingleEndChild(p);
			if (c != null) {
				// check to see if child had been deleted, and remove from
				// parent's link
				if (c.isDeleted().booleanValue()) {
					setSingleEndChild(p, null);
					return;
				}
			}
		}
	}

	public String getSingleEndPropertyName() {
		return singleEndPropertyName;
	}

	public void setSingleEndPropertyName(String singleEndPropertyName) {
		this.singleEndPropertyName = singleEndPropertyName;
	}

	public String getManyEndParentClassName() {
		return manyEndParentClassName;
	}

	public void setManyEndParentClassName(String manyEndParentClassName) {
		this.manyEndParentClassName = manyEndParentClassName;
	}

}