'''
TUIO Input Provider
===================

TUIO is the de facto standard network protocol for the transmission of
touch and fiducial information between a server and a client. To learn
more about TUIO (which is itself based on the OSC protocol), please
refer to http://tuio.org -- The specification should be of special
interest.

Configure a TUIO provider in the config.ini
-------------------------------------------

The TUIO provider can be configured in the configuration file in the
``[input]`` section::

    [input]
    # name = tuio,<ip>:<port>
    multitouchtable = tuio,192.168.0.1:3333

Configure a TUIO provider in the App
------------------------------------

You must add the provider before your application is run, like this::

    from kivy.app import App
    from kivy.config import Config

    class TestApp(App):
        def build(self):
            Config.set('input', 'multitouchscreen1', 'tuio,0.0.0.0:3333')
            # You can also add a second TUIO listener
            # Config.set('input', 'source2', 'tuio,0.0.0.0:3334')
            # Then do the usual things
            # ...
            return
'''

__all__ = ('TuioMotionEventProvider', 'Tuio2dCurMotionEvent',
           'Tuio2dObjMotionEvent')

from kivy.logger import Logger

from functools import partial
from collections import deque
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect


class TuioMotionEventProvider(MotionEventProvider):
    '''The TUIO provider listens to a socket and handles some of the incoming
    OSC messages:

        * /tuio/2Dcur
        * /tuio/2Dobj

    You can easily extend the provider to handle new TUIO paths like so::

        # Create a class to handle the new TUIO type/path
        # Replace NEWPATH with the pathname you want to handle
        class TuioNEWPATHMotionEvent(MotionEvent):

            def depack(self, args):
                # In this method, implement 'unpacking' for the received
                # arguments. you basically translate from TUIO args to Kivy
                # MotionEvent variables. If all you receive are x and y
                # values, you can do it like this:
                if len(args) == 2:
                    self.sx, self.sy = args
                    self.profile = ('pos', )
                self.sy = 1 - self.sy
                super().depack(args)

        # Register it with the TUIO MotionEvent provider.
        # You obviously need to replace the PATH placeholders appropriately.
        TuioMotionEventProvider.register('/tuio/PATH', TuioNEWPATHMotionEvent)

    .. note::

        The class name is of no technical importance. Your class will be
        associated with the path that you pass to the ``register()``
        function. To keep things simple, you should name your class after the
        path that it handles, though.
    '''

    __handlers__ = {}

    def __init__(self, device, args):
        super().__init__(device, args)
        args = args.split(',')
        if len(args) == 0:
            Logger.error('Tuio: Invalid configuration for TUIO provider')
            Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)')
            err = 'Tuio: Current configuration is <%s>' % (str(','.join(args)))
            Logger.error(err)
            return
        ipport = args[0].split(':')
        if len(ipport) != 2:
            Logger.error('Tuio: Invalid configuration for TUIO provider')
            Logger.error('Tuio: Format must be ip:port (eg. 127.0.0.1:3333)')
            err = 'Tuio: Current configuration is <%s>' % (str(','.join(args)))
            Logger.error(err)
            return
        self.ip, self.port = args[0].split(':')
        self.port = int(self.port)
        self.handlers = {}
        self.oscid = None
        self.tuio_event_q = deque()
        self.touches = {}

    @staticmethod
    def register(oscpath, classname):
        '''Register a new path to handle in TUIO provider'''
        TuioMotionEventProvider.__handlers__[oscpath] = classname

    @staticmethod
    def unregister(oscpath, classname):
        '''Unregister a path to stop handling it in the TUIO provider'''
        if oscpath in TuioMotionEventProvider.__handlers__:
            del TuioMotionEventProvider.__handlers__[oscpath]

    @staticmethod
    def create(oscpath, **kwargs):
        '''Create a touch event from a TUIO path'''
        if oscpath not in TuioMotionEventProvider.__handlers__:
            raise Exception('Unknown %s touch path' % oscpath)
        return TuioMotionEventProvider.__handlers__[oscpath](**kwargs)

    def start(self):
        '''Start the TUIO provider'''
        try:
            from oscpy.server import OSCThreadServer
        except ImportError:
            Logger.info(
                'Please install the oscpy python module to use the TUIO '
                'provider.'
            )
            raise
        self.oscid = osc = OSCThreadServer()
        osc.listen(self.ip, self.port, default=True)
        for oscpath in TuioMotionEventProvider.__handlers__:
            self.touches[oscpath] = {}
            osc.bind(oscpath, partial(self._osc_tuio_cb, oscpath))

    def stop(self):
        '''Stop the TUIO provider'''
        self.oscid.stop_all()

    def update(self, dispatch_fn):
        '''Update the TUIO provider (pop events from the queue)'''

        # read the Queue with event
        while True:
            try:
                value = self.tuio_event_q.pop()
            except IndexError:
                # queue is empty, we're done for now
                return
            self._update(dispatch_fn, value)

    def _osc_tuio_cb(self, oscpath, address, *args):
        self.tuio_event_q.appendleft([oscpath, address, args])

    def _update(self, dispatch_fn, value):
        oscpath, command, args = value

        # verify commands
        if command not in [b'alive', b'set']:
            return

        # move or create a new touch
        if command == b'set':
            id = args[0]
            if id not in self.touches[oscpath]:
                # new touch
                touch = TuioMotionEventProvider.__handlers__[oscpath](
                    self.device, id, args[1:])
                self.touches[oscpath][id] = touch
                dispatch_fn('begin', touch)
            else:
                # update a current touch
                touch = self.touches[oscpath][id]
                touch.move(args[1:])
                dispatch_fn('update', touch)

        # alive event, check for deleted touch
        if command == b'alive':
            alives = args
            to_delete = []
            for id in self.touches[oscpath]:
                if id not in alives:
                    # touch up
                    touch = self.touches[oscpath][id]
                    if touch not in to_delete:
                        to_delete.append(touch)

            for touch in to_delete:
                dispatch_fn('end', touch)
                del self.touches[oscpath][touch.id]


class TuioMotionEvent(MotionEvent):
    '''Abstraction for TUIO touches/fiducials.

    Depending on the tracking software you use (e.g. Movid, CCV, etc.) and its
    TUIO implementation, the TuioMotionEvent object can support multiple
    profiles such as:

        * Fiducial ID: profile name 'markerid', attribute ``.fid``
        * Position: profile name 'pos', attributes ``.x``, ``.y``
        * Angle: profile name 'angle', attribute ``.a``
        * Velocity vector: profile name 'mov', attributes ``.X``, ``.Y``
        * Rotation velocity: profile name 'rot', attribute ``.A``
        * Motion acceleration: profile name 'motacc', attribute ``.m``
        * Rotation acceleration: profile name 'rotacc', attribute ``.r``
    '''
    __attrs__ = ('a', 'b', 'c', 'X', 'Y', 'Z', 'A', 'B', 'C', 'm', 'r')

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('is_touch', True)
        kwargs.setdefault('type_id', 'touch')
        super().__init__(*args, **kwargs)
        # Default argument for TUIO touches
        self.a = 0.0
        self.b = 0.0
        self.c = 0.0
        self.X = 0.0
        self.Y = 0.0
        self.Z = 0.0
        self.A = 0.0
        self.B = 0.0
        self.C = 0.0
        self.m = 0.0
        self.r = 0.0

    angle = property(lambda self: self.a)
    mot_accel = property(lambda self: self.m)
    rot_accel = property(lambda self: self.r)
    xmot = property(lambda self: self.X)
    ymot = property(lambda self: self.Y)
    zmot = property(lambda self: self.Z)


class Tuio2dCurMotionEvent(TuioMotionEvent):
    '''A 2dCur TUIO touch.'''

    def depack(self, args):
        if len(args) < 5:
            self.sx, self.sy = list(map(float, args[0:2]))
            self.profile = ('pos', )
        elif len(args) == 5:
            self.sx, self.sy, self.X, self.Y, self.m = list(map(float,
                                                                args[0:5]))
            self.Y = -self.Y
            self.profile = ('pos', 'mov', 'motacc')
        else:
            self.sx, self.sy, self.X, self.Y = list(map(float, args[0:4]))
            self.m, width, height = list(map(float, args[4:7]))
            self.Y = -self.Y
            self.profile = ('pos', 'mov', 'motacc', 'shape')
            if self.shape is None:
                self.shape = ShapeRect()
            self.shape.width = width
            self.shape.height = height
        self.sy = 1 - self.sy
        super().depack(args)


class Tuio2dObjMotionEvent(TuioMotionEvent):
    '''A 2dObj TUIO object.
    '''

    def depack(self, args):
        if len(args) < 5:
            self.sx, self.sy = args[0:2]
            self.profile = ('pos', )
        elif len(args) == 9:
            self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6]
            self.A, self.m, self.r = args[6:9]
            self.Y = -self.Y
            self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot',
                            'motacc', 'rotacc')
        else:
            self.fid, self.sx, self.sy, self.a, self.X, self.Y = args[:6]
            self.A, self.m, self.r, width, height = args[6:11]
            self.Y = -self.Y
            self.profile = ('markerid', 'pos', 'angle', 'mov', 'rot', 'rotacc',
                            'acc', 'shape')
            if self.shape is None:
                self.shape = ShapeRect()
                self.shape.width = width
                self.shape.height = height
        self.sy = 1 - self.sy
        super().depack(args)


class Tuio2dBlbMotionEvent(TuioMotionEvent):
    '''A 2dBlb TUIO object.
    # FIXME 3d shape are not supported
    /tuio/2Dobj set s i x y a       X Y A m r
    /tuio/2Dblb set s   x y a w h f X Y A m r
    '''

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.profile = ('pos', 'angle', 'mov', 'rot', 'rotacc', 'acc', 'shape')

    def depack(self, args):
        self.sx, self.sy, self.a, self.X, self.Y, sw, sh, sd, \
            self.A, self.m, self.r = args
        self.Y = -self.Y
        if self.shape is None:
            self.shape = ShapeRect()
            self.shape.width = sw
            self.shape.height = sh
        self.sy = 1 - self.sy
        super().depack(args)


# registers
TuioMotionEventProvider.register(b'/tuio/2Dcur', Tuio2dCurMotionEvent)
TuioMotionEventProvider.register(b'/tuio/2Dobj', Tuio2dObjMotionEvent)
TuioMotionEventProvider.register(b'/tuio/2Dblb', Tuio2dBlbMotionEvent)
MotionEventFactory.register('tuio', TuioMotionEventProvider)
