package eu.dnetlib.dlms.epub;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;

import eu.dnetlib.dlms.lowlevel.objects.LLDigitalObject;
import eu.dnetlib.dlms.lowlevel.objects.Relation;
import eu.dnetlib.dlms.lowlevel.objects.RelationDAO;
import eu.dnetlib.dlms.lowlevel.objects.RelationSet;
import eu.dnetlib.dlms.lowlevel.objects.RelationSetDAO;
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.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.union.objects.UnionSetDAO;

/**
 * Class to generate samples for the Enhanced Publication Document Model. It uses the sets and types generated by class
 * EPubModelGenerator. Calling the method of this class will lead to the creation of a simple enhnaced publication that
 * has one main ePrint (linked to the hasEPrint relationship) and another ePrint linked thorugh the composedBy
 * relationship. The objects in the system after the call to generateSamples() method are:
 * <ul>
 * <li>
 * 2*numOfePrints ePrints: they are related to each other 2 by 2 through the 'cites' relation. They are not linked to
 * any enhanced publication.</li>
 * <li>numOfePubs enhanced publications: they are not related to each other, but each of them has a relation with:
 * <ul>
 * one ePrints, relation label = 'hasEPrint'
 * <li></li>numOfePrints -1 ePrints, relation label = 'composedBy'
 * <li></li>
 * </ul>
 * </li>
 * </ul>
 * Totally: <code>
 * #ePrints = 2*numOfePrints + numOfePubs * numOfePrints [if numOfePrints = 3 and numOfePubs = 5 #ePrints = 21]
 * #ePubs = numOfePubs
 * #wrappers = numOfePubs*(numOfePrints-1) [if numOfePrints = 3 and numOfePubs = 5 #wrappers = 10]
 * #relations = numOfePrints + numOfePubs * numOfePrints  [if numOfePrints = 3 and numOfePubs = 5 #relations = 18]
 * Total number of objects (sets excluded) is the sum of those values. [if numOfePrints = 3 and numOfePubs = 5 total = 54].
 * </code>
 * 
 * @author alessia
 * 
 */
public class EPubSamplesGenerator {

	/** Logger. */
	private static final Log log = LogFactory.getLog(EPubSamplesGenerator.class);
	/** DAO for union sets. */
	private UnionSetDAO unionSetDAO;
	/** DAO for relations. */
	private RelationDAO relDAO;
	/** DAO for relation sets. */
	private RelationSetDAO relSetDAO;
	/** DAO for sets. */
	private SetDAO setDAO;
	/** DAO for structures. */
	private StructureDAO structDAO;
	/** int value used to generate structure's fields values. */
	private int uuidCounter = 0;
	/** Number of objects in set ePubs to generate. */
	private int numOfEPubs;
	/** Number of objects in set ePrints to generate for each object in ePubs. */
	private int numOfePrints;
	/** Cardinality of set ePrints after sample generation. */
	private int totalEPrints = 0;
	/** Cardinality of set ePubs after sample generation. */
	private int totalEPubs = 0;
	/** Number of created wrappers. */
	private int totalWrappers = 0;
	/** Number of created relations . */
	private int totalRelations = 0;

	/**
	 * Generates and saves 2 ePrints in the ePrints' set and links them through a Relation in Set cites.
	 * 
	 * @param i
	 *            index to use to build the title and the info of the two ePrints together with the uuidCounter
	 */
	public void createEPrints(final int i) {
		final Set ePrints = this.getSetDAO().getSetByName("ePrints");
		final Structure ePrints1 = this.createStructure(ePrints, "ePrints-" + i + "-" + this.uuidCounter, "Title of ePrints-" + i + "-"
				+ this.uuidCounter, "IT", "uuid-sample-" + this.uuidCounter);
		this.totalEPrints++;
		this.uuidCounter++;
		final Structure ePrints2 = this.createStructure(ePrints, "ePrints-" + i + "-" + this.uuidCounter, "Title of ePrints-" + i + "-"
				+ this.uuidCounter, "EN", "uuid-sample-" + this.uuidCounter);
		this.totalEPrints++;
		this.uuidCounter++;
		this.setDAO.addToSet(ePrints, ePrints1);
		this.setDAO.addToSet(ePrints, ePrints2);
		this.setDAO.save(ePrints);
		final RelationSet citesSet = this.relSetDAO.getRelationSetByLabel("cites");
		this.createRelation(citesSet, ePrints1, ePrints2, "A Relation between two ePrint");
		this.uuidCounter++;
		log.debug("EXPECTED COMMIT");
	}

	/**
	 * Creates an enhnaced publication and saves it in the Set ePubs. The enhnaced publication is composed by one main
	 * ePrint linked through a "hasEPrint" relationship and numOfePrints - 1 secondary ePrints linked through
	 * "composedBy" relationships.
	 * 
	 * @param i
	 *            index to use to build the title and the info of the enhnaced publication and its related ePrints
	 *            together with the uuidCounter
	 */
	public void createEPub(final int i) {
		final Set ePubs = this.getSetDAO().getSetByName("ePubs");
		final Set ePrints = this.getSetDAO().getSetByName("ePrints");
		final Structure ePub = this.createStructure(ePubs, "Enhnanced publication #" + i, "EPub-" + this.uuidCounter + " for sample", "IT", "uuid"
				+ this.uuidCounter);
		this.totalEPubs++;
		this.uuidCounter++;
		for (int k = 0; k < this.numOfePrints; k++) {
			//links with ePrints:
			final Structure ePrints1 = this.createStructure(ePrints, "ePrint sample", "Title of ePrints-" + i + "-" + k + "-" + this.uuidCounter, "IT",
					"uuid-sample-" + this.uuidCounter);
			this.setDAO.addToSet(ePrints, ePrints1);
			this.setDAO.save(ePrints);
			this.totalEPrints++;
			if (k == 0) {
				//the link between ePub and the first ePrints
				final RelationSet hasEPrint = this.relSetDAO.getRelationSetByLabel("hasEPrint");
				this.createRelation(hasEPrint, ePub, ePrints1, "Relation between the first enhanced publication with its main ePrint");
			} else {
				//let's ad the eprint to components and add a composedBy relationships
				final RelationSet composedBy = this.relSetDAO.getRelationSetByLabel("composedBy");
				final String info = "Relation between my first enhanced publication and a secondary ePrint";
				this.createRelation(composedBy, ePub, ePrints1, info);
			}
		}
		this.setDAO.addToSet(ePubs, ePub);
		this.setDAO.save(ePubs);
		log.debug("EXPECTED COMMIT");
	}

	/**
	 * Creates and saves a Structure object in the givenSet using the provided arguments as info, title, language and
	 * objIdentifier to build up the structure's content.
	 * 
	 * @param theSet
	 *            Set of type Structure
	 * @param info
	 *            desriptive string about the Structure
	 * @param title
	 *            dc:title of the structure
	 * @param language
	 *            dc:language of the structure
	 * @param objIdentifier
	 *            dri:objIdentifier of the structure
	 * @return the Structure created and saved in theSet
	 */
	private Structure createStructure(final Set theSet, final String info, final String title, final String language, final String objIdentifier) {
		final Structure struct = this.structDAO.create();
		struct.setInfo(info);
		struct.setObjectType(theSet.getType());
		struct.setStructureContent(this.fillDMFStructure(title, language, objIdentifier));
		this.structDAO.save(struct);
		return struct;
	}

	/**
	 * Creates the content for a Structure object based on the provided arguments.
	 * 
	 * @param title
	 *            dc:title value
	 * @param language
	 *            dc:language value
	 * @param objIdentifier
	 *            dri:objIdentifier value
	 * @return a DescriptionValue instance ready to be set to a Structure's content
	 */
	private DescriptionValue fillDMFStructure(final String title, final String language, final String objIdentifier) {
		final DescriptionValue dv = new DescriptionValue();
		final Map<String, LabelValue> fields = new HashMap<String, LabelValue>();
		fields.put("dc:title", new StringBasicValue(title));
		log.debug("Added field dc:title = " + title);
		fields.put("dc:language", new StringBasicValue(language));
		log.debug("Added field dc:language = " + language);
		fields.put("dri:objIdentifier", new StringBasicValue(objIdentifier));
		log.debug("Added field dri:objIdentifier = " + objIdentifier);
		dv.setStructureFields(fields);
		return dv;
	}

	/**
	 * Logs info about the number of objects created sofar.
	 */
	public void printResultInfo() {
		log.info("# ePrints = " + this.totalEPrints);
		log.info("# ePubs = " + this.totalEPubs);
		log.info("# wrappers = " + this.totalWrappers);
		log.info("# relations = " + this.totalRelations);
	}

	/**
	 * Creates ans saves a Relation object in the given RelationSet, using fst and snd as linked object and the given
	 * string as info.
	 * 
	 * @param relSet
	 *            RelationSet
	 * @param fst
	 *            LLDigitalObject relation source
	 * @param snd
	 *            LLDigitalObject relation target
	 * @param info
	 *            descriptive string about the relation
	 */
	private void createRelation(final RelationSet relSet, final LLDigitalObject fst, final LLDigitalObject snd, final String info) {
		final Relation r = this.relDAO.create();
		r.setFstObj(fst);
		r.setSndObj(snd);
		r.setInfo(info);
		r.setObjectType(relSet.getType());
		this.relDAO.save(r);
		this.relSetDAO.addToSet(relSet, r);
		this.relSetDAO.save(relSet);
		this.totalRelations++;
	}

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

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

	@Required
	public void setRelDAO(final RelationDAO relDAO) {
		this.relDAO = relDAO;
	}

	public RelationDAO getRelDAO() {
		return this.relDAO;
	}

	@Required
	public void setRelSetDAO(final RelationSetDAO relSetDAO) {
		this.relSetDAO = relSetDAO;
	}

	public RelationSetDAO getRelSetDAO() {
		return this.relSetDAO;
	}

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

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

	@Required
	public void setStructDAO(final StructureDAO structDAO) {
		this.structDAO = structDAO;
	}

	public StructureDAO getStructDAO() {
		return this.structDAO;
	}

	@Required
	public void setNumOfePrints(final int numOfePrints) {
		this.numOfePrints = numOfePrints;
	}

	public int getNumOfePrints() {
		return this.numOfePrints;
	}

	@Required
	public void setNumOfEPubs(final int numOfEPubs) {
		this.numOfEPubs = numOfEPubs;
	}

	public int getNumOfEPubs() {
		return this.numOfEPubs;
	}

}
