package eu.dnetlib.dlms.config;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.query.BindingSet;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.TupleQuery;
import org.openrdf.query.TupleQueryResult;
import org.openrdf.repository.RepositoryConnection;
import org.springframework.beans.factory.annotation.Required;

import eu.dnetlib.dlms.impl.hibobjects.RepositoryWrapper;
import eu.dnetlib.dlms.lowlevel.DorotyConstants;
import eu.dnetlib.dlms.lowlevel.LowLevelException;
import eu.dnetlib.dlms.lowlevel.objects.DorotyObjectEnum;
import eu.dnetlib.dlms.lowlevel.objects.Set;
import eu.dnetlib.dlms.lowlevel.objects.SetDAO;
import eu.dnetlib.dlms.lowlevel.objects.structures.DescriptionValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.DescriptionValueCollection;
import eu.dnetlib.dlms.lowlevel.objects.structures.IntBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.LabelValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.StringBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.Structure;
import eu.dnetlib.dlms.lowlevel.objects.structures.StructureDAO;
import eu.dnetlib.dlms.lowlevel.types.AtomType;
import eu.dnetlib.dlms.lowlevel.types.LowLevelType;
import eu.dnetlib.dlms.lowlevel.types.ObjType;
import eu.dnetlib.dlms.lowlevel.types.RelationType;
import eu.dnetlib.dlms.lowlevel.types.structures.LabelType;
import eu.dnetlib.dlms.lowlevel.types.structures.StructureType;
import eu.dnetlib.dlms.lowlevel.types.structures.StructureTypeDAO;
import eu.dnetlib.dlms.union.objects.UnionSet;
import eu.dnetlib.dlms.union.objects.UnionSetDAO;
import eu.dnetlib.dlms.union.types.UnionType;

/**
 * Helper class to handle system sets and their contents.
 * 
 * @author lexis
 * 
 */
public class SystemSetsHelper {

	/** Logger. */
	private static final Log log = LogFactory.getLog(SystemSetsHelper.class);
	/**
	 * StructureDAO instance needed to create the structure that represents a Set.
	 */
	private StructureDAO structureDAO;
	/**
	 * SetDAO instance.
	 */
	private SetDAO setDAO;
	/**
	 * RepositoryWrapper to work with the graph db.
	 */
	private RepositoryWrapper repositoryWrapper;
	/** StructureTypeDAO instance. */
	private StructureTypeDAO structureTypeDAO;
	/**
	 * UnionSetDAO instance needed to add the system info about Sets into the UnionSet SystemSets.
	 */
	private UnionSetDAO unionSetDAO;

	/**
	 * Checks if the given set is a system set. TODO: actually the check is done by comparing the first part of the
	 * name: if the set's name starts with System, then it is a system set. It should be better to check deeply for each
	 * system set name.
	 * 
	 * @param s
	 *            Set instance
	 * @return true if s is a system set, false otherwise
	 */
	public boolean isSystemSet(final Set s) {
		return s.getName().startsWith(SystemSetsNames.SYSTEM_NS);
	}

	/**
	 * Finds the System Set name where the representation of s should be saved.
	 * 
	 * @param s
	 *            Set to be represented
	 * @return the name of the System Set where the representation of s should be saved.
	 */
	public String getSystemSetNameFor(final Set s) {
		final LowLevelType type = s.getType();
		if (type instanceof ObjType)
			return SystemSetsNames.OBJ_SETS;
		if (type instanceof AtomType)
			return SystemSetsNames.ATOM_SETS;
		if (type instanceof StructureType)
			return SystemSetsNames.STRUCT_SETS;
		if (type instanceof UnionType)
			return SystemSetsNames.UNION_SETS;
		if (type instanceof RelationType)
			return SystemSetsNames.REL_SETS;
		else
			throw new LowLevelException();
	}

	/**
	 * Deletes the system entry corresponding to the given Set.
	 * 
	 * @param s
	 *            a Set to delete
	 */
	public void removeSystemEntryFor(final Set s) {
		final Set systemSet = this.setDAO.getSetByName(this.getSystemSetNameFor(s));
		//rimuovere la corrispondente info dal set objSet? l'id del set sta nel campo id della struct
		final Structure toDelete = this.findStructureForSet(s.getId(), systemSet);
		this.setDAO.removeFromSet(systemSet, toDelete);
		this.structureDAO.save(toDelete);
		this.structureDAO.delete(toDelete);
		this.setDAO.save(systemSet);
		//also update the union of all system sets.
		final UnionSet systemAllSet = this.unionSetDAO.getUnionSetByName(SystemSetsNames.ALL_SETS);
		this.unionSetDAO.save(systemAllSet);
	}

	/**
	 * Finds the Structure in System sets representing the given Set.
	 * 
	 * @param toFind
	 *            Set
	 * @return a Structure instance which is the system representation of set toFind
	 */
	public Structure findSystemStructureFor(final Set toFind) {
		final Set systemSet = this.setDAO.getSetByName(this.getSystemSetNameFor(toFind));
		return this.findStructureForSet(toFind.getId(), systemSet);
	}

	/**
	 * Finds the System Set that should contain the representation of the given Set.
	 * 
	 * @param set
	 *            Set
	 * @return a Set instance, which is the system Set that should contain the Set set
	 */
	public Set getSystemSetFor(final Set set) {
		final String systemSetName = this.getSystemSetNameFor(set);
		return this.setDAO.getSetByName(systemSetName);

	}

	/**
	 * Finds the Structure in Set set having id as its fields called "setID".
	 * 
	 * @param id
	 *            value of the field "id" of the structure to return
	 * @param set
	 *            Set to search in (a system set)
	 * @return a Structure
	 */
	public Structure findStructureForSet(final long id, final Set set) {
		try {
			final RepositoryConnection con = this.repositoryWrapper.getConnection();
			con.setAutoCommit(true);
			final ValueFactory valueFactory = con.getValueFactory();
			final Value val = valueFactory.createLiteral((int) id);
			final String prefixes = "PREFIX doroty: <" + DorotyConstants.DOROTYNAMESPACE + ">\n" + "PREFIX dorotyPrivate: <"
					+ DorotyConstants.PRIVATEDOROTYNS + ">\n";
			final String sparql = prefixes + "SELECT ?structID WHERE { ?structID dorotyPrivate:" + DorotyConstants.BELONGSTO + " doroty:" + set.getId()
					+ ". ?structID doroty:setID " + val + ". }";
			log.debug("SPARQL = " + sparql);
			final TupleQuery q = con.prepareTupleQuery(QueryLanguage.SPARQL, sparql);
			final TupleQueryResult res = q.evaluate();
			Structure foundStructure = null;
			if (res.hasNext()) {
				final BindingSet bs = res.next();
				final URI structIDURI = (URI) bs.getValue("structID");
				final String idStr = structIDURI.stringValue().replaceFirst(DorotyConstants.DOROTYNAMESPACE, "");
				final long idStructure = Long.parseLong(idStr);
				foundStructure = this.structureDAO.getByID(idStructure);
			} else {
				//NO RESULT --> NO ENTRY IN set that have fields id = id
				log.debug("No entry found in Set " + set.getName() + " with field id = " + id);
			}
			res.close();
			return foundStructure;
		} catch (final Exception e) {
			throw new LowLevelException(e);
		}
	}

	/**
	 * Creates and saves a structure object that represents the given set.
	 * 
	 * @param s
	 *            Set instance
	 * @return a Structure that represents s
	 */
	public Structure createStructureFor(final Set s) {
		final LowLevelType type = s.getType();
		Structure struct = null;
		if (type instanceof ObjType)
			struct = this.createStructureFor((ObjType) type, s);
		if (type instanceof AtomType)
			struct = this.createStructureFor((AtomType) type, s);
		if (type instanceof StructureType)
			struct = this.createStructureFor((StructureType) type, s);
		if (type instanceof RelationType)
			struct = this.createStructureFor((RelationType) type, s);
		if (type instanceof UnionType)
			struct = this.createStructureFor((UnionType) type, s);
		//	save also the union of all unionset so that the objects are updated to belong to SystemSets Set too.
		final UnionSet systemAllSet = this.unionSetDAO.getUnionSetByName(SystemSetsNames.ALL_SETS);
		this.unionSetDAO.save(systemAllSet);
		return struct;
	}

	/**
	 * Create a Structure that represents the Set s as a Set of type type.
	 * 
	 * @param type
	 *            AtomType the type of the set
	 * @param s
	 *            Set instance to create a structure description for.
	 * @return a Structure instance that represents s in the System Set SystemAtomSets
	 */
	private Structure createStructureFor(final AtomType type, final Set s) {
		final StructureType systemType = this.structureTypeDAO.getByName(SystemSetsNames.TYPE_ATOM_SETS);
		final Set objSet = this.setDAO.getSetByName(SystemSetsNames.ATOM_SETS);
		final Map<String, LabelValue> structMap = new HashMap<String, LabelValue>();
		structMap.put("setID", new IntBasicValue((int) s.getId()));
		structMap.put("name", new StringBasicValue(s.getName()));
		structMap.put("mime", new StringBasicValue(type.getMimeString()));
		final Structure struct = this.structureDAO.create();
		struct.setStructureContent(new DescriptionValue(structMap));
		struct.setObjectType(systemType);
		this.structureDAO.save(struct);
		this.setDAO.addToSet(objSet, struct);
		this.structureDAO.save(struct);
		this.setDAO.save(objSet);
		return struct;
	}

	/**
	 * Create a Structure that represents the Set s as a Set of type type.
	 * 
	 * @param type
	 *            ObjType the type of the set
	 * @param s
	 *            Set instance to create a structure description for.
	 * @return a Structure instance that represents s in the System Set SystemObjSets
	 */
	private Structure createStructureFor(final ObjType type, final Set s) {
		final StructureType systemType = this.structureTypeDAO.getByName(SystemSetsNames.TYPE_OBJ_SETS);
		final Set objSet = this.setDAO.getSetByName(SystemSetsNames.OBJ_SETS);
		final Map<String, LabelValue> structMap = new HashMap<String, LabelValue>();
		structMap.put("setID", new IntBasicValue((int) s.getId()));
		structMap.put("name", new StringBasicValue(s.getName()));
		final Structure struct = this.structureDAO.create();
		struct.setStructureContent(new DescriptionValue(structMap));
		struct.setObjectType(systemType);
		this.structureDAO.save(struct);
		this.setDAO.addToSet(objSet, struct);
		this.structureDAO.save(struct);
		this.setDAO.save(objSet);
		return struct;
	}

	/**
	 * Create a Structure that represents the UnionSet s.
	 * 
	 * @param type
	 *            UnionType the type of the set
	 * @param s
	 *            Set instance to create a structure description for.
	 * @return a Structure instance that represents s in the System Set SystemUnionSets
	 */
	private Structure createStructureFor(final UnionType type, final Set s) {
		//creates the Structure
		final StructureType systemType = this.structureTypeDAO.getByName(SystemSetsNames.TYPE_UNION_SETS);
		final Set objSet = this.setDAO.getSetByName(SystemSetsNames.UNION_SETS);
		final Map<String, LabelValue> structMap = new HashMap<String, LabelValue>();
		structMap.put("setID", new IntBasicValue((int) s.getId()));
		structMap.put("name", new StringBasicValue(s.getName()));
		final Structure struct = this.structureDAO.create();
		struct.setStructureContent(new DescriptionValue(structMap));
		struct.setObjectType(systemType);
		this.structureDAO.save(struct);
		this.setDAO.addToSet(objSet, struct);
		this.structureDAO.save(struct);
		this.setDAO.save(objSet);
		log.debug("Saved Set with id = " + s.getId() + " in set " + objSet.getName() + " as structure with id = " + struct.getId());
		return struct;
	}

	/**
	 * Create a Structure that represents the RelationSet s.
	 * 
	 * @param type
	 *            RelationType type of s
	 * @param s
	 *            Set instance to create a structure description for.
	 * @return a Structure instance that represents s in the System Set SystemRelSets
	 */
	private Structure createStructureFor(final RelationType type, final Set s) {
		//creates the Structure
		final StructureType systemType = this.structureTypeDAO.getByName(SystemSetsNames.TYPE_REL_SETS);
		final Set objSet = this.setDAO.getSetByName(SystemSetsNames.REL_SETS);
		final Map<String, LabelValue> structMap = new HashMap<String, LabelValue>();
		structMap.put("setID", new IntBasicValue((int) s.getId()));
		structMap.put("name", new StringBasicValue(s.getName()));
		structMap.put("mult", new StringBasicValue(type.getMultiplicity().toString()));
		structMap.put("part", new StringBasicValue(type.getPartiality().toString()));
		final Structure struct = this.structureDAO.create();
		struct.setStructureContent(new DescriptionValue(structMap));
		struct.setObjectType(systemType);
		this.structureDAO.save(struct);
		this.setDAO.addToSet(objSet, struct);
		this.structureDAO.save(struct);
		this.setDAO.save(objSet);
		return struct;
	}

	/**
	 * Create a Structure that represents the Set s as a Set of type type.
	 * 
	 * @param type
	 *            StructureType the type of the set
	 * @param s
	 *            Set instance to create a structure description for.
	 * @return a Structure instance that represents s in the System Set SystemStructSets
	 */
	private Structure createStructureFor(final StructureType type, final Set s) {
		//creates the Structure
		final StructureType systemType = this.structureTypeDAO.getByName(SystemSetsNames.TYPE_STRUCT_SETS);
		final Set objSet = this.setDAO.getSetByName(SystemSetsNames.STRUCT_SETS);
		final DescriptionValueCollection dvc = new DescriptionValueCollection(DorotyObjectEnum.struct_def);
		dvc.setValueCollection(this.createLabelValueList(type.getDescriptionDefinition().getDescriptionDefinitionMap()));
		//dvc.setCollType((CollectionDescriptionType) type.getTypeForField("fields"));
		final Map<String, LabelValue> structMap = new HashMap<String, LabelValue>();
		structMap.put("setID", new IntBasicValue((int) s.getId()));
		structMap.put("name", new StringBasicValue(s.getName()));
		structMap.put("fields", dvc);
		final Structure struct = this.structureDAO.create();
		struct.setStructureContent(new DescriptionValue(structMap));
		struct.setObjectType(systemType);
		this.structureDAO.save(struct);
		this.setDAO.addToSet(objSet, struct);
		this.structureDAO.save(struct);
		this.setDAO.save(objSet);
		return struct;
	}

	/**
	 * Creates a list of LabelValue that can be used as parameter for a description value collection needed to represent
	 * part of the structure of a Set of kind structure. Each entry in the list is a DescriptionValue holding two
	 * fields: label and type. Length of the list is the size of the given map.
	 * 
	 * @param fieldsDeclMap
	 *            Map of String and LabelType
	 * @return a List of LabelValue describing the given map.
	 */
	private List<LabelValue> createLabelValueList(final Map<String, LabelType> fieldsDeclMap) {
		final List<LabelValue> dvList = new ArrayList<LabelValue>();
		for (final Entry<String, LabelType> e : fieldsDeclMap.entrySet()) {
			final Map<String, LabelValue> m = new HashMap<String, LabelValue>();
			m.put("label", new StringBasicValue(e.getKey()));
			m.put("type", new StringBasicValue(e.getValue().toString()));
			dvList.add(new DescriptionValue(m));
		}
		return dvList;
	}

	public StructureDAO getStructureDAO() {
		return this.structureDAO;
	}

	@Required
	public void setStructureDAO(final StructureDAO structureDAO) {
		this.structureDAO = structureDAO;
	}

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

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

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

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

	public StructureTypeDAO getStructureTypeDAO() {
		return this.structureTypeDAO;
	}

	@Required
	public void setStructureTypeDAO(final StructureTypeDAO structureTypeDAO) {
		this.structureTypeDAO = structureTypeDAO;
	}

	public UnionSetDAO getUnionSetDAO() {
		return this.unionSetDAO;
	}

	@Required
	public void setUnionSetDAO(final UnionSetDAO unionSetDAO) {
		this.unionSetDAO = unionSetDAO;
	}

}
