# 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 += ", "
        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
        """
        return "DEVS Exception: " + str(self.message)

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
