"""
Bridge

The Bridge Component is used for inter-process communications between
processes. Bridge is used internally when a Component is started in
"process mode" via :meth:`circuits.core.manager.start`. Typically a
Pipe is used as the socket transport between two sides of a Bridge
(*there must be a :class:`~Bridge` instance on both sides*).
"""

import traceback
from pickle import dumps, loads

from .components import BaseComponent
from .events import Event, exception
from .handlers import handler
from .values import Value


_sentinel = b'~~~'


class ipc(Event):
    """
    ipc Event

    Send an event to a child/parent process
    """

    def __init__(self, event, channel=None):
        """
        :param event:   Event to execute remotely.
        :type event:    :class:`circuits.core.events.Event`

        :param channel: IPC Channel (channel to use on child/parent).
        :type channel:  str
        """
        super().__init__(event, channel=channel)


class Bridge(BaseComponent):
    channel = 'bridge'

    def init(self, socket, channel=channel):
        self._buffer = b''
        self._socket = socket
        self._values = {}

        if self._socket is not None:
            self._socket.register(self)

    def _process_packet(self, eid, obj):
        if isinstance(obj, Event):
            obj.remote = True
            obj.notify = 'value_changed'
            obj.waitingHandlers = 0
            value = self.fire(obj)
            self._values[value] = eid
        elif isinstance(obj, Value):
            if obj.result:
                if isinstance(obj.value, list):
                    for item in obj.value:
                        self._values[eid].value = item
                else:
                    self._values[eid].value = obj.value
            event = Event.create(Bridge.__waiting_event(eid))
            event.remote = True
            self.fire(event, self.channel)

    @handler('value_changed', channel='*')
    def _on_value_changed(self, value):
        try:
            eid = self._values[value]
            if value.errors:
                Bridge.__adapt_error_value(value)
            self.__write(eid, value)
        except Exception:
            pass

    @handler('read')
    def _on_read(self, data):
        self._buffer += data
        items = self._buffer.split(_sentinel)

        if items[-1] != '':
            self._buffer = items.pop()

        for item in filter(None, items):
            self._process_packet(*loads(item))

    def __send(self, eid, event):
        try:
            if isinstance(event, exception):
                Bridge.__adapt_exception(event)
            self._values[eid] = event.value
            self.__write(eid, event)
        except Exception:
            pass

    def __write(self, eid, data):
        self._socket.write(dumps((eid, data)) + _sentinel)

    @handler('ipc')
    def _on_ipc(self, event, ipc_event, channel=None):
        """
        Send event to a child/parentprocess

        Event handler to run an event on a child/parent process
        (the event definition is :class:`circuits.core.bridge.ipc`)

        :param event:    The event triggered (by the handler)
        :type event:     :class:`circuits.node.events.remote`

        :param ipc_event:    Event to execute in child/parent process.
        :type ipc_event:     :class:`circuits.core.events.Event`

        :param channel:    Remote channel (channel to use on peer).
        :type channel:     str

        :return: The result of remote event
        :rtype: generator

        :Example:
        ``# hello is your event to execute in the child process
        result = yield self.fire(ipc(hello()))
        print(result.value)``
        """
        ipc_event.channels = (channel,) if channel is not None else event.channels
        event.value.value = ipc_event.value = Value(ipc_event, self)

        eid = hash(ipc_event)
        self.__send(eid, ipc_event)
        yield self.wait(Bridge.__waiting_event(eid))

    @staticmethod
    def __waiting_event(eid):
        return '%s_done' % eid

    @staticmethod
    def __adapt_exception(ex):
        fevent_value = ex.kwargs['fevent'].value
        Bridge.__adapt_error_value(fevent_value)

    @staticmethod
    def __adapt_error_value(value):
        if not isinstance(value[2], list):
            value._value = (value[0], value[1], traceback.extract_tb(value[2]))
