package ua.ansymo.hintco

import org.eclipse.core.runtime.Assert
import org.jgrapht.graph.DefaultWeightedEdge
import org.jgrapht.graph.DirectedMultigraph
import org.jgrapht.graph.SimpleDirectedWeightedGraph
import org.jgrapht.util.SupplierUtil
import org.eclipse.xtend.lib.annotations.Accessors

/*
 * Represents the search space used in the algebraic loop optimizer. Lazily expands the graph as the search algorithm works it's way towards the optimal solution.
 */
class LazySearchTree extends SimpleDirectedWeightedGraph<LazySearchNode, DefaultWeightedEdge> {
	
	DirectedMultigraph<CosimUnitInstance, DefaultWeightedEdge> triggerGraph
	
	@Accessors(PUBLIC_GETTER, NONE) EmptyTriggerSequence root = new EmptyTriggerSequence
	@Accessors(PUBLIC_GETTER, NONE) CompleteTriggerSequence bottom = new CompleteTriggerSequence
	
	
	new(DirectedMultigraph<CosimUnitInstance, DefaultWeightedEdge> tg) {
		super(SupplierUtil.createSupplier(LazySearchNode), SupplierUtil.createSupplier(DefaultWeightedEdge))
		Assert.isTrue(!tg.vertexSet.empty)
		triggerGraph = tg
		// Define the default start and end nodes.
		this.addVertex(root)
		this.addVertex(bottom)
	}
	
	/*
	 * Computes the edges and vertices that are outgoing from vertex, and adds them to the graph.
	 * It considers three kinds of LazySearchNode: the EmptyTriggerSequence, the PartialTriggerSequence, and the CompleteTriggerSequence.
	 * In the case of EmptyTriggerSequence, then the successors are all possible vertices on the graph.
	 * In the case of PartialTriggerSequence, then the successors are all possible vertices on the graph, except for those that are already part of the trigger sequence (this is naive, see below).
	 * In the case of CompleteTriggerSequence, then there are no successors, and an exception should be thrown.
	 * I think it is possible to reduce the successors in the case of a PartialTriggerSequence. Essentially, one would expect the optimal trigger sequence to be a path in the triggerGraph. 
	 * This is because whenever we pick a cosim unit A, any cosim units that depend on A have their cost reduced, so it would make sense that the optimal choice will be among them. 
	 * However, for cosim scenarios that results in non strongly connected graphs, one may be forced to chose a successor which is not a successor of A in triggerGraph.
	 * Therefore I'm going for the most naive approach to just enum all, and let A* select the best choice.
	 */
	override outgoingEdgesOf(LazySearchNode vertex) {
		if (vertex instanceof EmptyTriggerSequence) {
			Assert.isTrue(vertex.inDegreeOf == 0, "This vertex should be the root of the search tree.")
			for (v : triggerGraph.vertexSet) {
				createNewEdge(vertex, v)
			}
		} else if (vertex instanceof CompleteTriggerSequence) {
			throw new IllegalArgumentException()
		} else if (vertex instanceof PartialTriggerSequence) {
			val potentialSuccessors = triggerGraph.vertexSet.filter[v | !recIsAncestorOf(v, vertex)]
			if (potentialSuccessors.empty) {
				val edge = this.addEdge(vertex, bottom)
				Assert.isNotNull(edge) // Otherwise, this vertex has been expanded before, the solution found, and then the search problem got back here.
				this.setEdgeWeight(edge, 0.0) // No cost for the edge that reaches bottom
			} else {
				for (v : potentialSuccessors) {
					createNewEdge(vertex, v)
				}
			}
			
		}
		return super.outgoingEdgesOf(vertex)
	}
	
	private def createNewEdge(LazySearchNode vertex, CosimUnitInstance v) {
		val newV = new PartialTriggerSequence(v)
		val newEdge = this.addEdge(vertex, newV)
		this.setEdgeWeight(newEdge, costOfEdge(newV))
	}
	
	def boolean recIsAncestorOf(CosimUnitInstance ancestorUnit, PartialTriggerSequence vertex) {
		// Base case
		val predecessors = vertex.incomingEdgesOf
		Assert.isTrue(!predecessors.empty)
		if (predecessors.size == 1) {
			Assert.isTrue(predecessors.head.edgeSource instanceof EmptyTriggerSequence)
			return false
		}
		for (p : predecessors) {
			val partialP = p.edgeSource as PartialTriggerSequence
			if (partialP.unit == ancestorUnit) {
				return true
			}
			if (recIsAncestorOf(ancestorUnit, partialP)){
				return true
			}
		}
		return false
	}
	
	/*
	 * Defines the cost of an edge A->B. 
	 * This is given by the sum of the weights of incoming connections to B (in the triggerGraph) that are not ancestors of B in the search tree.
	 */
	def costOfEdge(PartialTriggerSequence B){
		val Bunit = B.unit
		val incommingEdges = triggerGraph.incomingEdgesOf(Bunit).filter[inc | recIsAncestorOf(triggerGraph.getEdgeSource(inc), B)]
		val cost = incommingEdges.map[edge|triggerGraph.getEdgeWeight(edge)].reduce[p1, p2|p1+p2]
		return cost
	}
}

abstract class LazySearchNode {
	
}

/*
 * Represents a partial trigger sequence of a graph.
 * 
 */
class PartialTriggerSequence extends LazySearchNode {
	@Accessors(PUBLIC_GETTER, PROTECTED_SETTER) CosimUnitInstance unit
	new (CosimUnitInstance u){
		unit = u	
	}
	override toString() {
		unit.toString
	}
}

/*
 * The following two classes are used to identify the start and end of the search
 */
class EmptyTriggerSequence extends LazySearchNode {}
class CompleteTriggerSequence extends LazySearchNode {}


