package com.tandbergtv.metadatamanager;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Expression;
import org.hibernate.criterion.Restrictions;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;

import com.tandbergtv.metadatamanager.exception.InvalidRevisionException;
import com.tandbergtv.metadatamanager.exception.MetadataException;
import com.tandbergtv.metadatamanager.exception.SearchException;
import com.tandbergtv.metadatamanager.model.Asset;
import com.tandbergtv.metadatamanager.model.AssetState;
import com.tandbergtv.metadatamanager.model.Field;
import com.tandbergtv.metadatamanager.model.FieldRevision;
import com.tandbergtv.metadatamanager.model.Group;
import com.tandbergtv.metadatamanager.model.NextRevision;
import com.tandbergtv.metadatamanager.model.Relation;
import com.tandbergtv.metadatamanager.model.RootAssetRevision;
import com.tandbergtv.metadatamanager.search.AssetSearchService;
import com.tandbergtv.metadatamanager.specimpl.ttv.TTVId;
import com.tandbergtv.metadatamanager.util.AssetUtil;

/**
 * This class communicates with the database to save/retrieve assets. It depends
 * on HibernateTemplate for this purpose which is injected via Spring.
 * 
 * @author spuranik
 * 
 */
public class MetadataManagerDAOImpl implements MetadataManagerDAO {
	private static final Logger logger = Logger
	.getLogger(MetadataManagerDAOImpl.class);

	/* wrapper over the session factory */
	private HibernateTemplate template;

	/* tx manager to get and commit transactions */
	private PlatformTransactionManager platformTxMgr;

	/* table prefix */
	private String tablePrefix;

	/* asset search service */
	private AssetSearchService assetSearchService;

	/**
	 * @return the table prefix
	 */
	public String getTablePrefix() {
		return tablePrefix;
	}

	/**
	 * @param tablePrefix
	 *            the table prefix to set
	 */
	public void setTablePrefix(String tablePrefix) {
		this.tablePrefix = tablePrefix;
	}

	/**
	 * @return the platformTxMgr
	 */
	public PlatformTransactionManager getPlatformTxMgr() {
		return platformTxMgr;
	}

	/**
	 * @param platformTxMgr
	 *            the platformTxMgr to set
	 */
	public void setPlatformTxMgr(PlatformTransactionManager platformTxMgr) {
		this.platformTxMgr = platformTxMgr;
	}

	/**
	 * @return the template
	 */
	public HibernateTemplate getTemplate() {
		return template;
	}

	/**
	 * @param template
	 *            the template to set
	 */
	public void setTemplate(HibernateTemplate template) {
		this.template = template;
	}

	/*
	 * Creates a new entry for this asset in the db or throws an exception in
	 * case of any problems while adding the asset to the db.
	 */
	@Transactional
	public TTVId saveAsset(Asset asset) throws MetadataException {
		logger.debug("Start of saveAsset()");
		try {
			if (asset.getState() == AssetState.INACTIVE) {
				throw new MetadataException(
						"Cannot add inactive assets to the store.");
			}
			asset = AssetUtil.deleteEmptyFields(asset);		
			getTemplate().saveOrUpdate(asset);
			getTemplate().flush();
			return asset.getTTVId();
		} catch (DataAccessException e) {
			throw new MetadataException(e.getMessage(), e);
		}
	}

	/*
	 * if the asset is an non root item, deletes the asset. deletes the relation
	 * in this assets parent. if the asset is a root, marks it inactive and
	 * marks all the children as inactive too.
	 */
	@Transactional
	public void delete(Asset a) throws SearchException {
		if (a.getRoot() == null) {
			// asset is a root item. need to mark it inactive and all its
			// children inactive too.
			inactivateAllChildren(a);
			getTemplate().update(a);
		} else {
			// asset is a non root item. need to delete this item.
			deleteChildren(a);
			
			Asset root = a.getRoot();
			Asset parent = root.getAssetsParent(a);
			Relation toBeDeletedRelation = null;
			for (Relation r : parent.getRelations()) {
				if (r.getTargetAsset().getId() == a.getId()) {
					toBeDeletedRelation = r;
				}
			}
			parent.getRelations().remove(toBeDeletedRelation);
			getTemplate().update(root);
			getTemplate().delete(a);
		}
	}
	
	/**
	 * Physically, and violently, delete the entire tree rooted at this specified asset. 
	 * 
	 * @param asset
	 */
	private void deleteChildren(Asset asset) {
		Collection<Relation> relations = new ArrayList<Relation>();
		
		/* WARNING traverses ALL possible relations while purging. */
		for (Relation relation : asset.getRelations()) {
			Asset target = relation.getTargetAsset();
			
			deleteChildren(target);
			getTemplate().delete(target);
			relations.add(relation);
		}
		
		asset.getRelations().removeAll(relations);
	}

	/*
	 * recursively marks all the assets in the asset tree as inactive
	 */
	private void inactivateAllChildren(Asset a) {
		a.setState(AssetState.INACTIVE);
		for (Relation relation : a.getRelations()) {
			Asset targetAsset = relation.getTargetAsset();
			targetAsset.setState(AssetState.INACTIVE);
			inactivateAllChildren(targetAsset);
		}
	}

	/*
	 * Retrieves an asset given its TTV id. If version is not specified in
	 * TTVId, the latest version is retrieved.
	 * @revisionNumber if equals NextRevision.NULL_REVISION_NUMBER, get the latest version
	 */
	@Transactional
	public Asset getAsset(TTVId id, int revisionNumber, boolean isForReadOnly) throws SearchException, InvalidRevisionException {
		logger.debug("Start of getAsset("+ id.getId() + "," +revisionNumber + ")");
		
		Session session = getCurrentSession();
		
		Asset asset = (Asset) getTemplate().get(Asset.class, id.getId());

		if (asset == null || asset.getState() == AssetState.INACTIVE) {
			throw new SearchException("Could not get asset with id: " + id.getId());
		}

		if(revisionNumber != NextRevision.NULL_REVISION_NUMBER){
			if(!isValidRevisionNumber(asset, revisionNumber)){
				throw new InvalidRevisionException("Revision Number does not exist");
			}
		}else{
			revisionNumber = Integer.MAX_VALUE;
		}

		session.enableFilter("relationRevisionFilter").setParameter("revisionParam", revisionNumber);	
		asset.getRelations();
		
		Asset returnAsset = null;
		boolean needToGetFieldRevision = needToGetFieldRevision(revisionNumber, asset.getLatestRevisionNumber(), isForReadOnly);

		asset = traversAssetTree(asset, needToGetFieldRevision);
		AssetUtil.evictAssetTree(asset, getCurrentSession());
		if(needToGetFieldRevision){
		 returnAsset = getAssetTree(asset, revisionNumber, isForReadOnly, false);
		}else{
			returnAsset = asset;
		}
		logger.debug("END of getAsset("+ id.getId() + ")");
		
		return returnAsset;
	}
	
	private boolean needToGetFieldRevision(int desiiredVersionNumber, int assetLatestVersionNumber, boolean isForReadOnly){
		boolean isGettingLatest = desiiredVersionNumber>= assetLatestVersionNumber;
		return !(isGettingLatest&&isForReadOnly);
	}
	
	private boolean isValidRevisionNumber(Asset asset, int revisionNumber) {
		boolean isValid = false;

		if (asset instanceof Group) {
			for (RootAssetRevision rootAssetRevision : ((Group) asset).getRevisions()) {
				if (rootAssetRevision.getRevisionNumber() == revisionNumber) {
					isValid = true;
					break;
				}
			}

		} else {
			isValid = true;
		}
		return isValid;
	}

	@Transactional
	public Asset getAsset(final TTVId id, boolean isForReadOnly) throws SearchException {
		try {
			return getAsset(id, NextRevision.NULL_REVISION_NUMBER, isForReadOnly);
		} catch (InvalidRevisionException e) {
			logger.error("Unable to get latest Asset with TTVid: " + id, e);
			throw new SearchException("Invalid Revision");
		}
	}

	@SuppressWarnings("unchecked")
	@Transactional
	public Relation getRelation(Asset owner, Asset target)
			throws SearchException {
		DetachedCriteria criteria = DetachedCriteria.forClass(Relation.class);
		criteria.add(Restrictions.eq("targetAsset.id", target.getTTVId()
				.getId()));
		criteria
				.add(Restrictions.eq("ownerAsset.id", owner.getTTVId().getId()));
		List<Relation> relations = getTemplate().findByCriteria(criteria);
		if (relations.size() > 1) {
			throw new SearchException(
					"Multiple relations found between owner: "
							+ owner.getTTVId().getId() + " and target: "
							+ target.getTTVId().getId());
		}
		return (relations.size() == 1) ? relations.get(0) : null;
	}

	public AssetSearchService getAssetSearchService() {
		return assetSearchService;
	}

	public void setAssetSearchService(AssetSearchService assetSearchService) {
		this.assetSearchService = assetSearchService;
	}

	@SuppressWarnings("unchecked")
	@Override
	@Transactional
	public List<FieldRevision> getUnDeletedFieldRevsions(FieldRevision fieldRevision) {
		DetachedCriteria criteria = DetachedCriteria.forClass(FieldRevision.class).add(Expression.eq("parentAsset.id", fieldRevision.getParentAsset().getId()));
		criteria.add(Expression.eq("ttvXPath", fieldRevision.getTtvXPath()));
		criteria.add(Expression.eq("storedIndices", fieldRevision.getStoredIndices()));
		criteria.add(Expression.lt("revisionNumber", fieldRevision.getRevisionNumber()));
		criteria.add(Expression.le("deleteRevision", NextRevision.NULL_REVISION_NUMBER));
 
		return getTemplate().findByCriteria(criteria);
	}
	
	/**
	 * Given an Asset retrieved from DB, construct a new Asset from it by navigating through the DB Asset tree and filter
	 * out the correct fieldRevisions
	 * 
	 * @param ttvId
	 * @param desiredRevision
	 * @param isForReadOnly
	 * @return the newly constructed Asset
	 * @throws SearchException
	 */
	public Asset getAssetTree(Asset dbAsset, int desiredRevision, boolean isForReadOnly, boolean forceCopyFromFieldRevisions) throws SearchException{
		Asset asset =dbAsset;
		boolean isGettingLatest = desiredRevision>= asset.getLatestRevisionNumber();
		//1. If 'Group',  take care of RootAssetRevisions
		if((asset instanceof Group) && !isGettingLatest){
			List<RootAssetRevision> toBeDeletedRevisions = new ArrayList<RootAssetRevision>();
			for(RootAssetRevision rootAssetRevision: ((Group)asset).getRevisions()){
				if(rootAssetRevision.getRevisionNumber() > desiredRevision){
					toBeDeletedRevisions.add(rootAssetRevision);
				}else{
					break;
				}
			}
			((Group)asset).getRevisions().removeAll(toBeDeletedRevisions);
		}
		//2. take care of fields&fieldRevisions
		if(!isForReadOnly || !isGettingLatest || forceCopyFromFieldRevisions){
			//1. Get the correct version of fieldRevisions
			getRightVersionOfFieldRevisions(asset, desiredRevision);
			//2. copy fieldRevisions to fields
			if(isForReadOnly){//i.e. if (!isGettingLatest || forceCopyFromFieldRevisions)
				copyFromRevisionsToFields(asset, desiredRevision, forceCopyFromFieldRevisions);
			}
		}
		
		//3. for each of its child, repeat
		for (Relation targetRelation : asset.getRelations()) {
			Asset ta = targetRelation.getTargetAsset();
			boolean forceCopy = false;
			if(targetRelation.getDeleteRevision()!=NextRevision.NULL_REVISION_NUMBER){
				//If a relation's DeleteRevision is not NULL( the child asset is deleted on latest revision, 
				// i.e. current fields don't contain this older revision's child target asset's fieldRevisions)
				forceCopy=true;
			}
			targetRelation.setTargetAsset(getAssetTree(ta, desiredRevision, isForReadOnly, forceCopy));
		}
		
		
		return asset;
	}
	
	/**
	 * Filter Asset's FieldRevisions to remove all FieldRevisions with invalid revisions; 
	 * After this method call, Asset's FieldRevisions only contains the correct revisions
	 * @param asset
	 * @param desiredRevision
	 */
	private void getRightVersionOfFieldRevisions(Asset asset, int desiredRevision) {
		Map<String, FieldRevision> rightVersionOfFieldRevisions = new HashMap<String, FieldRevision>();
		List<FieldRevision> invalidVersionOfFieldRevisions = new ArrayList<FieldRevision>();

		for (FieldRevision fieldRevision : asset.getFieldRevisions()) {
			int deleteRevisionNumber = fieldRevision.getDeleteRevision();
			int revisionNumber = fieldRevision.getRevisionNumber();
			String mapKey = fieldRevision.getTtvXPath() + fieldRevision.getIndices();
			FieldRevision fieldRevisionMapEntry = rightVersionOfFieldRevisions.get(mapKey);

			if ((fieldRevision.getAddRevision() <= desiredRevision)
					&&(deleteRevisionNumber > desiredRevision || deleteRevisionNumber == NextRevision.NULL_REVISION_NUMBER)
					&&(revisionNumber <= desiredRevision)){
					if(fieldRevisionMapEntry == null ){
						rightVersionOfFieldRevisions.put(mapKey, fieldRevision);
					}else if(fieldRevisionMapEntry.getRevisionNumber() < revisionNumber){
						invalidVersionOfFieldRevisions.add(fieldRevisionMapEntry);
						rightVersionOfFieldRevisions.put(mapKey, fieldRevision);
					}else{
						invalidVersionOfFieldRevisions.add(fieldRevision);
					}
			}else {
				invalidVersionOfFieldRevisions.add(fieldRevision);
			}
		}
		asset.getFieldRevisions().removeAll(invalidVersionOfFieldRevisions);
		asset.setFieldRevisionsMap(rightVersionOfFieldRevisions);
	}

	
	/**
	 * When getting an older revision of Asset, we need to replace asset.fields with 
	 * asset.fieldRevisions
	 * 1. If Asset's parameter revisonNumber is less than asset's latestRevisionNumber
	 * 2. If a relation's DeleteRevision is not NULL( the child asset is deleted on latest revision, 
	 * i.e. current fields don't contain this older revision's child target asset's fieldRevisions)
	 * @param asset
	 * @param revisionNumber
	 * @param forceCopy
	 */
	protected void copyFromRevisionsToFields(Asset asset, int revisionNumber, boolean forceCopy){
		if(revisionNumber< asset.getLatestRevisionNumber() || forceCopy){
			List<Field> assetFields = asset.getFields();
			assetFields.removeAll(assetFields);
			for(FieldRevision fieldRevision: asset.getFieldRevisions()){
				assetFields.add(new Field(fieldRevision));
			}
		}
	}
	
	protected Asset traversAssetTree(Asset asset, boolean getFieldRevisions) {
		if(getFieldRevisions){
			if(asset.getFieldRevisions() != null)
				asset.getFieldRevisions().size();
		}
		asset = new AssetUtil().unWrap(asset);
		Hibernate.initialize(asset.getAllDescendantAssetFields());
		// Set the target assets back to the right class type for the binder to
		// recognize it.
		for (Relation targetRelation : asset.getRelations()) {
			Asset ta = targetRelation.getTargetAsset();
			ta = new AssetUtil().unWrap(ta);
			targetRelation.setTargetAsset(ta);
			// do the same for this target assets
			traversAssetTree(ta, getFieldRevisions);
		}
		return asset;
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.metadatamanager.MetadataManagerDAO#getCurrentSession()
	 */
	public Session getCurrentSession() {
		/* Get the current native session used for the current transaction */ 
		return (Session) this.getTemplate().executeWithNativeSession(new HibernateCallback() {
			@Override
			public Session doInHibernate(Session session) {
				return session;
			}
		});
	}
}