package edu.fiu.jtlex.data;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import java.util.List;

import java.util.stream.Collectors;

import org.jacop.constraints.PrimitiveConstraint;
import org.jacop.constraints.XeqY;
import org.jacop.constraints.XgtY;
import org.jacop.constraints.XltY;

import org.jacop.core.IntVar;
import org.jacop.core.Store;
import org.jacop.search.DepthFirstSearch;
import org.jacop.search.IndomainMin;
import org.jacop.search.IndomainRandom;
import org.jacop.search.InputOrderSelect;
import org.jacop.search.Search;
import org.jacop.search.SelectChoicePoint;

import edu.fiu.jtlex.data.ILink.ALinkType;
import edu.fiu.jtlex.data.ILink.TLinkType;

/**
 * Converts a TimeML Graph into a Temporal Constain Satisfation Problem (TCSP)
 * as follows:
 * 
 * Each node in the TimeML graph represents an event or time. These are
 * converted to time points in the TCSP. For each node N we get two time points
 * N- for the begining time and N+ for the end time. Each timeML link is
 * converted to a constrain in the TCSP following a mapping between TimeML links
 * types and primitive constrains.
 * 
 * @author luisrobaina, ceber003, pedrodiaz
 * 
 */
public class GraphTCSP {

	// Set of timePoints for the temporal constrain satisfaction problem.
	private IntVar[] timePoints;
	private IntVar[] timePointsRandom;
	// JACOP store object.
	private Store store;
	private Store storeRandom;
	private List<IntVar> solution;
	private List<IntVar> solutionRandom;
	
	/**
	 * Turn a timeML graph into a TCSP.
	 * 
	 * @param TimeML
	 */
	public GraphTCSP(IGraph TMLGraph) {

		final int TIMEPOINTSCOUNT = 2 * TMLGraph.getNodesCount();
	
		// Initialize set of time points variables.
		timePoints = new IntVar[TIMEPOINTSCOUNT];
		timePointsRandom = new IntVar[TIMEPOINTSCOUNT];

		ArrayList<INode> nodes = null;
		ArrayList<ILink> links = null;

		// Get nodes and links form the TimeML graph.
		Set<INode> nodesSet = TMLGraph.getNodes();
		if (nodesSet != null) {
			nodes = new ArrayList<>(nodesSet);
		}
		Set<ILink> linksSet = TMLGraph.getLinks();
		if (linksSet != null) {
			links = new ArrayList<>(linksSet);
		}
		// Map a node ID string to the node's timepoint index.
		HashMap<String, Integer> nodesIndex = new HashMap<>();

		// Represents the TCSP including the variables and the constrains.
		store = new Store();
		storeRandom = new Store();

		/*
		 * Step 1: From each node in the nodes set we get two discrete Integer variables
		 * in the TCSP. Each variable can take a value from 1 to TIMEPOINTSCOUNT.
		 */
		if (nodes != null) {
			for (int i = 0; i < nodes.size(); i++) {
				// Get the String ID for this node. Ex: e1.
				String nodeID = nodes.get(i).getIdStr();

				// This node's time points are in position i and i+1 in the timePoints array.
				nodesIndex.put(nodeID, 2 * i);

				// String identifiers for the beginning and end timepoints corresponding to
				// this node.
				String beginningTimePointID = nodeID + '-';
				String endTimePointID = nodeID + '+';

				timePoints[2 * i] = new IntVar(store, beginningTimePointID, 1, TIMEPOINTSCOUNT);
				timePoints[2 * i + 1] = new IntVar(store, endTimePointID, 1, TIMEPOINTSCOUNT);
				timePointsRandom[2 * i ] = new IntVar(storeRandom, beginningTimePointID, 1, TIMEPOINTSCOUNT);
				timePointsRandom[2 * i + 1] = new IntVar(storeRandom, endTimePointID, 1, TIMEPOINTSCOUNT);
				// For each node n, its timepoints must satisfy that n- < n+.
				store.impose(new XltY(timePoints[2 * i], timePoints[2 * i + 1]));
				storeRandom.impose(new XltY(timePointsRandom[2 * i], timePointsRandom[2 * i + 1]));

			}
		}

		/*
		 * For each link, generate a set of constrains (maximum of 2) for the TCSP.
		 */
		// Assert that there are links in this graph.
		if (linksSet != null) {
			for (ILink link : links) {
				// Get the nodes involved in this link.
				INode nodeA = link.getEventInstance();
				INode nodeB = link.getRelatedToEvent();

				// Get the link type.
				ILinkType linkType = link.getRelType();

				// Using the IDs of these nodes get where they are in the timePoints array.
				int nodeAIntVarIndex = nodesIndex.get(nodeA.getIdStr());
				int nodeBIntVarIndex = nodesIndex.get(nodeB.getIdStr());

				// Iterate over the constrains generated for this link (maximum of 2) and impose
				// it on the TCSP.
				ArrayList<PrimitiveConstraint> PCs = generateConstrain(linkType, nodeAIntVarIndex, nodeBIntVarIndex);
				for (PrimitiveConstraint PC : PCs) {
					// Impose the constrain into the TCSP.
					store.impose(PC);
				}
			}
		}
	}

	/**
	 * Assigns integer values to the JaCoP problems shortest solutions.
	 * 
	 * @return A list of strings corresponding to the timepoints and the numerical
	 *         value assigned to it for ordering or null if there is no solution.
	 */
	public List<IntVar> solveRandom() {

		Search<IntVar> search = new DepthFirstSearch<IntVar>();
		// Stop searching after 10 seconds.
		search.setTimeOut(10);
		// prevert JaCobs from printing logs to console.
		search.setPrintInfo(false);
	
		SelectChoicePoint<IntVar> selectRandom = new InputOrderSelect<IntVar>(store, timePoints, new IndomainRandom<IntVar>());

		boolean result = search.labeling(store, selectRandom);
		solution = new ArrayList<>();

		if (result) {

			for (int i = 0; i < timePoints.length; i++) {
				IntVar timePoint = timePoints[i];
				solution.add(timePoint);
			}
			solution.sort((IntVar TP1, IntVar TP2) -> compareIntVar(TP1, TP2));
			return solution;
		}

		return null;
	}
	
	public List<IntVar> solve() {

		Search<IntVar> search = new DepthFirstSearch<IntVar>();
		// Stop searching after 10 seconds.
		search.setTimeOut(10);
		// prevert JaCobs from printing logs to console.
		search.setPrintInfo(false);
	
		SelectChoicePoint<IntVar> select = new InputOrderSelect<IntVar>(store, timePoints, new IndomainMin<IntVar>());

		boolean result = search.labeling(store, select);
		solution = new ArrayList<>();

		if (result) {

			for (int i = 0; i < timePoints.length; i++) {
				IntVar timePoint = timePoints[i];
				solution.add(timePoint);
			}
			solution.sort((IntVar TP1, IntVar TP2) -> compareIntVar(TP1, TP2));
			return solution;
		}

		return null;
	}
	/**
	 * 
	 * @return
	 */
	public List<String> stringTimeLine(){
		List<String> solutionAsList = this.solution.stream().map(TP -> TP.toString()).collect(Collectors.toList());
		return solutionAsList;
	}
	/**
	 * Comprator for IntVar objects based on their assigned integer value.
	 * 
	 * @param TP1: An IntVar object.
	 * @param TP2: An IntVar object.
	 * @return : If TP1 is after TP2 then +1, if TP1 is before TP2 then -1, 0
	 *         otherwise.
	 */
	private int compareIntVar(IntVar TP1, IntVar TP2) {
		if (TP1.value() < TP2.value()) {
			return -1;
		}
		if (TP1.value() > TP2.value()) {
			return 1;
		}
		return 0;
	}

	/**
	 * Given a link type (ALink or TLink) this method generates the corresponding
	 * set of JACOP Primitive Constrain objects that should be enforced based on the
	 * type of link.
	 * 
	 * @param linkType:      An ALinkType or TLinkType enum value.
	 * @param nodeAVarIndex: The index of the IntVar that corresponds to the Node A
	 *                       of this Link.
	 * @param nodeBVarIndex: The index of the IntVar that corresponds to the Node B
	 *                       of this Link.
	 * @return An ArrayList with 1 or 2 PrimitiveConstraint objects.
	 * 
	 */
	private ArrayList<PrimitiveConstraint> generateConstrain(ILinkType linkType, int nodeAVarIndex, int nodeBVarIndex) {

		ArrayList<PrimitiveConstraint> constrains = new ArrayList<>();

		// Transform ALinks into equivalent TLinks constrain.
		if (linkType instanceof ALinkType) {

			// All transformations involve swapping the IntVars.
			int temp = nodeAVarIndex;
			nodeAVarIndex = nodeBVarIndex;
			nodeBVarIndex = temp;

			ALinkType ALink = (ALinkType) linkType;

			// Transform into corresponding TLink.
			switch (ALink) {
			case INITIATES:
				// A Initiates B is equivalent to B BEGUN_BY A.
				linkType = TLinkType.BEGUN_BY;
				break;
			case CULMINATES:
				// A CULMINATES B is equivalent to B ENDED_BY A.
				linkType = TLinkType.ENDED_BY;
				break;
			case TERMINATES:
				// A TERMINATES B is equivalent to B BEFORE A.
				linkType = TLinkType.BEFORE;
				break;
			case CONTINUES:
				// A CONTINUES B is equivalent to B INCLUDES A.
				linkType = TLinkType.INCLUDES;
				break;
			case REINITIATES:
				// A REINITIATES B is equivalent to B IS_INCLUDED A.
				linkType = TLinkType.IS_INCLUDED;
				break;
			}
		}

		// Get a reference to IntVars from the timePoints array.
		// Note that the Indexes could have been swapped if the Link was an ALinkType.

		// Timepoints for node A.
		IntVar nodeAMinus = timePoints[nodeAVarIndex];
		IntVar nodeAPlus = timePoints[nodeAVarIndex + 1];
		// Time points for node B.
		IntVar nodeBMinus = timePoints[nodeBVarIndex];
		IntVar nodeBPlus = timePoints[nodeBVarIndex + 1];

		if (linkType instanceof TLinkType) {
			TLinkType TLink = (TLinkType) linkType;

			switch (TLink) {

			case BEFORE:
				// Before TLink requires one constrains: A+ < B-.
				constrains.add(new XltY(nodeAPlus, nodeBMinus));
				break;
			case AFTER:
				// After TLink enforces A- > B+.
				constrains.add(new XgtY(nodeAMinus, nodeBPlus));
				break;
			case INCLUDES:
				// INCLUDES TLink requires two constrains: A- < B- AND B+ < A+.
				constrains.add(new XltY(nodeAMinus, nodeBMinus));
				constrains.add(new XltY(nodeBPlus, nodeAPlus));
				break;
			case IS_INCLUDED:
				// IS_INCLUDED TLink requires two constrains: B- < A- AND A+ < B+.
				constrains.add(new XltY(nodeBMinus, nodeAMinus));
				constrains.add(new XltY(nodeAPlus, nodeBPlus));
				break;
			case DURING:
				// DURING TLink requires two constrains: B- < A- AND A+ < B+.
				constrains.add(new XltY(nodeBMinus, nodeAMinus));
				constrains.add(new XltY(nodeAPlus, nodeBPlus));
				break;
			case DURING_INV:
				// DURING_INV TLink requires two constrains: A- < B- AND B+ < A+.
				constrains.add(new XltY(nodeAMinus, nodeBMinus));
				constrains.add(new XltY(nodeBPlus, nodeAPlus));
				break;
			case SIMULTANEOUS:
				// SIMULTANEOUS TLink requires two constraints: A- = B- AND A+ = B+.
				constrains.add(new XeqY(nodeAMinus, nodeBMinus));
				constrains.add(new XeqY(nodeAPlus, nodeBPlus));
				break;
			case IAFTER:
				// IAFTER TLink requires one constraint: B+ = A-.
				constrains.add(new XeqY(nodeBPlus, nodeAMinus));
				break;
			case IBEFORE:
				// IBEFORE TLink requires one constraint: A+ = B-.
				constrains.add(new XeqY(nodeAPlus, nodeBMinus));
				break;
			case IDENTITY:
				// IDENTITY TLink requires two constraints: A- = B- AND A+ = B+.
				constrains.add(new XeqY(nodeAMinus, nodeBMinus));
				constrains.add(new XeqY(nodeAPlus, nodeBPlus));
				break;
			case BEGINS:
				// BEGINS TLink requires two constraints: A- = B- AND A+ < B+.
				constrains.add(new XeqY(nodeAMinus, nodeBMinus));
				constrains.add(new XltY(nodeAPlus, nodeBPlus));
				break;
			case ENDS:
				// ENDS TLink requires two constraints: B- < A- AND A+ = B+.
				constrains.add(new XltY(nodeBMinus, nodeAMinus));
				constrains.add(new XeqY(nodeAPlus, nodeBPlus));
				break;
			case BEGUN_BY:
				// BEGUN_BY TLink requires two constraints: A- = B- AND B+ < A+.
				constrains.add(new XeqY(nodeAMinus, nodeBMinus));
				constrains.add(new XltY(nodeBPlus, nodeAPlus));
				break;
			case ENDED_BY:
				// ENDED_BY TLink requires two constraints: A- < B- AND A+ < B+.
				constrains.add(new XltY(nodeAMinus, nodeBMinus));
				constrains.add(new XltY(nodeAPlus, nodeBPlus));
				break;
			}
		}
		// Return the set of constraints (maximum of 2).
		return constrains;
	}

	/**
	 * This method is meant to allow for getting the values held in the store.
	 * 
	 * @return store
	 */
	public Store getStore() {
		return store;
	}
}
