# Source code for pypdevsbbl.generic.io

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

    """Set of PythonPDEVS I/O blocks for user interaction.

    The classes in this file will allow for logging values
    and messages to files for analysis and tracing.

    Execute this file to test the :class:`Sound` block.
    You should hear a melody being played on the piano.
    If this is not the case, something is wrong with the
    audio on your system. See the block itself for more
    info.

    Note:
        In order to obtain the ``syslog`` messages, a
        ``syslog`` server needs to be setup. `You can write
        your own server <https://gist.github.com/marcelom/4218010>`_
        or you can install one on your system.
        `PySysLogQt <https://github.com/RandyParedis/PySysLogQt>`_
        is a tiny, graphical server that was made by the author
        for this specific purpose. It accepts both
        `RFC 5424`-formatted messages as well as the default Python
        format.

    Warning:
        In order for the :class:`Logger` block and all its
        subclasses to work, the :meth:`setLogger` function
        needs to be called.
    """

    from pypdevs.DEVS import AtomicDEVS
    from pypdevs import logger
    from pypdevs.infinity import INFINITY

    def setLogger(loc, address=('localhost', 1024), loglevel=0, rfc5424=True):
        """Sets the logger to a syslog logger that might follow RFC 5424.

        Note:
            The :meth:`pypdevs.logger.setLogger` creates a logger that's based
            on the standard implementation of the ``SysLogHandler``. Alas, this
            handler does not follow the full `syslog` specification as described
            in `RFC 5424 <https://tools.ietf.org/html/rfc5424>`_ and therefore
            `Python(P)DEVS` doesn't either. This function replaces
            :meth:`pypdevs.logger.setLogger` and provides the correct `RFC`.

        Args:
            loc (any):          Location of the server, to prepend to every
                                logged message.
            address (tuple):    The address of the syslog server in the form of
                                (ip-address, port). Defaults to ``('localhost', 1024)``.
            loglevel (numeric): Minimal log level to support. All messages with a
                                lower level will be discarded. Should be one from
                                in the above list. Defaults to 0.
            rfc5424 (bool):     Whether or not to follow ``RFC 5424`` as a logging
                                format. Defaults to ``True``. Note that this requires
                                `rfc5424-logging-handler` to be installed.
        """
        # Create the default logger
        import logging
        if rfc5424:
            if loglevel is None or logger.logger is not None:
                return
            from rfc5424logging import Rfc5424SysLogHandler
            handler = Rfc5424SysLogHandler(address, facility=19)
            local_logger = logging.getLogger('PyPDEVS-logging')
            local_logger.addHandler(handler)
            local_logger.setLevel(loglevel)
            logger.location = loc
            logger.logger = local_logger
        else:
            logger.setLogger(loc, address, loglevel)
            thelogger = logger.logger
            logging.addLevelName(25, "NOTICE")
            logging.addLevelName(60, "ALERT")
            logging.addLevelName(70, "EMERG")

            # Set all missing functions
            def log(lvl, msg, *args, **kwargs):
                if thelogger.isEnabledFor(lvl):
                    thelogger._log(lvl, msg, args, **kwargs)

            thelogger.notice = lambda msg, *args, **kwargs: log(25, msg, *args, **kwargs)
            thelogger.alert = lambda msg, *args, **kwargs: log(60, msg, *args, **kwargs)
            thelogger.emerg = lambda msg, *args, **kwargs: log(70, msg, *args, **kwargs)

    class Logger(AtomicDEVS):
        """Generic ``syslog``-logger.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/logger.svg
    		:align: center

        Args:
            name (str):     The name of the block.
            level (str):    Logging level, in string format.
                            This is a function that is called on the
                            logger object.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str, level: str="info"):
            super(Logger, self).__init__(name)
            self.level = level

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

        def extTransition(self, inputs):
            if self.input in inputs:
                logger.log(self.level, str(inputs[self.input]), logger.logger)

    class Emergency(Logger):
        """Logs emergency messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/emergency.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Emergency, self).__init__(name, "emerg")

    class Alert(Logger):
        """Logs alert messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/alert.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Alert, self).__init__(name, "alert")

    class Critical(Logger):
        """Logs critical messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/critical.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Critical, self).__init__(name, "critical")

    class Error(Logger):
        """Logs error messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/error.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Error, self).__init__(name, "error")

    class Warn(Logger):
        """Logs warning messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/warn.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Warn, self).__init__(name, "warn")

    class Notice(Logger):
        """Logs notice messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/notice.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Notice, self).__init__(name, "notice")

    class Info(Logger):
        """Logs informational messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/info.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Info, self).__init__(name, "info")

    class Debug(Logger):
        """Logs debug messages.

        Before this class can be used, :meth:`setLogger` needs to be called.

        .. image:: ../paper/images/blocks/io/debug.svg
    		:align: center

        Args:
            name (str): The name of the block.

        Input Ports:
            input (str):    The message to be logged.
        """
        def __init__(self, name: str):
            super(Debug, self).__init__(name, "debug")

    class FileWriter(AtomicDEVS):
        """Writes data to a file.

        .. image:: ../paper/images/blocks/io/file-writer.svg
    		:align: center

        Args:
            blockname (str):    The name of the block.
            filename (str):     The name of the file to write to.
            binary (bool):      Whether or not to open the file in binary mode.
                                Defaults to ``False``.
            append (bool):      Whether or not to append instead of overwriting.
                                Note that this requires the file to already exist.
                                Defaults to ``True``.

        Input Ports:
            input (str/bytes):  When in binary mode, it expects a ``bytes``-object,
                                otherwise a ``str``. This is the data that needs to be
                                written to the file.
        """
        def __init__(self, blockname: str, filename: str, binary: bool=False, append: bool=True):
            super(FileWriter, self).__init__(blockname)
            self.filename = filename
            self.binary = binary
            self.append = append

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

        def extTransition(self, inputs):
            if self.input in inputs:
                mode = 'w'
                if self.append:
                    mode = 'a'
                if self.binary:
                    mode += 'b'
                with open(self.filename, mode) as file:
                    file.write(inputs[self.input])
            return self.state

    class FileReader(AtomicDEVS):
        """Reads contents of a file.

        Every time a filename arrives at the block, its contents
        will be outputted in the same time instance.

        .. image:: ../paper/images/blocks/io/file-reader.svg
    		:align: center

        Args:
            name (str):             The name of the block.
            binary (bool):          Whether or not to read the file in binary mode.
                                    Defaults to ``False``.

        State:
            contents (str):         The contents of the just-read file or ``None``
                                    if no file has been read.

        Input Ports:
            filename (str):         The name of the file to read.

        Output Ports:
            contents (str/bytes):   The contents of the file.
        """
        def __init__(self, name: str, binary: bool=False):
            super(FileReader, self).__init__(name)
            self.state = {
                "contents": None
            }
            self.binary = binary

            self.filename = self.addInPort("filename")
            self.contents = self.addOutPort("contents")

        def timeAdvance(self):
            if self.state["contents"] is not None:
                return 0
            return INFINITY

        def intTransition(self):
            self.state["contents"] = None
            return self.state

        def extTransition(self, inputs):
            if self.filename in inputs:
                mode = 'r'
                if self.binary:
                    mode += 'b'
                with open(inputs[self.filename], mode) as file:
                    self.state["contents"] = file.read()
            return self.state

        def outputFnc(self):
            if self.state["contents"] is not None:
                return { self.contents: self.state["contents"] }
            return {}

    class Listener(AtomicDEVS):
        """Listens to an extrenal source.

        The provided function is executed every ``dt`` time and its
        return value is outputted on the `polled` port. Alternatively,
        every time a message arrives on the `dt` port, the ``dt``
        value will change.

        Note:
            The block will poll at the beginning of the simulation.

        .. image:: ../paper/images/blocks/io/listener.svg
    		:align: center

        Args:
            dt (numeric):       The delay between messages. Cannot be negative
                                or zero. Defaults to ``INFINITY``.
            fp (def):           The polling function to be executed. Takes at least the
                                current time as first input. When returning ``None``,
                                the :class:`Listener` will not fire an output event.
                                Defaults to a `do nothing` function: ``lambda x: None``.
            *args:              Additional arguments used to call the function.

        State:
            dt (numeric):       The time between polls. Cannot be negative or zero.
            elapsed (numeric):  The currently elapsed time.
            value (any):        The return value.

        Input Ports:
            dt (any):           A new ``dt`` value.

        Output Ports:
            listen (any):       The polled result.
        """

        def __init__(self, name: str, dt = INFINITY, fp = lambda t: None, *args):
            assert dt > 0
            super(Listener, self).__init__(name)
            self.state = {
                "dt": dt,
                "elapsed": dt,
                "time": 0.0
            }
            self.fp = fp
            self.args = args

            self.dt = self.addInPort("dt")
            self.listen = self.addOutPort("listen")

        def timeAdvance(self):
            return max(0.0, self.state["dt"] - self.state["elapsed"])

        def outputFnc(self):
            return { self.listen: self.fp(self.state["time"], *self.args) }

        def intTransition(self):
            self.state["elapsed"] = 0.0
            self.state["time"] += self.timeAdvance()
            return self.state

        def extTransition(self, inputs):
            self.state["elapsed"] += self.elapsed
            if self.dt in inputs:
                dt = inputs[self.dt]
                if 0 < dt:
                    self.state["dt"] = dt
                    self.state["time"] += dt - self.elapsed - 1
            return self.state

    # In a try/catch block to allow importing this module even if no
    # sounds can/should be played.
    try:
        import simpleaudio
        from pydub import AudioSegment
        class Sound(AtomicDEVS):
            """Plays a sound whenever the input is evaluated to be ``True``.

            Can be used for an auditory response on certain events.

            See Also:
                - https://github.com/jiaaro/pydub
                - https://pypi.org/project/simpleaudio/
                - https://github.com/jiaaro/pydub/issues/160

            Note:
                This block is ignored when ``pydub`` or ``simpleaudio`` cannot be imported.

                ``pydub`` will be used to read from as many sound formats as possible (as
                long as it's `supported by ffmpeg <http://www.ffmpeg.org/general.html#File-Formats>`_)
                and ``simpleaudio`` will allow for the sounds to be played asynchronously.

            .. image:: ../paper/images/blocks/io/sound.svg
    		    :align: center

            Args:
                name (str):         The name of the block.
                soundfile (str):    The filename of the sound to play.
                condition (def):    A function that takes the input as argument
                                    and outputs ``True`` if a sound must be played.
                                    Defaults to `always`.
                start (int):        When to start the sound, in milliseconds.
                                    Defaults to 0 (= beginning of the sound).
                end (int):          When to end the sound, in milliseconds.
                                    Defaults to ``None`` (= end of the sound).

            Input Ports:
                input (any):        The item that is checked against the ``condition``
                                    before the sound is played.
            """
            def __init__(self, name: str, soundfile: str, condition=lambda x: True, start: int=0, end: int=None):
                super(Sound, self).__init__(name)
                self.sound = soundfile
                self.cond = condition

                self.start = start
                self.end = end

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

            def play(self):
                """Plays the sound using simpleaudio."""
                ext = self.sound.split(".")[-1]
                sound = AudioSegment.from_file(self.sound, ext)[self.start:self.end]
                playback = simpleaudio.play_buffer(
                    sound.raw_data,
                    num_channels=sound.channels,
                    bytes_per_sample=sound.sample_width,
                    sample_rate=sound.frame_rate
                )

            def extTransition(self, inputs):
                if self.input in inputs and self.cond(inputs[self.input]):
                    self.play()

    except ImportError as e:
        import warnings
        warnings.warn("Could not import Sound block, because '%s' is not installed." % e.name)

    if __name__ == "__main__":
        if 'Sound' in dir():
            from pypdevs.simulator import Simulator
            from pypdevs.DEVS import CoupledDEVS
            from pypdevsbbl.generic.generators import TableGenerator
            from pypdevsbbl.extra import fileutils

            sfile = fileutils.test("sound.mp3")

            class Model(CoupledDEVS):
                def __init__(self):
                    super(Model, self).__init__("Model")

                    self.gen = self.addSubModel(TableGenerator("Generator", [(0, "play")]))
                    self.sound = self.addSubModel(Sound("Sound 1", sfile))

                    self.connectPorts(self.gen.output, self.sound.input)

            model = Model()
            sim = Simulator(model)
            sim.setClassicDEVS()
            sim.simulate()
        else:
            print("The Sound block was not found.", file=sys.stderr)