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.intocps.fmi.IFmiComponent
import org.slf4j.Logger
import org.slf4j.LoggerFactory

enum MODE {
  NOT_INIT,
  INITING,
  INITED
}

class AdaptedFMUInstance extends FmuInstance {
	
	var int innerRate = 1
	Map<String, List<IAdaptedInputPort>> adaptedInPorts = newHashMap()
	Map<String, IAdaptedOutputPort> adaptedOutPorts = newHashMap()
	
	Logger logger = LoggerFactory.getLogger(typeof(AdaptedFMUInstance))
	
	protected MODE currentMode
	
	public UnitInstance unit
	
	List<OutputPortInstance> outputPorts
	
	new(UnitInstance u, IFmiComponent f,  Map<Scenario, List<PrecendenceNode>> scenarioNodesMap) {
		super(f)
		unit = u
		instantiateAdaptations(unit, scenarioNodesMap)
		currentMode = MODE.NOT_INIT
		outputPorts = unit.outputPorts
	}
	
	new(UnitInstance unit, Map<Scenario, List<PrecendenceNode>> scenarioNodesMap) {
		this(unit, null, scenarioNodesMap)
	}
	
	def instantiateAdaptations(UnitInstance unit, Map<Scenario, List<PrecendenceNode>> scenarioNodesMap) {
		val unitAdaptations = unit.selectedAdaptations
		
		if (!unitAdaptations.empty){
			// Check if unit has a multi-rate adaptation
			val adaptation = unitAdaptations.head
			if (adaptation instanceof MultiRateAdaptation) {
				innerRate = adaptation.rate				
			}
			
			// Check if there is a power bond adaptation
			if (adaptation instanceof PowerBondAdaptation) {
				
				val siblings = scenarioNodesMap.get(unit.scenario)
				val dualAdaptation = adaptation.dual
				// Create Store input for the input port that goes in the power calculation
				//  if this unit has to extrapolate one of the power inputs, then it is the one that is going to make the correction.
				if (adaptation.effort instanceof InputPortInstance){
					addPowerPortAdaptations(adaptation, adaptation.effort, dualAdaptation.effort, siblings)
				} else {
					addPowerPortAdaptations(adaptation, adaptation.flow, dualAdaptation.flow, siblings)
				}
			}
		}
		
		// Instantiate each adaptation input port of the unit.
		for (p : unit.ports.filter(InputPortInstance)){
			// Check which adaptation is selected for the input port, and instantiate the appropriate class.
			val portAdaptations = p.selectedAdaptations
			if (!portAdaptations.empty){
				val portAdaptation = portAdaptations.head
				Assert.isTrue(portAdaptation.adapted === p)
				switch (portAdaptation) {
					InterpolationAdaptation case portAdaptation.order == 1: {
						addInAdaptation(p.identifier, new InterpolationAdaptedPort(p.identifier, this))
					}
					ExtrapolationAdaptation case portAdaptation.order == 1: {
						addInAdaptation(p.identifier, new ExtrapolationAdaptedPort(p.identifier, this))
					}
				}
				
				// Check for incompatibilities
				if (adaptedInPorts.containsKey(p.identifier)){
					// Port approximations are not compatible with power input corrections.
					// We need a better way to coordinate which port adaptations gets to propagate its value to the internal fmu.
					val noApps = adaptedInPorts.get(p.identifier).filter(ApproximationAdaptation).empty
					val noPCs = adaptedInPorts.get(p.identifier).filter(PowerCorrectionPort).empty
					Assert.isTrue((!noApps && noPCs) 
									|| (noApps && !noPCs)
									|| (noApps && noPCs))
				}
			}
		}
		
	}
	
	def private addPowerPortAdaptations(PowerBondAdaptation adaptation, PortInstance inputPowerTerm, PortInstance dualPowerTerm, List<PrecendenceNode> siblings) {

		var StoreAdaptedPort powerTermStorage
		
		var PowerOutAdaptedPort powerOutAdaptor
		
		val powerInStorage = new StoreAdaptedPort()
		
		if (executesBefore(dualPowerTerm, dualPowerTerm.unit, siblings)){
			powerTermStorage = new StoreAdaptedPort()
			powerOutAdaptor = new PowerOutAdaptedPort(adaptation, powerTermStorage, this)
			val powerCorrectionPort = new PowerCorrectionPort(inputPowerTerm.identifier, powerOutAdaptor, powerInStorage, this)
			
			addInAdaptation(inputPowerTerm.identifier, powerCorrectionPort)
		} else {
			powerTermStorage = new FeedThroughAdaptedPort(inputPowerTerm.identifier, this)
			powerOutAdaptor = new PowerOutAdaptedPort(adaptation, powerTermStorage, this)
		}
		
		// Create new adapted output for the pOut port
		adaptedOutPorts.put(adaptation.POut.identifier, powerOutAdaptor)
		
		// Add storage to input power term.
		addInAdaptation(inputPowerTerm.identifier, powerTermStorage)
		
		// Create adapted input for the pIn port.
		addInAdaptation(adaptation.PIn.identifier, powerInStorage)
		
		
		
		return powerTermStorage
	}
	
	def private executesBefore(PortInstance port, UnitInstance unit, List<PrecendenceNode> nodes) {
		nodes.indexOf(port) < nodes.indexOf(unit)
	}
	
	override enterInitializationMode() throws FmuInvocationException {
		currentMode = MODE.INITING
		super.enterInitializationMode()
	}
	
	override exitInitializationMode() throws FmuInvocationException {
		currentMode = MODE.INITED
		super.exitInitializationMode()
	}
	
	override doStep(double currentCommunicationPoint, double communicationStepSize) throws FmuInvocationException {
		logger.debug(">{}.doStep({},{})", unit.identifier, currentCommunicationPoint, communicationStepSize)
		val micro_step = communicationStepSize/innerRate
		var inner_time = currentCommunicationPoint
		var Fmi2Status status;
		
		// Prepare port adaptations for stepping
		for (pl : adaptedInPorts.values){
			for (p : pl){
				p.startStep()
			}
		}
		for (p : adaptedOutPorts.values){
			p.startStep()
		}
		
		for (i : 0 ..< innerRate){
			// Set adapted inputs before doStep.
			for (pl : adaptedInPorts.values){
				for (p : pl){
					p.computeReal(inner_time, inner_time-currentCommunicationPoint, micro_step, communicationStepSize)					
				}
			}
			
			// doStep
			status = doInnerStep(inner_time, micro_step)
			
			// Update inner time
			inner_time = inner_time + micro_step
			
			// Set adapted outputs after doStep
			for (p : adaptedOutPorts.keySet){
				adaptedOutPorts.get(p).recordReal(inner_time, inner_time-currentCommunicationPoint, micro_step, communicationStepSize)
			}
		}
		
		// Inform input port adaptations of ending of step
		for (pl : adaptedInPorts.values){
			for (p : pl){
				p.endStep()
			}
		}
		for (p : adaptedOutPorts.values){
			p.endStep()
		}
		
		return status
	}
	
	def doInnerStep(double inner_time, double micro_step) {
		super.doStep(inner_time, micro_step)
	}
	
	override setReal(String portName, double v) {
		logger.debug(">{}.setReal({},{})", unit.identifier, portName, v)
		val adaptedPorts = adaptedInPorts.get(portName)
		if (adaptedPorts !== null){
			logger.debug("Going through adapted ports.")
			for (p : adaptedPorts){
				p.setReal(v)
				// In initialization mode, the values of the adaptation must be propagated to the internal fmus.
				if (currentMode == MODE.INITING){
					logger.debug("Propagating value.")
					p.computeReal(0.0, 0.0, 0.0, 0.0)	
				}
			}
		} else {
			logger.debug("No adaptations used.")
			setInnerReal(portName, v)
		}
	}
	
	override getReal(String portName){
		logger.debug(">{}.getReal({})", unit.identifier, portName)
		var double res
		val adaptedPort = adaptedOutPorts.get(portName)
		if (adaptedPort !== null){
			logger.debug("Going through adapted port.")
			// In initialization mode, the values of the adaptation must be obtained from the internal fmus.
			if (currentMode == MODE.INITING){
				adaptedPort.recordReal(0.0,0.0,0.0,0.0)
			}
			res = adaptedPort.getReal()
		} else {
			logger.debug("No adaptation used.")
			res = getInnerReal(portName)
		}
		logger.debug("<{}.getReal({}) = {}", unit.identifier, portName, res)
		return res
	}
	
	def setInnerReal(String portName, double v){
		super.setReal(portName, v)
	}
	
	def getInnerReal(String portName){
		super.getReal(portName)
	}

	override outputSnapshot(Map<OutputPortInstance, Double> outVals, Set<OutputPortInstance> filter) {
		for (outP : outputPorts){
			if (filter===null || filter.contains(outP)){
				outVals.put(outP, getReal(outP.identifier))
			}
		}
	}
	
	// Utility methods to deal with adaptedPort structures
	
	def private addInAdaptation(String id, IAdaptedInputPort port) {
		if (!adaptedInPorts.containsKey(id)){
			adaptedInPorts.put(id, newLinkedList())
		}
		adaptedInPorts.get(id).add(port)
	}
	
	override startCosimStep(double time, double stepSize) {
		// inform port adaptations
		for (ap : adaptedInPorts.values.flatten){
			ap.startCosimStep(time, stepSize)
		}
		for (ap : adaptedOutPorts.values){
			ap.startCosimStep(time, stepSize)
		}
	}
	
	override endCosimStep(double time, double stepSize) {
		// inform port adaptations
		for (ap : adaptedInPorts.values.flatten){
			ap.endCosimStep(time, stepSize)
		}
		for (ap : adaptedOutPorts.values){
			ap.endCosimStep(time, stepSize)
		}
	}
	
}