# Copyright 2015 Modelling, Simulation and Design Lab (MSDL) at 
# McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
The minimal PythonPDEVS simulation kernel. It only supports simple Parallel DEVS simulation, without any fancy configuration options.
While it behaves exactly the same as the normal simulation kernel with default options, it is a lot faster due to skipping all features.
"""

from collections import defaultdict

# Uncomment this part to make a completely stand-alone simulation kernel
class BaseDEVS(object):
    def __init__(self, name):
        self.name = name
        self.IPorts = []
        self.OPorts = []
        self.ports = []
        self.parent = None
        self.time_last = (0.0, 0)
        self.time_next = (0.0, 1)
        self.my_input = {}

    def addPort(self, name, is_input):
        name = name if name is not None else "port%s" % len(self.ports)
        port = Port(is_input=is_input, name=name)
        if is_input:
            self.IPorts.append(port)
        else:
            self.OPorts.append(port)
        port.port_id = len(self.ports)
        self.ports.append(port)
        port.host_DEVS = self
        return port

    def addInPort(self, name=None):
        return self.addPort(name, True)

    def addOutPort(self, name=None):
        return self.addPort(name, False)

    def getModelName(self):
        return self.name

    def getModelFullName(self):
        return self.full_name

class AtomicDEVS(BaseDEVS):
    ID = 0

    def __init__(self, name):
        BaseDEVS.__init__(self, name)
        self.elapsed = 0.0
        self.state = None
        self.model_id = AtomicDEVS.ID
        AtomicDEVS.ID += 1

    def extTransition(self, inputs):
        return self.state

    def intTransition(self):
        return self.state

    def confTransition(self, inputs):
        self.state = self.intTransition()
        return self.extTransition(inputs)

    def timeAdvance(self):
        return float('inf')

    def outputFnc(self):
        return {}

class CoupledDEVS(BaseDEVS):
    def __init__(self, name):
        BaseDEVS.__init__(self, name)
        self.component_set = []

    def addSubModel(self, model):
        model.parent = self
        self.component_set.append(model)
        return model

    def connectPorts(self, p1, p2):
        p1.outline.append(p2)
        p2.inline.append(p1)

class RootDEVS(object):
    def __init__(self, components):
        from schedulers.schedulerAuto import SchedulerAuto as Scheduler
        self.component_set = components
        self.time_next = float('inf')
        self.scheduler = Scheduler(self.component_set, 1e-6, len(self.component_set))

class Port(object):
    def __init__(self, is_input, name=None):
        self.inline = []
        self.outline = []
        self.host_DEVS = None
        self.name = name

    def getPortname(self):
        return self.name

def directConnect(component_set):
    """
    Perform a trimmed down version of the direct connection algorithm.

    It does not support transfer functions, but all the rest is the same.

    :param component_set: the iterable to direct connect
    :returns: the direct connected component_set
    """
    new_list = []
    for i in component_set:
        if isinstance(i, CoupledDEVS):
            component_set.extend(i.component_set)
        else:
            # Found an atomic model
            new_list.append(i)
    component_set = new_list

    # All and only all atomic models are now direct children of this model
    for i in component_set:
        # Remap the output ports
        for outport in i.OPorts:
            # The new contents of the line
            outport.routing_outline = set()
            worklist = list(outport.outline)
            for outline in worklist:
                # If it is a coupled model, we must expand this model
                if isinstance(outline.host_DEVS, CoupledDEVS):
                    worklist.extend(outline.outline)
                else:
                    outport.routing_outline.add(outline)
            outport.routing_outline = list(outport.routing_outline)
    return component_set

class Simulator(object):
    """
    Minimal simulation kernel, offering only setTerminationTime and simulate.
    
    Use this Simulator instead of the normal one to use the minimal kernel.
    While it has a lot less features, its performance is much higher.
    The polymorphic scheduler is also used by default.
    """
    def __init__(self, model):
        """
        Constructor

        :param model: the model to simulate
        """
        if isinstance(model, CoupledDEVS):
            component_set = directConnect(model.component_set)
            ids = 0
            for m in component_set:
                m.time_last = (-m.elapsed, 0)
                m.time_next = (-m.elapsed + m.timeAdvance(), 1)
                m.model_id = ids
                ids += 1
            self.model = RootDEVS(component_set)
        elif isinstance(model, AtomicDEVS):
            for p in model.OPorts:
                p.routing_outline = []
            model.time_last = (-model.elapsed, 0)
            model.time_next = (model.time_last[0] + model.timeAdvance(), 1)
            model.model_id = 0
            self.model = RootDEVS([model])
        self.termination_time = float('inf')

    def setTerminationTime(self, time):
        """
        Set the termination time of the simulation.

        :param time: simulation time at which simulation should terminate
        """
        self.termination_time = time

    def simulate(self):
        """
        Perform the simulation
        """
        scheduler = self.model.scheduler
        tn = scheduler.readFirst()
        tt = self.termination_time
        while tt > tn[0]:
            # Generate outputs
            transitioning = defaultdict(int)
            for c in scheduler.getImminent(tn):
                transitioning[c] |= 1
                outbag = c.outputFnc()
                for outport in outbag:
                    p = outbag[outport]
                    for inport in outport.routing_outline:
                        inport.host_DEVS.my_input.setdefault(inport, []).extend(p)
                        transitioning[inport.host_DEVS] |= 2

            # Perform transitions
            for aDEVS, ttype in transitioning.iteritems():
                if ttype == 1:
                    aDEVS.state = aDEVS.intTransition()
                elif ttype == 2:
                    aDEVS.elapsed = tn[0] - aDEVS.time_last[0]
                    aDEVS.state = aDEVS.extTransition(aDEVS.my_input)
                elif ttype == 3:
                    aDEVS.elapsed = 0.
                    aDEVS.state = aDEVS.confTransition(aDEVS.my_input)
                aDEVS.time_next = (tn[0] + aDEVS.timeAdvance(), 1 if tn[0] > aDEVS.time_last[0] else tn[1] + 1)
                aDEVS.time_last = tn
                aDEVS.my_input = {}

            # Do reschedules
            scheduler.massReschedule(transitioning)
            tn = scheduler.readFirst()

    def __getattr__(self, attr):
        """
        Wrapper to inform users that they are using the minimal kernel if they zant to do some unsupported configuration option.
        """
        if attr.startswith("set"):
            raise Exception("You are using the minimal simulation kernel, which does not support any configuration except for the termination time. Please switch to the normal simulation kernel to use this option.")
        else:
            raise AttributeError()
