package eu.dnetlib.dlms.impl.daos;

import java.util.Collection;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Transaction;
import org.openrdf.model.Resource;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.RepositoryResult;
import org.springframework.beans.factory.annotation.Required;

import eu.dnetlib.dlms.impl.GeneralHibernateDAO;
import eu.dnetlib.dlms.impl.hibobjects.RepositoryWrapper;
import eu.dnetlib.dlms.impl.hibobjects.TripleStoreSynchronization;
import eu.dnetlib.dlms.lowlevel.DorotyConstants;
import eu.dnetlib.dlms.lowlevel.LowLevelException;
import eu.dnetlib.dlms.lowlevel.objects.LLDigitalObject;
import eu.dnetlib.dlms.lowlevel.objects.LLDigitalObjectDAO;
import eu.dnetlib.dlms.lowlevel.objects.Set;
import eu.dnetlib.dlms.lowlevel.objects.SetDAO;
import eu.dnetlib.dlms.union.objects.UnionSet;
import eu.dnetlib.dlms.union.objects.UnionSetDAO;

/**
 * UnionSetDAO implementation that does not use wrappers but holds the reference of the objects, as if they were
 * imported. UnionSet managed by this DAO will contain references to all objects in the included sets. Mreover, when a
 * UnionSet is created, it is automatically filled to contain all objects of all included sets. TODO: needs a way to
 * know when an object is created in every included set.
 * 
 * @author lexis
 * 
 */
public class UnionSetDAOImpl extends GeneralHibernateDAO<UnionSet> implements UnionSetDAO {
	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(UnionSetDAOImpl.class);

	/** Triplestore Wrapper to get connections from. */
	private RepositoryWrapper repositoryWrapper;
	/** Object DAO to update objects included in the union set. */
	private LLDigitalObjectDAO objDAO;
	private SetDAO setDAO;

	private final static String QUERY_OBJECTS_NAME = "eu.dnetlib.dlms.union.objects.UnionSet.unionObjects";

	/**
	 * {@inheritDoc}. When a UnionSet is saved, the triplestore should be updated as follows: for each object in the
	 * includedSet a tuple must be added stating that the object belongs to the union set. objID BTO uSetID [usetName]
	 * No matter if the triples are already there: duplicate triples are silently avoided.
	 * 
	 * @see eu.dnetlib.dlms.impl.GeneralHibernateDAO#save(eu.dnetlib.dlms.lowlevel.objects.LLDigitalObject)
	 */
	@Override
	public void save(final UnionSet unionSet) {
		log.debug("Saving a union set");
		RepositoryConnection con = null;
		try {
			con = this.repositoryWrapper.getConnection();
			if (!this.repositoryWrapper.isConnectionRegistered()) {
				final Transaction t = this.getSession().getTransaction();
				t.registerSynchronization(new TripleStoreSynchronization(con));
				log.debug("Current transaction in save(UnionSet) is: " + t + ", registered RepositoryConnection: " + con);
				this.repositoryWrapper.setConnectionRegistered(true);
			}
			super.save(unionSet);
			this.updateObjects(unionSet);
			this.saveTuples(unionSet, con);
		} catch (final Exception e) {
			throw new LowLevelException(e);
		}
	}

	private void updateObjects(final UnionSet unionSet) {
		for (final Set s : unionSet.getSets()) {
			for (final LLDigitalObject o : s.getObjects()) {
				o.getBelongingSets().add(unionSet);
				//need to save the object otherwise the change is not stored.
				this.objDAO.save(o);
			}
		}
	}

	private void saveTuples(final UnionSet unionSet, final RepositoryConnection con) {
		try {
			//need to add one tuple for each object in each included set:
			final ValueFactory valFactory = con.getValueFactory();
			final URI unionID = con.getValueFactory().createURI(DorotyConstants.DOROTYNAMESPACE + unionSet.getId());
			final Resource unionName = con.getValueFactory().createURI(DorotyConstants.DOROTYNAMESPACE + unionSet.getName());
			final URI belongsToPred = valFactory.createURI(DorotyConstants.BELONGSTOPREDICATE);
			//rimuovo le triple x BTO unionSet
			con.remove((Resource) null, belongsToPred, unionID);
			for (final Set s : unionSet.getSets()) {
				final URI theSetIdURI = con.getValueFactory().createURI(DorotyConstants.DOROTYNAMESPACE + s.getId());
				//cerco tutte le triple con predicate belongsTo e oggetto doroty:s.id
				final RepositoryResult<Statement> stms = con.getStatements(null, belongsToPred, theSetIdURI, false);
				while (stms.hasNext()) {
					final Statement curr = stms.next();
					final Statement st = valFactory.createStatement(curr.getSubject(), belongsToPred, unionID, unionName);
					con.add(st);
					log.debug("SaveInTripleStore for UnionSet - Added statement: " + st);
				}
			}
		} catch (final RepositoryException e) {
			throw new LowLevelException(e);
		}
	}

	/**
	 * {@inheritDoc}. If a UnionSet is deleted what happens to the included sets? Nothing. The triples concerning the
	 * unionset are to be deleted only after the set is removed from hibernate. If hibernate deletion succeeds, then it
	 * means that the union set is not referenced by any relation, otherwise an exception is thrown.
	 * 
	 * @see eu.dnetlib.dlms.impl.GeneralHibernateDAO#delete(eu.dnetlib.dlms.lowlevel.objects.LLDigitalObject)
	 */
	@Override
	public void delete(final UnionSet unionSet) {
		RepositoryConnection con = null;
		try {
			con = this.repositoryWrapper.getConnection();
			if (!this.repositoryWrapper.isConnectionRegistered()) {
				final Transaction t = this.getSession().getTransaction();
				t.registerSynchronization(new TripleStoreSynchronization(con));
				log.debug("Current transaction in delete(UnionSet) is: " + t + ", registered RepositoryConnection: " + con);
				this.repositoryWrapper.setConnectionRegistered(true);
			}
			super.delete(unionSet);
			this.deleteTuples(unionSet, con);
		} catch (final Exception e) {
			throw new LowLevelException(e);
		}

	}

	private void deleteTuples(final UnionSet unionSet, final RepositoryConnection con) {
		final URI unionContext = con.getValueFactory().createURI(DorotyConstants.DOROTYNAMESPACE + unionSet.getName());
		//delete all tuples in the context of the union.
		try {
			con.remove(null, null, (Value) null, unionContext);
		} catch (final RepositoryException e) {
			throw new LowLevelException(e);
		}
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see eu.dnetlib.dlms.union.objects.UnionSetDAO#getUnionSetByName(java.lang.String)
	 */
	@SuppressWarnings("unchecked")
	public UnionSet getUnionSetByName(final String name) {
		final List<UnionSet> res = this.getHibernateTemplate().findByNamedParam("FROM UnionSet WHERE setType.name = :setTypeName", "setTypeName", name);
		if (res.isEmpty()) {
			return null;
		} else
			return res.get(0);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see eu.dnetlib.dlms.union.objects.UnionSetDAO#getUnionSetsIncluding(eu.dnetlib.dlms.lowlevel.objects.Set)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public Collection<UnionSet> getUnionSetsIncluding(final Set set) {
		return this.getHibernateTemplate().findByNamedParam("FROM UnionSet WHERE :set IN elements(sets)", "set", set);
	}

	/**
	 * {@inheritDoc}. With the load have to call the named query.
	 * 
	 * @see eu.dnetlib.dlms.impl.GeneralHibernateDAO#load(eu.dnetlib.dlms.lowlevel.objects.LLDigitalObject)
	 */
	@SuppressWarnings("unchecked")
	@Override
	public void load(final UnionSet unionSet) {
		super.load(unionSet);
		final List l = this.getHibernateTemplate().findByNamedQueryAndNamedParam(QUERY_OBJECTS_NAME, "unionset", unionSet);
		unionSet.setObjectsInUnion(l);
		unionSet.setCurrentObjCount(l.size());
	}

	@Required
	public void setRepositoryWrapper(final RepositoryWrapper repositoryWrapper) {
		this.repositoryWrapper = repositoryWrapper;
	}

	public RepositoryWrapper getRepositoryWrapper() {
		return this.repositoryWrapper;
	}

	/** NO args constructor. */
	public UnionSetDAOImpl() {
		this.setClazz(UnionSet.class);
	}

	@Required
	public void setObjDAO(final LLDigitalObjectDAO objDAO) {
		this.objDAO = objDAO;
	}

	public LLDigitalObjectDAO getObjDAO() {
		return this.objDAO;
	}

	@Required
	public void setSetDAO(final SetDAO setDAO) {
		this.setDAO = setDAO;
	}

	public SetDAO getSetDAO() {
		return this.setDAO;
	}

}
