'''
Created on 2015/11/30

:author: hubo

Process events multi-threaded or multi-processed
'''
from vlcp.event.runnable import RoutineContainer
import multiprocessing
import threading
import socket
import signal
import errno
import sys
from vlcp.event.event import Event, withIndices, M_
from vlcp.event.core import POLLING_IN, PollEvent
import functools
import traceback
from contextlib import closing
if sys.version_info[0] >= 3:
    from queue import Full, Queue, Empty
else:
    from Queue import Full, Queue, Empty
import logging
from vlcp.event.connection import ResolveRequestEvent, ResolveResponseEvent

@withIndices('connector', 'type')
class ConnectorControlEvent(Event):
    ADDMATCHERS = 'addmatcher'
    REMOVEMATCHERS = 'removematcher'
    SETMATCHERS = 'setmatchers'
    STOPRECEIVE = 'stopreceive'
    STARTRECEIVE = 'startreceive'
import os

@withIndices()
class MoreResultEvent(Event):
    pass

class _Pipe(object):
    "Make a pipe looks like a socket"
    def __init__(self, fd, canread = True):
        self.canread = canread
        self.canwrite = not canread
        self.fd = fd
    def setblocking(self, blocking):
        import fcntl
        if blocking:
            fcntl.fcntl(self.fd, fcntl.F_SETFL, 0)
        else:
            fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NONBLOCK)
    def fileno(self):
        return self.fd
    def send(self, data):
        try:
            os.write(self.fd, data)
        except OSError as exc:
            raise socket.error(*exc.args)
    def recv(self, size):
        try:
            return os.read(self.fd, size)
        except OSError as exc:
            raise socket.error(*exc.args)
    def close(self):
        if self.fd:
            os.close(self.fd)
            self.fd = None
    def __del__(self):
        self.close()

class Connector(RoutineContainer):
    def __init__(self, worker_start, matchers = (), scheduler = None, mp = True, inputlimit = 0, allowcontrol = True):
        '''
        :param worker_start: func(queuein, queueout), queuein is the input queue, queueout is the
               output queue. For queuein, each object is (event, matcher) tuple; For queueout, each
               object is a tuple of events to send. Every object in queuein must have a response in
               queueout.
        :param matcheres: match events to be processed by connector.
        :param scheduler: bind to specified scheduler
        :param mp: use multiprocessing if possible. For windows, multi-threading is always used.
        :param inputlimit: input queue size limit. 0 = infinite.
        :param allowcontrol: if True, the connector accepts ConnectorControlEvent for connector configuration.
        '''
        RoutineContainer.__init__(self, scheduler, True)
        self.worker_start = worker_start
        self.matchers = set(matchers)
        self.mp = mp
        self.inputlimit = inputlimit
        self.allowcontrol = allowcontrol
        self.stopreceive = False
        self.jobs = 0
        self._moreresult_matcher = MoreResultEvent.createMatcher()
    @staticmethod
    def wrap_signal(func):
        def f(*args, **kwargs):
            signal.signal(signal.SIGINT, signal.SIG_IGN)
            signal.signal(signal.SIGTERM, signal.SIG_DFL)
            return func(*args, **kwargs)
        return f
    @staticmethod
    def connector_pipe(queuein, pipeout, worker_start):
        try:
            queueout = multiprocessing.Queue()
            worker_start(queuein, queueout)
            while True:
                try:
                    events = queueout.get()
                    if events is None:
                        break
                    pipeout.send(events)
                except EOFError:
                    break
                except OSError as exc:
                    if exc.args[0] == errno.EINTR:
                        continue
                    else:
                        break
        finally:
            pipeout.close()
    @staticmethod
    def connector_socket(queuein, pipeout, worker_start):
        try:
            queueout = Queue()
            worker_start(queuein, queueout)
            while True:
                try:
                    events = queueout.get()
                    if events is None:
                        break
                    pipeout[0].put(events)
                    while True:
                        try:
                            events = queueout.get(False)
                            pipeout[0].put(events)
                        except Empty:
                            break
                    pipeout[1].send(b'\x00')
                except EOFError:
                    break
                except OSError as exc:
                    if exc.args[0] == errno.EINTR:
                        continue
                    else:
                        break
                except Exception:
                    pass
        finally:
            pipeout[1].close()
    def enqueue(self, queue, event, matcher):
        queue.put((event, matcher), False)
        event.canignore = True
        self.jobs += 1
        if self.jobs == 1:
            self.scheduler.setPollingDaemon(self.pipein, False)
    def sendevents(self, events):
        moreResult = False
        for e in events:
            if self._moreresult_matcher.isMatch(e):
                moreResult = True
            else:
                self.scheduler.emergesend(e)
        if not moreResult:
            self.jobs -= 1
        if self.jobs == 0:
            self.scheduler.setPollingDaemon(self.pipein, True)
    def _createobjs(self, fork, mp):
        if mp:
            queue = multiprocessing.Queue(self.inputlimit)
        else:
            queue = Queue(self.inputlimit)
        if mp:
            pipein, pipeout = multiprocessing.Pipe()
            process = multiprocessing.Process(target=self.wrap_signal(self.connector_pipe), args=(queue, pipeout, self.worker_start))
            outqueue = None
        else:
            # Use a thread instead
            # Create a socket on localhost
            if fork:
                # Linux
                pifd, pofd = os.pipe()
                pipein = _Pipe(pifd, True)
                pipeout = _Pipe(pofd, False)
                pipein.setblocking(False)
                pipeout.setblocking(True)
            else:
                addrinfo = socket.getaddrinfo('localhost', 0, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG|socket.AI_PASSIVE)
                socket_s = socket.socket(*addrinfo[0][0:2])
                socket_s.bind(addrinfo[0][4])
                socket_s.listen(1)
                addr_target = socket_s.getsockname()
                pipeout = socket.socket(*addrinfo[0][0:2])
                pipeout.setblocking(False)
                pipeout.connect_ex(addr_target)
                pipein, _ = socket_s.accept()
                pipein.setblocking(False)
                pipeout.setblocking(True)
                socket_s.close()
            outqueue = Queue()
            process = threading.Thread(target=self.connector_socket, args=(queue, (outqueue, pipeout), self.worker_start))
        process.daemon = True
        self.pipein = pipein
        return (process, queue, pipein, outqueue)
    async def main(self):
        import os
        self.resolving = set()
        if hasattr(os, 'fork'):
            fork = True
        else:
            fork = False
        mp = self.mp and fork
        (process, queue, pipein, outqueue) = self._createobjs(fork, mp)
        try:
            process.start()
            self.scheduler.registerPolling(pipein, POLLING_IN, True)
            response_matcher = PollEvent.createMatcher(pipein.fileno(), PollEvent.READ_READY)
            error_matcher = PollEvent.createMatcher(pipein.fileno(), PollEvent.ERROR)
            control_matcher = ConnectorControlEvent.createMatcher(self)
            if self.allowcontrol:
                system_matchers = (response_matcher, error_matcher, control_matcher)
            else:
                system_matchers = (response_matcher, error_matcher)
            isFull = False
            isEOF = False
            while True:
                if not isEOF:
                    if isFull or self.stopreceive:
                        ev, m = await M_(*system_matchers)
                    else:
                        ev, m = await M_(*(tuple(self.matchers) + system_matchers))
                    if m is error_matcher:
                        isEOF = True
                if isEOF:
                    self.scheduler.unregisterPolling(pipein, self.jobs == 0)
                    self.jobs = 0
                    pipein.close()
                    pipein = None
                    await self.wait_with_timeout(1)
                    if mp:
                        process.terminate()
                    (process, queue, pipein, outqueue) = self._createobjs(fork, mp)
                    process.start()
                    self.scheduler.registerPolling(pipein, POLLING_IN, True)
                    response_matcher = PollEvent.createMatcher(pipein.fileno(), PollEvent.READ_READY)
                    error_matcher = PollEvent.createMatcher(pipein.fileno(), PollEvent.ERROR)
                    if self.allowcontrol:
                        system_matchers = (response_matcher, error_matcher, control_matcher)
                    else:
                        system_matchers = (response_matcher, error_matcher)
                    isFull = False
                elif m is control_matcher:
                    if ev.type == ConnectorControlEvent.ADDMATCHERS:
                        for m in ev.matchers:
                            self.matchers.add(m)
                    elif ev.type == ConnectorControlEvent.REMOVEMATCHERS:
                        for m in ev.matchers:
                            self.matchers.discard(m)
                    elif ev.type == ConnectorControlEvent.SETMATCHERS:
                        self.matchers = set(ev.matchers)
                    elif ev.type == ConnectorControlEvent.STOPRECEIVE:
                        self.stopreceive = True
                    else:
                        self.stopreceive = False
                elif m is response_matcher:
                    if mp:
                        while pipein.poll():
                            try:
                                events = pipein.recv()
                            except EOFError:
                                isEOF = True
                                break
                            self.sendevents(events)
                    else:
                        while True:
                            try:
                                if not pipein.recv(4096):
                                    isEOF = True
                                    break
                            except socket.error as exc:
                                if exc.errno == errno.EAGAIN or exc.errno == errno.EWOULDBLOCK:
                                    break
                                elif exc.errno == errno.EINTR:
                                    continue
                                else:
                                    isEOF = True
                                    break
                        while True:
                            try:
                                events = outqueue.get(False)
                            except Empty:
                                break
                            self.sendevents(events)
                    isFull = False
                else:
                    try:
                        self.enqueue(queue, ev, m)
                    except Full:
                        isFull = True
        finally:
            if pipein is not None:
                self.scheduler.unregisterPolling(pipein, self.jobs == 0)
                pipein.close()
            if mp:
                process.terminate()
            else:
                queue.put(None)

class ThreadPool(object):
    def __init__(self, poolsize, worker, mp = False):
        self.poolsize = poolsize
        self.worker = worker
        self.mp = mp
    def create(self, queuein, queueout):
        import os
        if hasattr(os, 'fork') and self.mp:
            pool = [multiprocessing.Process(target=self.worker, args=(queuein, queueout)) for i in range(0, self.poolsize)]
        else:
            pool = [threading.Thread(target=self.worker, args=(queuein, queueout)) for i in range(0, self.poolsize)]
        for p in pool:
            p.daemon = True
            p.start()



def async_to_async(newthread = True, mp = False):
    def decorator(func):
        @functools.wraps(func)
        def handler(*args, **kwargs):
            if mp:
                p = multiprocessing.Process(target=func, args=args, kwargs=kwargs)
                p.daemon = True
                p.start()
            elif newthread:
                t = threading.Thread(target=func, args=args, kwargs=kwargs)
                t.daemon = True
                t.start()
            else:
                func(*args, **kwargs)
        return handler
    return decorator

def async_processor(func):
    @functools.wraps(func)
    def handler(queuein, queueout):
        while True:
            try:
                r = queuein.get(True)
                if r is None:
                    queueout.put(None)
                    break
                event, matcher = r
            except OSError as exc:
                if exc.args[0] == errno.EINTR:
                    continue
                else:
                    break
            try:
                func(event, matcher, queueout)
            except Exception:
                # Ignore
                pass
    return handler

def processor_to_async(newthread = False, mp = False):
    def decorator(func):
        @functools.wraps(func)
        @async_to_async(newthread, mp)
        def handler(event, matcher, queueout):
            try:
                output = func(event, matcher)
            except Exception:
                queueout.put(())
            else:
                queueout.put(output)
        return handler
    return decorator

def processor(func, newthread = False, mp = False):
    return async_processor(processor_to_async()(func))

def generator_to_async(newthread = True, mp = False):
    def decorator(func):
        @functools.wraps(func)
        @async_to_async(newthread, mp)
        def handler(event, matcher, queueout):
            for es in func(event, matcher):
                queueout.put(tuple(es) + (MoreResultEvent(),))
            queueout.put(())
        return handler
    return decorator
    
def async_generator_processor(func, newthread = True, mp = False):
    return async_processor(generator_to_async(func, newthread, mp))

@withIndices('pool')
class TaskEvent(Event):
    canignore = False

@withIndices('request')
class TaskDoneEvent(Event):
    pass

class TaskPool(Connector):
    "Thread pool for small tasks"
    @staticmethod
    def _generator_wrapper(func):
        def g(event, matcher):
            try:
                r = yield from func()
            except Exception:
                (typ, val, tr) = sys.exc_info()
                yield (TaskDoneEvent(event, exception = val),)
            else:
                yield (TaskDoneEvent(event, result = r),)
        return g
    @staticmethod
    def _processor_wrapper(func):
        def f(event, matcher):
            try:
                return (TaskDoneEvent(event, result=func()),)
            except Exception:
                (typ, val, tr) = sys.exc_info()
                return (TaskDoneEvent(event, exception = val),)
        return f
    @staticmethod
    def _async_wrapper(func):
        def f(event, matcher, queueout):
            def sender(es):
                queueout.put(tuple(es) + (MoreResultEvent(),))
            try:
                queueout.put((TaskDoneEvent(event, result=func(sender)),))
            except Exception:
                (typ, val, tr) = sys.exc_info()
                queueout.put((TaskDoneEvent(event, exception = val),))
        return f
    @staticmethod
    @async_processor
    def _processor(event, matcher, queueout):
        if hasattr(event, 'task'):
            processor_to_async(getattr(event, 'newthread', False))(TaskPool._processor_wrapper(event.task))(event, matcher, queueout)
        elif hasattr(event, 'gen_task'):
            generator_to_async(getattr(event, 'newthread', True))(TaskPool._generator_wrapper(event.gen_task))(event, matcher, queueout)
        elif hasattr(event, 'async_task'):
            async_to_async(getattr(event, 'newthread', True))(TaskPool._async_wrapper(event.async_task))(event, matcher, queueout)
        else:
            queueout.put(())
    def __init__(self, scheduler=None, poolsize = 64):
        self.threadpool = ThreadPool(poolsize, self._processor, False)
        Connector.__init__(self, self.threadpool.create, matchers=(TaskEvent.createMatcher(self),), scheduler=scheduler,
                           mp=False, inputlimit=poolsize, allowcontrol=False)
    async def run_task(self, container, task, newthread = False):
        "Run task() in task pool. Raise an exception or return the return value"
        e = TaskEvent(self, task=task, newthread = newthread)
        await container.wait_for_send(e)
        ev = await TaskDoneEvent.createMatcher(e)
        if hasattr(ev, 'exception'):
            raise ev.exception
        else:
            return ev.result
    
    runTask = run_task
    
    async def run_gen_task(self, container, gentask, newthread = True):
        "Run generator gentask() in task pool, yield customized events"
        e = TaskEvent(self, gen_task = gentask, newthread = newthread)
        await container.wait_for_send(e)
        ev = await TaskDoneEvent.createMatcher(e)
        if hasattr(ev, 'exception'):
            raise ev.exception
        else:
            return ev.result
    
    runGenTask = run_gen_task
    
    async def run_async_task(self, container, asynctask, newthread = True):
        "Run asynctask(sender) in task pool, call sender(events) to send customized events, return result"
        e = TaskEvent(self, async_task = asynctask, newthread = newthread)
        await container.wait_for_send(e)
        ev = await TaskDoneEvent.createMatcher(e)
        if hasattr(ev, 'exception'):
            raise ev.exception
        else:
            return ev.result
    
    runAsyncTask = run_async_task

class Resolver(Connector):
    logger = logging.getLogger(__name__ + '.Resolver')
    def __init__(self, scheduler = None, poolsize = 256):
        rm = ResolveRequestEvent.createMatcher()
        Connector.__init__(self, ThreadPool(poolsize, Resolver.resolver).create, (rm,), scheduler, False, poolsize, False)
        self.resolving = set()
    @staticmethod
    @processor
    def resolver(event, matcher):
        params = event.request
        try:
            addrinfo = socket.getaddrinfo(*params)
            return (ResolveResponseEvent(params, response=addrinfo),)
        except Exception:
            et, ev, tr = sys.exc_info()
            return (ResolveResponseEvent(params, error=ev),)
    def enqueue(self, queue, event, matcher):
        if event.request in self.resolving:
            # Duplicated resolves are finished in the same time
            event.canignore = True
        else:
            Connector.enqueue(self, queue, event, matcher)
            self.resolving.add(event.request)
    def sendevents(self, events):
        Connector.sendevents(self, events)
        for e in events:
            self.resolving.remove(e.request)
