# Source code for pypdevs.util

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

    """
    Common utility functions used in PyPDEVS
    """
    import pypdevs.middleware as middleware
    from pypdevs.MPIRedirect import MPIRedirect
    from collections import defaultdict

    EPSILON = 1E-6

    try:
        import cPickle as pickle
    except ImportError:
        import pickle

    def broadcastModel(data, proxies, allow_reinit, scheduler_locations):
        """
        Broadcast the model to simulate to the provided proxies

        :param data: data to be broadcasted to everywhere
        :param proxies: iterable containing all proxies
        :param allowReinit: should reinitialisation be allowed
        """
        if (len(proxies) == 1) and not allow_reinit:
            # Shortcut for local simulation with the garantee that no reinits will happen
            proxies[0].sendModel(data, scheduler_locations[0])
            return
        # Otherwise, we always have to pickle
        pickled_data = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
        if len(proxies) == 1:
            proxies[0].saveAndProcessModel(pickled_data, scheduler_locations[0])
        else:
            for i, proxy in enumerate(proxies[1:]):
                # Prepare by setting up the broadcast receiving
                proxy.prepare(scheduler_locations[i+1])
            # Pickle the data ourselves, to avoid an MPI error when this goes wrong (as we can likely back-up from this error)
            # Broadcast the model to everywhere
            middleware.COMM_WORLD.bcast(pickled_data, root=0)
            # Immediately wait for a barrier, this will be OK as soon as all models have initted their model
            # Still send to ourselves, as we don't receive it from the broadcast
            # Local calls, so no real overhead
            proxies[0].sendModel(data, scheduler_locations[0])
            proxies[0].setPickledData(pickled_data)
            middleware.COMM_WORLD.barrier()

    def broadcastCancel():
        """
        Cancel the broadcast receiving in a nice way, to prevent MPI errors
        """
        middleware.COMM_WORLD.bcast(None, root=0)

    def toStr(inp):
        """
        Return a string representation of the input, enclosed with ' characters

        :param inp: the input value
        :returns: string -- input value, enclosed by ' characters
        """
        return "'%s'" % inp

    def addDict(destination, source):
        """
        Adds 2 dicts together in the first dictionary

        :param destination: the destination dictionary to merge the source into
        :param source: the dictionary to merge in

        .. note:: the *destination* parameter will be modified and no return value is provided. The *source* parameter is not modified.
        """
        for i in source:
            destination[i] = destination.get(i, 0) + source[i]

    def allZeroDict(source):
        """
        Checks whether or not a dictionary contains only 0 items

        :param source: a dictionary to test
        :returns: bool -- whether or not all entries in the dictionary are equal to zero
        """
        for i in source.values():
            if i != 0:
                return False
        return True

    def runTraceAtController(server, uid, model, args):
        """
        Run a trace command on our version that is running at the constroller

        :param server: the server to ask the proxy from
        :param uid: the UID of the tracer (identical throughout the simulation)
        :param model: the model that transitions
        :param args: the arguments for the trace function
        """
        to_run = easyCommand("self.tracers.getByID(%i).trace" % uid,
                             args).replace("\n", "\\n")
        if server.getName() == 0:
            server.getProxy(0).delayedAction(model.time_last, model.model_id, to_run)
        else:
            server.queueMessage(model.time_last, model.model_id, to_run)

    def easyCommand(function, args):
        """
        Easy wrapper to create a string representation of function calls

        :param function: the function should be called
        :param args: list of all the arguments for the function
        :returns: str -- string representation to be passed to *exec*
        """
        text = str(function) + "("
        for i in range(len(args)):
            if i != 0:
                text += ", "
            if isinstance(args[i], str):
                args[i] = '"%s"' % args[i][1:-1].replace('"', "\\\"").replace("'", "\\'")
            text += str(args[i])
        text += ")"
        return text

    class DEVSException(Exception):
        """
        DEVS specific exceptions
        """
        def __init__(self, message="not specified in source"):
            """
            Constructor

            :param message: error message to print
            """
            Exception.__init__(self, message)

        def __str__(self):
            """
            String representation of the exception
            """
            if hasattr(self, "message"):
                return "DEVS Exception: " + str(self.message)
            return "DEVS Exception: " + str(self.args[0])

    class QuickStopException(Exception):
        """
        An exception specifically to stop the simulation and perform a relocation ASAP
        """
        def __init__(self):
            Exception.__init__(self, "(none)")

        def __str__(self):
            """
            Should be unused
            """
            return "Quick Stop Exception"

    def saveLocations(filename, model_locations, model_ids):
        """
        Save an allocation specified by the parameter.

        :param filename: filename to save the allocation to
        :param modellocations: allocation to save to file
        :param model_ids: all model_ids to model mappings
        """
        # Save the locations
        f = open(filename, 'w')
        for model_id in model_locations:
            # Format:
            #   model_id location fullname
            f.write("%s %s %s\n" % (model_id,
                                    model_locations[model_id],
                                    model_ids[model_id].getModelFullName()))
        f.close()

    def constructGraph(models):
        """
        Construct a graph from the model, containing the weight (= number of messages) on a connection
        between two components.

        :param models: the root model to use for graph construction
        :returns: dict -- all from-to edges with their number of events
        """
        edges = defaultdict(lambda: defaultdict(int))
        for model in models.component_set:
            for outport in model.OPorts:
                for inport in outport.outline:
                    edges[outport.host_DEVS][inport.host_DEVS] += outport.msg_count
        return edges