#  Copyright 2020 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.

"""Domain-specific building block for industrial purposes.

"""
from pypdevsbbl.generic.queues import SimpleQueue
from pypdevsbbl.generic.transformers import Transformer
from pypdevs.DEVS import CoupledDEVS, AtomicDEVS
from pypdevs.infinity import INFINITY


class ConveyorBelt(CoupledDEVS):
    """A conveyor belt with items moving over it.

    Basically, a conveyor can be seen as a glorified queue that uses
    speed for timing purposes. Internally, there is a
    :class:`pypdevsbbl.generic.queues.SimpleQueue` and a
    :class:`pypdevsbbl.generic.transformers.Transformer` that have been
    called ``<name>-queue`` and ``<name>-transformer`` respectively,
    where ``<name>`` indicates the current block's name.

    .. image:: ../img/industry/conveyor-belt.svg
		:align: center

    Args:
        name (str):         The name of the block.
        v (numeric):        The conveyor speed.
        L (numeric):        The length of the conveyor.
        K (numeric):        The capacity of the conveyor. Defaults to
                            ``INFINITY``.

    Input Ports:
        enter (any):        The items that enter the conveyor belt.
        velocity (numeric): A new velocity at which the conveyor must run.

    Output Ports:
        leave (any):        The items that leave the conveyor belt.
        overflow (any):     The items that leave the conveyor because of
                            overflow.
    """
    def __init__(self, name, v, L, K=INFINITY):
        super(ConveyorBelt, self).__init__(name)

        self.L = L
        self.enter = self.addInPort("enter")
        self.velocity = self.addInPort("velocity")
        self.leave = self.addOutPort("leave")
        self.overflow = self.addOutPort("overflow")

        self._queue = self.addSubModel(SimpleQueue("%s-queue" % name, K=K, dr=self.L / v))
        self._transformer = self.addSubModel(Transformer("%s-transformer" % name, lambda i, _, __: self.L / i))

        self.connectPorts(self.enter, self._queue.enqueue)
        self.connectPorts(self.velocity, self._transformer.input)
        self.connectPorts(self._transformer.transformed, self._queue.dr)
        self.connectPorts(self._queue.dequeue, self.leave)
        self.connectPorts(self._queue.overflow, self.overflow)


from pypdevsbbl.extra.spatial import distance
import numpy as np
class Transporter(AtomicDEVS):
    """Transports an item linearly over a path.

    When not :attr:`chained`, the item is picked up at the start of
    the path before being transported to the end of the path, where
    it is dropped off. Next, we return over a straight line to the
    beginning and start over again.

    When :attr:`chained`, instead of returning to the beginning, we
    move to the next position where items can be picked up and start
    over again.

    .. image:: ../img/industry/transporter.svg
		:align: center

    Args:
        name (str):             The name of the block.
        path (iter):            An ordered set of :class:`pypdevsbbl.extra.spatial.Coordinate`
                                objects, indicative of each point in the path. It is
                                assumed there is a full stop between all points in the
                                path. There must be at least one point.
        vmax (numeric):         The maximal velocity of the transporter.
        a (numeric):            Strictly positive value that indicates the acceletarion
                                of the transporter. Must not be ``INFINITY``. Defaults
                                to 1.
        d (numeric):            Strictly positive value that indicates the deceleration
                                of the transporter. Must not be ``INFINITY``. Defaults
                                to 1.
        dist (def):             Distance function to use. As arguments, it takes two
                                :class:`pypdevsbbl.extra.spatial.Coordinate` objects and it
                                returns a numeric value, indicative of the distance.
                                See also :class:`pypdevsbbl.extra.spatial.distance`. Defaults
                                to :meth:`pypdevsbbl.extra.spatial.distance.euclidean`.
        chained (bool):         Indicator that the block is chained. Defaults to ``False``.
        start (Coordinate):     The starting point of the transporter. It can be used to
                                indicate a depository where the transporter starts. When
                                ``None``, the beginning of the path is assumed. If
                                :attr:`chained` is ``True``, this must be set to indicate
                                the first block and/or its offset. All other blocks in the
                                chain **must** have this value set to ``None``. Defaults
                                to ``None``.
        passthrough (bool):     Can be used to indicate that this transporter will only
                                follow a path and not pick up any items. In a chain, this
                                allows for more complex motion to the next pickup point.
                                Only useful within that context and is therefore only set
                                if :attr:`chained` is ``True``.
        eps (numeric):          The maximal distance from a point the transformer may be to
                                be assumed at that location. Defaults to ``1e-6``.

    State:
        item (any):             The item that's being carried.
        input (any):            The new item that arrives.
        elapsed (numeric):      The elapsed time since last event.
        duration (numeric):     The duration until a next event is scheduled.

    Input Ports:
        input (any):            A new item that wants to be transported.
        previous (Coordinate):  The end location in a chained transformer.

    Output Ports:
        restock (bool):         A simple message that indicates the block can accept a new
                                item for transportation.
        blocked (any):          The items that don't have permission to enter.
        output (any):           The items that are dropped off.
        next (Coordinate):      The end location in a chained transformer.
    """
    def __init__(self, name, path, vmax, a = 1, d = 1, dist = distance.euclidean,
                 chained=False, start = None, passthrough=False, eps=1e-6):
        assert len(path) > 0 and 0 < a < INFINITY and 0 < d < INFINITY
        super(Transporter, self).__init__(name)
        self.chained = chained
        self.path = path
        self.dist = dist
        self.eps = eps

        self.vmax = vmax
        self.a = a
        self.d = d

        self.passthrough = passthrough and chained

        dur = self.dist(start, path[0]) if start is not None else 0.0
        if self.chained and start is None:
            dur = INFINITY

        self.state = {
            "item": None,
            "input": None,
            "elapsed": 0.0,
            "duration": dur
        }

        self.input = self.addInPort("input")
        self.previous = self.addInPort("previous")

        self.restock = self.addOutPort("restock")
        self.blocked = self.addOutPort("blocked")
        self.output = self.addOutPort("output")
        self.next = self.addOutPort("next")

    def duration(self):
        """Actual computation of the duration of the full path."""
        time = 0.0
        a = self.a
        d = self.d
        I = (1.0 / a) + (1.0 / d)
        vmax = self.vmax
        for i in range(len(self.path[:-1])):
            dx = self.dist(self.path[i], self.path[i + 1])
            v = np.sqrt((2 * a * d * dx) / (a + d))
            v = np.min(v, vmax)
            xc = dx - np.power(v, 2) * (d + a) / (2 * a * d)
            time += (v * I) + (xc / v)
        return time

    def timeAdvance(self):
        if self.state["input"] is not None:
            return 0.0
        if self.state["item"] is not None or self.state["duration"] > self.eps:
            return max(0.0, self.state["duration"] - self.state["elapsed"])
        return INFINITY

    def outputFnc(self):
        res = {}
        if self.state["item"] is not None or self.passthrough:
            if self.state["input"] is not None:
                res[self.blocked] = self.state["input"]
            else:
                res[self.output] = self.state["item"]
                if self.chained:
                    res[self.next] = self.path[-1]
        else:
            if self.state["input"] is not None and self.state["duration"] > self.eps:
                res[self.blocked] = self.state["input"]
            else:
                res[self.restock] = True
        return res

    def intTransition(self):
        if self.state["item"] is not None or self.passthrough:
            if self.state["input"] is None: # OUTPUT
                self.state["item"] = None
                self.state["elapsed"] = 0.0
                if self.chained:
                    self.state["duration"] = INFINITY
                else:
                    self.state["duration"] = self.dist(self.path[0], self.path[-1])
        else:
            if self.state["input"] is not None:
                self.state["item"] = self.state["input"]
                self.state["elapsed"] = 0.0
                self.state["duration"] = self.duration()
        self.state["input"] = None
        return self.state

    def extTransition(self, inputs):
        self.state["elapsed"] += self.elapsed
        if self.input in inputs:
            self.state["input"] = inputs[self.input]
        if self.previous in inputs:
            last = inputs[self.previous]
            if self.state["duration"] == INFINITY:
                self.state["duration"] = self.dist(last, self.path[0])
        return self.state
