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

"""Create and use tracers for your models.

This file provides a basic tracer (:class:`BaseTracer`) that can
be used as a superclass for all custom tracers. Furthermore, a
:meth:`setTracer` function is provided to allow for more complex
tracing possibilities.

The subpackage also provides a few commonly used tracers in
production-based models.
"""

from pypdevs.util import runTraceAtController, toStr
import sys

# Efficiency can be increased if this dict is cleared, but data can be lost
# in cases where multiple tracers use the same attributes.
_hashes = {}
"""Helper variable that will allow the :meth:`setTracer` method to work.

It is a dict of the :meth:`id`-value of a tracer and its arguments.
"""

def _hashedTracer(uid, server, tracer, *hargs):
    """Helper function to set the tracer."""
    global _hashes
    TRC = _hashes[tracer]
    args = [_hashes[x] for x in hargs]
    return TRC(uid, server, *args)


def setTracer(simulator, tracerclass, *args):
    """Assigns a custom tracer with variable arguments.

    In `PythonPDEVS`' custom tracer registration process, all
    additional arguments are casted to a string before passed
    to an :meth:`eval` function call. Not all objects can be
    cast to a string and still retain their information.

    This function allows you to set a tracer with custom
    arguments that cannot be cast to a string.

    See Also:
        :file:`src/dependencies/pythonpdevs/src/pypdevs/tracer.py:40-42`

    Args:
        simulator:      The simulator to set the tracer on.
        tracerclass:    The tracer class to set as an
                        instatiatable.
        *args:          All arguments to pass to the tracer's
                        :meth:`__init__`-call, **except** the
                        ``uid`` and the ``server``.
    """
    global _hashes

    a = []
    for r in [tracerclass] + list(args):
        _hashes[id(r)] = r
        a.append(id(r))
    simulator.setCustomTracer("pypdevsbbl.tracers", "_hashedTracer", a)


def superTrace(server, uid, model, args):
    runTraceAtController(server, uid, model, [toStr(x) for x in args])


class BaseTracer:
    """Base for all custom tracers.

    Implements all required methods for a custom tracer.

    Args:
        uid:        A unique identifier for distributed simulation.
                    Can be ignored, but must be the first argument!
        server:     The server that runs the distributed simulation.
                    Can be ignored, but must be the second argument!
        filename:   The name of the file to write the results to.
                    When ``None``, all results will be written to the
                    standard output.
    """
    def __init__(self, uid, server, filename):
        # Contents of the function here are taken from the already existing tracers
        if server.getName() == 0:
            self.filename = filename
        else:
            self.filename = None
        self.server = server
        self.uid = uid

    def startTracer(self, recover):
        """Starts the Tracer

        Args:
            recover (bool): Represents whether or not this is a recovered call.
                            E.g., should the file be overwritten (``False``) or
                            appended to (``True``)?
        """
        if self.filename is None:
            self.file = sys.stdout
        elif recover:
            self.file = open(self.filename, 'a+')
        else:
            self.file = open(self.filename, 'w')


    def stopTracer(self):
        """Stops the tracer (e.g., flush the file)"""
        if self.file is not None:
            self.file.flush()

    def trace(self, *text, sep=' ', end=''):
        """Actual tracing function.

        This function sort of works like the "print" function,
        but only takes strings or bytes as arguments.

        Args:
            text:       The text to trace.
            sep (str):  The text separator. Defaults to a space.
            end (str):  The ending token of the stream to write.
                        Defaults to the empty string.
        """
        def write(obj):
            if isinstance(obj, str):
                self.file.write(obj)
            elif isinstance(obj, bytes):
                self.file.write(obj.decode())
            else:
                self.file.write(str(obj))

        for string in text[:-1]:
            write(string)
            self.file.write(sep)
        write(text[-1])
        write(end)

    def traceInternal(self, aDEVS):
        """Called for each atomic DEVS model that does an internal transition."""
        pass

    def traceExternal(self, aDEVS):
        """Called for each atomic DEVS model that does an external transition."""
        pass

    def traceConfluent(self, aDEVS):
        """Called for each atomic DEVS model that does a confluent transition."""
        pass

    def traceInit(self, aDEVS, t):
        """Called upon initialization of a model.

        Args:
            t (numeric):    The time at which the model commences (likely 0).
        """
        pass

    def traceUser(self, time, aDEVS, variable, value):
        """Called upon so-called *god events* during debugging.

        A *god event* is an event where a user manually alters the state of an
        atomic DEVS instance.
        """
        pass


# Make these available from the `tracers` subpackge root.
from pypdevsbbl.tracers.FootprintTracer import FootprintTracer
from pypdevsbbl.tracers.PlotTracer import PlotTracer
from pypdevsbbl.tracers.StatisticsTracer import StatisticsTracer
from pypdevsbbl.tracers.ProfileTracer import ProfileTracer
