from .scheme.node import Node, WorkflowException, _callback_info
import multiprocessing
import importlib
from importlib.machinery import SourceFileLoader
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logger = logging.getLogger()


def get_handler(obj, data_name):
    """Return the name of the handler for this data_name"""
    def input_list_to_dict(inputs):
        res = {}
        # link each link name to the appropriate handler
        for input_ in inputs:
            res[input_[0]] = input_[2]
        return res
    input_dict = input_list_to_dict(obj.inputs)
    if data_name in input_dict:
        return input_dict[data_name]
    else:
        raise ValueError('link with name', data_name, 'is not managed by the object')


def _createCallback(name):
    sname = name.rsplit('.')
    if not (len(sname) > 1):
        raise ValueError('Invalid name')
    class_name = sname[-1]
    del sname[-1]
    module_name = '.'.join(sname)
    try:
        m = importlib.import_module(module_name)
    except ImportError:
        # try if given from file name
        del sname[-1]
        class_name = name.rsplit('.')[-1]
        m = SourceFileLoader(module_name, module_name).load_module()
    return getattr(m, class_name)()


def exec_process(callback_info, input_, properties):
    logger.debug('processing', str(callback_info), 'with input', input_, 'and',
                 properties, 'as properties')

    if type(input_) is tuple:
        data_name, data = input_
    else:
        data_name = None
        data = input_
    callback_info = _callback_info(callback=callback_info.callback,
                                   handler=callback_info.handler,
                                   need_instanciation=callback_info.need_instanciation)
    if callback_info.need_instanciation:
        import inspect
        if inspect.isclass(callback_info.callback):
            callback = callback_info.callback()
        else:
            callback = _createCallback(callback_info.callback)
    else:
        callback = callback_info.callback
    if hasattr(callback, 'setProperties'):
        callback.setProperties(properties)

    if data_name:
        # if the link specify a name, then we should be able to have several
        # handler
        handler = get_handler(callback, data_name)
    else:
        if not callable(callback):
            handler = 'process'
        else:
            handler = None

    # pick the default process function is no handler and object not callable
    if handler is not None:
        if not hasattr(callback, handler):
            raise ValueError('handler function', handler, 'does not exist for', callback)
        out = getattr(callback, handler)(data)
    else:
        out = callback(data)
    if hasattr(out, 'to_dict'):
        return out.to_dict()
    else:
        return out


class AsyncFactory:
    def __init__(self, actor, callback_info=None):
        self.actor = actor
        self.pool = multiprocessing.Pool()
        self.callback_info = callback_info

    def call(self, *args, **kwargs):
        logger.debug('args={0}, kwargs={1}'.format(args, kwargs))
        if self.callback_info is None:
            self.pool.apply_async(exec_process, args, kwargs)
        else:
            self.pool.apply_async(exec_process, args, kwargs, self.callback_info)


class ActorWrapper(object):
    def __init__(self, actor):
        assert isinstance(actor, Node)
        self.actor = actor

    def run(self, inData):

        outData = self.actor.run(inData)
        if isinstance(outData, WorkflowException):
            return outData
        else:
            inData.update(outData)
            return inData


class ActorFactory(object):
    def __init__(self, name, errorHandler=None):
        self.name = name
        self.errorHandler = errorHandler
        self._a = None
        self.listDownStreamActor = []
        self.outData = None
        self.actorWrapper = ActorWrapper(name)
        self.af = None
        self.lock = multiprocessing.Lock()

    def connect(self, actor):
        self.listDownStreamActor.append(actor)

    def _trigger_process(self, inData):
        """Trigger one process"""
        logger.debug('In trigger {0}, inData = {1}'.format(self.name, inData))
        if isinstance(inData, WorkflowException):
            logger.error(
                    'Error from previous actor! Not running actor {0}'.format(
                            self.name))
            if self.errorHandler is not None:
                workflowException = inData
                oldInData = workflowException.data
                exceptionDict = {
                    'errorMessage': workflowException.errorMessage,
                    'traceBack': workflowException.traceBack,
                }
                oldInData['WorkflowException'] = exceptionDict
                self.errorHandler.triggerOnError(oldInData)
        elif len(self.listDownStreamActor) == 0:
            # Run without any callback
            self.af = AsyncFactory(self.actorWrapper.actor.callback)
            self.af.call(self.actorWrapper.actor.callback,
                         inData,
                         self.actorWrapper.actor.properties)
        else:
            # Run with callback
            self.af = AsyncFactory(self.actorWrapper.actor.callback,
                                   self.triggerDownStreamActor)
            self.af.call(self.actorWrapper.actor.callback,
                         inData,
                         self.actorWrapper.actor.properties)

    def trigger(self, data):
        if hasattr(data, 'to_dict'):
            _data = data.to_dict()
        else:
            _data = data
        if _data and 'sig_type' in _data:
            self._trigger_signal(data=_data)
        else:
            properties = self.actorWrapper.actor.properties
            if _data is None and '_scanIDs' in properties:
                inDataList = properties['_scanIDs']
            else:
                inDataList = [_data, ]

            for inData in inDataList:
                self._trigger_process(inData=inData)

            if self.actorWrapper.actor.need_stop_join:
                self._trigger_stop(len(inDataList))

    def _trigger_stop(self, nProcess):
        """Signal that this process is finished and the downstream nodes
        Can stop once they processed nProcess"""
        self._trigger_signal(data={'sig_type': 'stop', 'n_process': nProcess})

    def _trigger_signal(self, data):
        if len(self.listDownStreamActor) == 0:
            return
        else:
            self.triggerDownStreamActor(inData=data)

    def triggerDownStreamActor(self, inData=None):
        if inData is None:
            inData = {}
        for downStreamActor in self.listDownStreamActor:
            logger.debug(
                'In trigger {0}, triggering actor {1}, inData={2}'.format(
                    self.name, downStreamActor.name, inData))
            downStreamActor.trigger(inData)


class StartActor(object):
    def __init__(self, name='Start actor'):
        self.name = name
        self.listDownStreamActor = []

    def connect(self, actor):
        self.listDownStreamActor.append(actor)

    def trigger(self, inData):
        for actor in self.listDownStreamActor:
            actor.trigger(inData)


class StopActor(object):
    def __init__(self, name='Stop actor'):
        self.name = name
        self.lock = multiprocessing.Lock()
        self.lock.acquire()
        self.outData = None

    def trigger(self, inData):
        logger.debug('In trigger {0}, inData = {1}'.format(self.name, inData))
        self.outData = inData
        self.lock.release()

    def join(self, timeout=7200):
        self.lock.acquire(timeout=timeout)


class Join(object):
    def __init__(self, name, numberOfConnections):
        self.name = name
        self.numberOfConnections = numberOfConnections
        self.listInData = []
        self.listDownStreamActor = []

    def connect(self, actor):
        self.listDownStreamActor.append(actor)

    def trigger(self, inData):
        self.listInData.append(inData)
        if len(self.listInData) == self.numberOfConnections:
            newInData = {}
            for data in self.listInData:
                newInData.update(data)
            for actor in self.listDownStreamActor:
                actor.trigger(newInData)


class JoinUntilStopSignal(object):
    def __init__(self, name):
        self.name = name
        self.listInData = []
        self.listDownStreamActor = []
        self._nprocess_received = 0
        self._nprocess_waited = 0
        self._can_stop = False

    def connect(self, actor):
        self.listDownStreamActor.append(actor)

    def trigger(self, inData):
        print('***JoinUntilStopSignal***', inData)

        if type(inData) is dict and 'sig_type' in inData and inData['sig_type'] == 'stop':
            self._can_stop = True
            self._nprocess_waited = inData['n_process']
        else:
            self._nprocess_received += 1

        self.listInData.append(inData)
        print('_can_stop', self._can_stop, ', ', self._nprocess_waited <= self._nprocess_received)

        if self._can_stop and self._nprocess_waited <= self._nprocess_received:
            newInData = {}
            for data in self.listInData:
                newInData.update(data)
            for actor in self.listDownStreamActor:
                actor.trigger(newInData)


class RouterActor(object):
    def __init__(self, parent=None, name='Router', itemName=None, listPort=[]):
        self.parent = parent
        self.name = name
        self.itemName = itemName
        self.listPort = listPort
        self.dictValues = {}

    def connect(self, actor, expectedValue='other'):
        if expectedValue != 'other' and not expectedValue in self.listPort:
            raise RuntimeError(
                'Port {0} not defined for router actor {1}!'.format(
                    expectedValue, self.name))
        self.dictValues[expectedValue] = actor

    def trigger(self, inData):
        value = inData[self.itemName]
        if value in self.dictValues:
            self.dictValues[value].trigger(inData=inData)
        else:
            self.dictValues['other'].trigger(inData=inData)


class Port(object):
    def __init__(self, parent, name):
        self.name = parent.name + '.' + name
        self.parent = parent
        self.listActor = []
        self.inPortTrigger = None

    def connect(self, actor):
        logger.debug(
            "Connect {0} -> actorName {1}".format(self.name, actor.name))
        self.listActor.append(actor)

    def setTrigger(self, trigger):
        self.inPortTrigger = trigger

    def trigger(self, inData):
        logger.debug("In {0} trigger".format(self.name))
        if len(self.listActor) > 0:
            for actor in self.listActor:
                logger.debug(
                    "In trigger {0} -> actorName {1}".format(self.parent.name,
                                                             actor.name))
                actor.trigger(inData)
        if self.inPortTrigger is not None:
            logger.debug(
                "In {0} trigger, trigger = {1}".format(self.parent.name,
                                                       self.inPortTrigger))
            self.inPortTrigger(inData)


class SubModel(object):
    def __init__(self, errorHandler=None, name=None, portNames=['In', 'Out']):
        self.name = name
        self.errorHandler = errorHandler
        self.dictPort = {}
        self.listOnErrorActor = []
        for portName in portNames:
            self.dictPort[portName] = Port(self, portName)

    def getPort(self, portName):
        logger.debug(
            "In {0} getPort, portName = {1}".format(self.name, portName))
        return self.dictPort[portName]

    def connect(self, actor, portName='Out'):
        logger.debug("In {0} connect, portName = {2} -> actorName = {1}".format(
            self.name, actor.name, portName))
        self.dictPort[portName].connect(actor)

    def connectOnError(self, actor):
        logger.debug(
            "In connectOnError in subModule {0}, actor name {1}".format(
                self.name, actor.name))
        self.listOnErrorActor.append(actor)

    def triggerOnError(self, inData):
        for onErrorActor in self.listOnErrorActor:
            logger.debug(
                "In triggerOnError in subModule {0}, trigger actor {1}, inData = {2}".format(
                    self.name, onErrorActor.name, args[0]))
            onErrorActor.trigger(inData)
        if self.errorHandler is not None:
            self.errorHandler.triggerOnError(inData)


class ErrorHandler(object):
    def __init__(self, name='Start actor'):
        self.name = name
        self.listDownStreamActor = []

    def connect(self, actor):
        self.listDownStreamActor.append(actor)

    def triggerOnError(self, inData):
        for actor in self.listDownStreamActor:
            actor.trigger(inData)
