package ua.ansymo.hintco

import java.util.List
import java.util.Map
import java.util.Set
import org.eclipse.core.runtime.Assert
import org.intocps.fmi.Fmi2Status
import org.intocps.fmi.FmuInvocationException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import ua.ansymo.hintco.HierarchicalCosimUnit
import ua.ansymo.hintco.HierarchicalUnitPort
import ua.ansymo.hintco.InputPortInstance
import ua.ansymo.hintco.OutputPortInstance
import ua.ansymo.hintco.Scenario
import ua.ansymo.hintco.UnitInstance

import static ua.ansymo.hintco.CosimRunUtils.*

class HierarchicalInstance extends AdaptedFMUInstance {
	
	FmuLoader loader
	List<PrecedenceNode> nodes
	HierarchicalCosimUnit unit
	Map<UnitInstance, IFmuInstance> fmuInstanceMap = newHashMap()
	
	Logger logger = LoggerFactory.getLogger(typeof(HierarchicalInstance))
	
	List<UnitInstance> units
	
	List<OutputPortInstance> childOutputPorts
	
	Set<InputPortInstance> inPorts
	
	new(HierarchicalCosimUnit u, Map<Scenario, List<PrecedenceNode>> scenarioNodesMap, FmuLoader l) {
		super(u, scenarioNodesMap)
		loader = l
		unit = u
		Assert.isTrue(scenarioNodesMap.containsKey(unit))
		
		// Ignore units own output ports 
		nodes = scenarioNodesMap.get(unit).filter[!unit.ports.contains(it)].toList
		
		inPorts = nodes.filter(InputPortInstance).filter[it.input].toSet
		
		// Ignore hierarchical ports that are inputs.
		childOutputPorts = nodes.filter(OutputPortInstance).filter[!it.isInput].toList
		
		// Instantiate subunits.
		units = nodes.filter(UnitInstance).toList		
		instantiateUnits(units, fmuInstanceMap, loader, scenarioNodesMap)
	}

	override setupExperiment() {
		// setup experiment
		setupExperiment(units, fmuInstanceMap)
		return Fmi2Status.OK
	}
	
	override enterInitializationMode() throws FmuInvocationException {
		intoInitMode(units, fmuInstanceMap)
		return Fmi2Status.OK
	}
	
	override exitInitializationMode() throws FmuInvocationException {
		// Go into init mode
		exitInitMode(units, fmuInstanceMap)
		return Fmi2Status.OK
	}
	
	override setInnerReal(String portName, double v) {
		/*
		 * This method is called after the adaptations are processed.
		 * If we are in initialization mode, then we need to propagate the values to the internal units, and trigger their propagation.
		 * Otherwise, just set the value on the internal ports.
		 */
		logger.debug(">{}.setInnerReal({},{})", unit.identifier, portName, v)
		
		val unitPort = unit.ports.findFirst[it.identifier == portName]
		Assert.isNotNull(unitPort)
		Assert.isTrue(unitPort instanceof HierarchicalUnitPort)
		val hUnitPort = unitPort as HierarchicalUnitPort
		Assert.isTrue(hUnitPort.input)
		
		// TODO: This is wrong. It should only be called when we need to set that input. For now, it should just be stored in a buffer.
		// Except during the initialization phase.
		for (trgPort : hUnitPort.valueTo) {
			val trgUnit = fmuInstanceMap.get(trgPort.unit)
			trgUnit.setReal(trgPort.identifier, v)
		}

		// trigger value propagation
		if (currentMode == MODE.INITING) {
			val MAX_IT = 10 // TODO Should be a config parameter.
			fixPointOutputs(childOutputPorts, fmuInstanceMap, MAX_IT)
		}
	}
	
	override getInnerReal(String portName) {
		/*
		 * This method is called after the adaptations are processed.
		 * Just get the value on the internal ports.
		 */
		val unitPort = unit.ports.findFirst[it.identifier == portName]
		Assert.isNotNull(unitPort)
		Assert.isTrue(unitPort instanceof HierarchicalUnitPort)
		val hUnitPort = unitPort as HierarchicalUnitPort
		Assert.isTrue(! hUnitPort.input)
		
		val srcPort = hUnitPort.valueFrom
		Assert.isNotNull(srcPort)
		
		val srcUnit = fmuInstanceMap.get(srcPort.unit)
		
		return srcUnit.getReal(srcPort.identifier)
	}
	
	override doInnerStep(double inner_time, double micro_step){
		/*
		 * This method is called after the adaptations are processed.
		 * The input values have been propagated.
		 * All we have to do now is step the inner units, and propagate their values in order.
		 */
		 executeStep(unit, inner_time, micro_step, nodes, units, inPorts, fmuInstanceMap, null)
		 return Fmi2Status.OK
	}
	
	override outputSnapshot(Map<OutputPortInstance, Double> outVals, Set<OutputPortInstance> filter) {
		getOutputSnapshot(units, fmuInstanceMap, outVals, filter)
		super.outputSnapshot(outVals, filter)
	}
	
	override free(){
		freeUnits(units, fmuInstanceMap)
	}
	
	override reset() {
		// This reset is never called in the case of hard reset coming from parent scenarios.
		resetUnits(units, fmuInstanceMap, null, null, false)
	}
	
}