package ua.ansymo.hintco

import be.uantwerpen.cosys.contractLang.ContractSet
import be.uantwerpen.cosys.contractLang.FMUProperty
import be.uantwerpen.cosys.contractLang.FMUPropertyEnum
import be.uantwerpen.cosys.contractLang.IntValue
import be.uantwerpen.cosys.contractLang.Property
import java.util.List
import org.eclipse.core.runtime.Assert
import org.eclipse.emf.ecore.util.EcoreUtil
import ua.ansymo.hintco.Adaptation
import ua.ansymo.hintco.HintcoFactory
import ua.ansymo.hintco.CosimUnitInstance
import ua.ansymo.hintco.ExtrapolationAdaptation
import ua.ansymo.hintco.HintConfiguration
import ua.ansymo.hintco.InputPortInstance
import ua.ansymo.hintco.InterpolationAdaptation
import ua.ansymo.hintco.OutputPortInstance
import ua.ansymo.hintco.RootCandidateScenario

/*
 * Main purpose: take a hint model, and generate a candidate model that we assume will best implement the hints.
 * If no hint is provided, generates the default hints.
 */
class CandidateSpaceGenerator {
	
	def createCandidateSpace(ContractSet hints, HintConfiguration cs){
		
		// Assume that there are no adaptations
		Assert.isTrue(cs.eAllContents.filter(Adaptation).empty)
		
		// For now, execute each candidate handler in sequence.
		// In the future, we might want to refactor this to include conflict resolution, etc...
		handleSoftwareHints(hints, cs)
		
		return cs
	}
	
	def handleSoftwareHints(ContractSet hints, HintConfiguration cs) {
		/*
		 * Assumes that there is only one software controller fmu.
		 * Then sets the scenario step size to the frequency of the software controller.
		 * Then sets every other fmu to a higher rate, giving preference to interpolations over extrapolations.
		 * The ports that are fed by the outputs of the software controller have zero order extrapolations/interpolations
		 */
	 	if (!(hints.eAllContents.filter(FMUProperty).filter[p | p.property === FMUPropertyEnum.EXEC_RATE].size == 1)){
	 		return 
	 	}
		
	 	Assert.isNotNull(cs.candidates.head)
	 	
	 	val prop = hints.eAllContents
	 						.filter(Property)
	 						.findFirst[p |
	 							val v = p.^var
	 							if (v instanceof FMUProperty){
		 							v.property == FMUPropertyEnum.EXEC_RATE &&
		 							p.expr instanceof IntValue
	 							} else {
	 								false
	 							}
	 						]
	 						
//		val scenario = duplicateScenario(cs.candidates.head)
//		cs.candidates.add(scenario)
//		val maxScenarioWeight = cs.candidates.map[c | c.weight].max
//		scenario.weight = maxScenarioWeight + 1
		val scenario = cs.candidates.head
		
		val hz = (prop.expr as IntValue).v
		
		scenario.stepSize = 1.0/hz
		
		val ctrlUnitName = (prop.^var as FMUProperty).fmu.name.stripQuotes
		val ctrlUnit = scenario.cosimunits.findFirst[u | u.identifier == ctrlUnitName] as CosimUnitInstance
		Assert.isNotNull(ctrlUnit, "Contracts refers to a non existing unit: " + ctrlUnitName)
		
		for (outP : ctrlUnit.ports.filter(OutputPortInstance)){
			for(inP : outP.valueTo){
				createDefaultInPortApproximations(inP, 0)
			}
		}
		
		val otherUnits = scenario.cosimunits.filter[u | u.identifier != ctrlUnitName]
		for (otherUnit : otherUnits) {
			Assert.isTrue(otherUnit instanceof CosimUnitInstance)
			createDefaultInPortsApproximations(otherUnit as CosimUnitInstance, #[10])
		}
	}
	
	def stripQuotes(String string) {
		string.subSequence(1, string.length-1)
	}
	
	def duplicateScenario(RootCandidateScenario scenario) {
		val newScenario = EcoreUtil.copy(scenario)
		return newScenario
	}
	
	def createDefaultCandidateSpace(HintConfiguration cs) {
		/*
		 * Creates a "good for general purpose" candidate scenario space.
		 * The rule of thumb is:
		 * 	- prefer interpolations over extrapolations
		 *  - avoid using mixed approximations: if one input port is interpolation, the others should be as well.
		 */
		
		// assume that there is no adaptations in any port, and there is only one scenario
		Assert.isTrue(!cs.eAllContents.exists[o | o instanceof Adaptation])
		Assert.isTrue(cs.eAllContents.filter[s | s instanceof RootCandidateScenario].size === 1)
		
		val scenario = cs.candidates.head
		
		for (unit : scenario.cosimunits) {
			Assert.isTrue(unit instanceof CosimUnitInstance)
			createDefaultInPortsApproximations(unit as CosimUnitInstance, #[10])
		}
		
		Assert.isTrue(cs.eAllContents.filter(CosimUnitInstance).forall[u | 
			val interPs = u.ports.filter(InterpolationAdaptation).toList
			interPs.forall[a | a.implies.containsAll(interPs)]
			])
		Assert.isTrue(cs.eAllContents.filter(CosimUnitInstance).forall[u | 
			val extraPs = u.ports.filter(ExtrapolationAdaptation).toList
			extraPs.forall[a | a.implies.containsAll(extraPs)]
			])
		
		return cs
	}
	
	def createDefaultInPortsApproximations(CosimUnitInstance unit, List<Integer> rates) {
		val f = HintcoFactory.eINSTANCE
		
		// Add multi-rate adaptation
		{
			if (rates.size == 1){
				val mr = f.createMultiRateAdaptation
				unit.adaptation = mr
				mr.rate = 10				
			} else {
				val xor = f.createXorUnitAdaptation()
				unit.adaptation = xor
				for (r : rates){
					val mr = f.createMultiRateAdaptation
					xor.children.add(mr)
					mr.rate = r	
				}
			}
		}
		{
			val inPorts = unit.ports.filter(InputPortInstance)
			for (inPort : inPorts) {
				createDefaultInPortApproximations(inPort, 1)
			}
		}
		
		// Add implies constraints
		val intraAdaptations = unit.eAllContents.filter(InterpolationAdaptation).toList
		val extraAdaptations = unit.eAllContents.filter(ExtrapolationAdaptation).toList
		val sI = intraAdaptations.size
		val sE = extraAdaptations.size
		Assert.isTrue(sI === sE)
		for (aS : intraAdaptations){
			for (aT : intraAdaptations){
				aS.implies.add(aT)
			}
			aS.implies.add(unit.adaptation)
		}
		for (aS : extraAdaptations){
			for (aT : extraAdaptations){
				aS.implies.add(aT)
			}
			aS.implies.add(unit.adaptation)
		}
	}
	
	
	def createDefaultInPortApproximations(InputPortInstance inPort, int order) {
		if (inPort.adaptation !== null){
			return
		}
		val f = HintcoFactory.eINSTANCE
		val xor = f.createXorPortAdaptation()
		inPort.adaptation = xor
		{
			val extra = f.createExtrapolationAdaptation()
			xor.children.add(extra)
			extra.weight = 0
			extra.order = order
		}
		{
			val intra = f.createInterpolationAdaptation()
			xor.children.add(intra)
			intra.weight = 10
			intra.order = order
		}
	}
	
}