# encoding=utf8
# wfpy internal implementation
# snowdreamist@live.cn

from contextlib import contextmanager
from threading import Thread, Lock, Semaphore

@contextmanager
def lock(lock):
    try:
        lock.acquire(True)
        yield
    finally:
        lock.release()

class InternalLogger(object):
    """
    Internal logger
    """
    def __init__(self, name, logger):
        self.__name = name
        self.__logger = logger

    @property
    def name(self):
        return self.__name

    def error(self, message, error = None, paths = None):
        """
        Log error
        Parameters:
            message         error message
            error           exception object
        """
        if paths is None:
            paths = [ self.name ]
        else:
            paths.append(self.name)
        self.__logger.error(message, error, paths)

    def info(self, message, error = None, paths = None):
        """
        Log info
        """
        if paths is None:
            paths = [ self.name ]
        else:
            paths.append(self.name)
        self.__logger.info(message, error, paths)

    def warn(self, message, error = None, paths = None):
        """
        Log warn
        """
        if paths is None:
            paths = [ self.name ]
        else:
            paths.append(self.name)
        self.__logger.warn(message, error, paths)

    def debug(self, message, error = None, paths = None):
        """
        Log debug
        """
        if paths is None:
            paths = [ self.name ]
        else:
            paths.append(self.name)
        self.__logger.debug(message, error, paths)

class InternalRootLogger(InternalLogger):
    """
    Root internal logger
    """
    def __init__(self, sysLogger, logCallback = None):
        """
        Parameters:
            sysLogger       python built-in logger
            logCallback     callback on log event
                callback function signature: callbackHandler(level, path, message, error)
        """
        self.__sysLogger = sysLogger
        self.__logCallback = logCallback
        super(InternalRootLogger, self).__init__('root', None)

    def __formatexception__(self, error):
        """
        Format exception
        """
        pass

    def __log__(self, level, paths, message, error):
        """
        Do log
        """
        if paths is None:
            path = ''
        else:
            path = '.'.join(reversed(paths))
        # Callback
        cbError = None
        if not self.__logCallback is None:
            try:
                self.__logCallback(level, path, message, error)
            except Exception as callbackError:
                cbError = callbackError
        # Log
        if isinstance(path, unicode):
            path = path.encode('utf8')
        if isinstance(message, unicode):
            message = message.encode('utf8')
        # Get method
        if level == 'error':
            logfunc = self.__sysLogger.error
        elif level == 'warn':
            logfunc = self.__sysLogger.warn
        elif level == 'debug':
            logfunc = self.__sysLogger.debug
        else:
            logfunc = self.__sysLogger.info

        logfunc('[%s]:[%s]', path, message)
        if not error is None:
            logfunc('[%s]:[Related exception]:[%s]', path, self.__formatexception__(error))

        if not cbError is None:
            self.__sysLogger.warn('[Logger][%s]:[Callback failed]', path)
            self.__sysLogger.warn('[Logger][%s]:[Related exception]:[%s]', path, self.__formatexception__(cbError))

    def error(self, message, error = None, paths = None):
        """
        Log error
        """
        self.__log__('error', paths, message, error)

    def info(self, message, error, paths = None):
        """
        Log info
        """
        self.__log__('info', paths, message, error)

    def warn(self, message, error = None, paths = None):
        """
        Log warn
        """
        self.__log__('warn', paths, message, error)

    def debug(self, message, error = None, paths = None):
        """
        Log debug
        """
        self.__log__('debug', paths, message, error)

class Dispatcher(object):
    """
    The worker dispatcher
    """
    class Instance(object):
        """
        Dispatcher instance
        """
        def __init__(self, method, args, kwargs):
            self.method = method
            self.args = args
            self.kwargs = kwargs

        def __call__(self):
            """
            Invoke
            """
            args = self.args if not self.args is None else []
            kwargs = self.kwargs if not self.kwargs is None else {}
            return self.method(*args, **kwargs)

    def __init__(self, name, logger):
        self.__thread = Thread(name = name, target = self.__worker__)
        self.__lock = Lock()
        self.__sema = Semaphore(0)
        self.__isStopping = False
        self.__isRunning = False
        self.__queue = []
        self.__logger = InternalLogger('dispatcher', logger)

    def run(self):
        """
        Start running
        """
        if self.__isStopping:
            raise RuntimeError('Cannot run when stopping or stopped')
        if self.__isRunning:
            raise RuntimeError('Cannot run when running')
        self.__isRunning = True
        self.__thread.start()
        self.__logger.info('Started')

    def stop(self):
        """
        Stop running
        """
        self.__isStopping = True
        self.__sema.release()   # Notify worker thread

    def invoke(self, method, args = None, kwargs = None):
        """
        Add an method
        """
        if self.__isStopping:
            raise RuntimeError('Cannot invoke when stopping')
        if method is None:
            raise ValueError('Argument method cannot be None')
        instance = Dispatcher.Instance(method, args, kwargs)
        with lock(self.__lock):
            self.__queue.append(instance)
        self.__sema.release()

    def __worker__(self):
        """
        The dispatcher worker
        """
        while True:
            # Wait until new request in
            self.__sema.acquire()
            # Check if the dispatcher is stopping
            # If so ignore all existing invokings and exit
            if self.__isStopping:
                break

            with lock(self.__lock):
                # Test empty
                if len(self.__queue) == 0:
                    continue
                # Pop an instance
                instance = self.__queue.pop(0)
            # Do it
            try:
                instance()
            except Exception as error:
                # Something went wrong, do error report
                self.logger.error('Invoke failed', error)
        # Exit
        if len(self.__queue) > 0:
            self.logger.warn('Abandon %d invoking instance on exiting' % len(self.__queue))
        self.logger.info('Exit')

class Event(object):
    """
    Event object
    """
    def __init__(self):
        self.__events = []

    def add(self, handler):
        """
        Add an event handler
        """
        self.__events.append(handler)

    def remove(self, handler):
        """
        Remove an event handler
        """
        self.__events.remove(handler)

    def clear(self):
        """
        Clear all events
        """
        self.__events = []

    def __call__(self, sender, eventArgs):
        """
        Raise this event
        """
        for handler in this.__events:
            handler(sender, eventArgs)

class WorkerInstance(object):
    """
    A worker instance
    """
    def __init__(self, worker):
        self.__worker = worker

    @property
    def worker(self):
        return self.__worker

class Workerpool(object):
    """
    The worker pool
    """
    def __init__(self):
        self.__queues = {}
        self.__lock = Lock()

    def add(self, instance):
        """
        Add an worker instance
        """
        self[instance.worker.guid].append(instance)

    def __getitem__(self, guid):
        """
        Get a queue or create one by guid
        """
        with lock(self.__lock):
            if not guid in self.__queues:
                queue = WorkerQueue()
                self.__queues[guid] = queue
            else:
                queue = self.__queues[guid]
            return queue

class WorkerQueue(object):
    """
    The worker queue
    """
    def __init__(self):
        self.__lst = []
        self.__lock = Lock()

    def append(self, instance):
        """
        Add a worker instance
        """
        with lock(self.__lock):
            self.__lst.append(instance)

    def pop(self):
        """
        Pop a worker instance
        None if the queue is empty
        """
        with lock(self.__lock):
            if len(self.__lst) == 0:
                return None
            return self.__lst.pop(0)

class Components(object):
    """
    Component container
    NOTE:
        Why not use PluginRepo as Components directly?
        Since there're some components are application wide and some are workflow wide.
        The scopes are not consistent
    """
    def __init__(self, pluginRepo):
        self.pluginRepo = pluginRepo
        self.__comsdct = {}

    def __contains__(self, name):
        """
        Check component
        """
        return name in self.__comsdct

    def __getitem__(self, name):
        """
        Get component
        """
        return self.__comsdct[name]

    def __setitem__(self, name, value):
        """
        Set component
        """
        self.__comsdct[name] = value

class CallbackInvoker(object):
    """
    Support all callback invoking
    """
    def __init__(self, components):
        self.__dispatcher = components['dispatcher']

    def invoke(self, callbackHandler, *args, **kwargs):
        """
        Invoke callbackHandler
        NOTE:
            All callback should run in dispatcher in order to avoid multi-thread problems
        """
        self.__dispatcher.invoke(callbackHandler, args, kwargs)