'''
Native support of MultitouchSupport framework for MacBook (MaxOSX platform)
===========================================================================
'''

__all__ = ('MacMotionEventProvider', )

import ctypes
import threading
import collections
import os
from kivy.input.provider import MotionEventProvider
from kivy.input.factory import MotionEventFactory
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect

if 'KIVY_DOC' not in os.environ:
    CFArrayRef = ctypes.c_void_p
    CFMutableArrayRef = ctypes.c_void_p
    CFIndex = ctypes.c_long

    dll = '/System/Library/PrivateFrameworks/' + \
        'MultitouchSupport.framework/MultitouchSupport'
    MultitouchSupport = ctypes.CDLL(dll)

    CFArrayGetCount = MultitouchSupport.CFArrayGetCount
    CFArrayGetCount.argtypes = [CFArrayRef]
    CFArrayGetCount.restype = CFIndex

    CFArrayGetValueAtIndex = MultitouchSupport.CFArrayGetValueAtIndex
    CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex]
    CFArrayGetValueAtIndex.restype = ctypes.c_void_p

    MTDeviceCreateList = MultitouchSupport.MTDeviceCreateList
    MTDeviceCreateList.argtypes = []
    MTDeviceCreateList.restype = CFMutableArrayRef

    class MTPoint(ctypes.Structure):
        _fields_ = [('x', ctypes.c_float),
                    ('y', ctypes.c_float)]

    class MTVector(ctypes.Structure):
        _fields_ = [('position', MTPoint),
                    ('velocity', MTPoint)]

    class MTData(ctypes.Structure):
        _fields_ = [
            ('frame', ctypes.c_int),
            ('timestamp', ctypes.c_double),
            ('identifier', ctypes.c_int),
            # Current state (of unknown meaning).
            ('state', ctypes.c_int),
            ('unknown1', ctypes.c_int),
            ('unknown2', ctypes.c_int),
            # Normalized position and vector of the touch (0 to 1)
            ('normalized', MTVector),
            # The area of the touch.
            ('size', ctypes.c_float),
            ('unknown3', ctypes.c_int),
            # The following three define the ellipsoid of a finger.
            ('angle', ctypes.c_float),
            ('major_axis', ctypes.c_float),
            ('minor_axis', ctypes.c_float),
            ('unknown4', MTVector),
            ('unknown5_1', ctypes.c_int),
            ('unknown5_2', ctypes.c_int),
            ('unknown6', ctypes.c_float), ]

    MTDataRef = ctypes.POINTER(MTData)

    MTContactCallbackFunction = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int,
                                                 MTDataRef, ctypes.c_int,
                                                 ctypes.c_double, ctypes.c_int)

    MTDeviceRef = ctypes.c_void_p

    MTRegisterContactFrameCallback = \
        MultitouchSupport.MTRegisterContactFrameCallback
    MTRegisterContactFrameCallback.argtypes = \
        [MTDeviceRef, MTContactCallbackFunction]
    MTRegisterContactFrameCallback.restype = None

    MTDeviceStart = MultitouchSupport.MTDeviceStart
    MTDeviceStart.argtypes = [MTDeviceRef, ctypes.c_int]
    MTDeviceStart.restype = None

else:
    MTContactCallbackFunction = lambda x: None


class MacMotionEvent(MotionEvent):
    '''MotionEvent representing a contact point on the touchpad. Supports pos
    and shape profiles.
    '''

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('is_touch', True)
        kwargs.setdefault('type_id', 'touch')
        super().__init__(*args, **kwargs)
        self.profile = ('pos', 'shape')

    def depack(self, args):
        self.shape = ShapeRect()
        self.sx, self.sy = args[0], args[1]
        self.shape.width = args[2]
        self.shape.height = args[2]
        super().depack(args)

    def __str__(self):
        return '<MacMotionEvent id=%d pos=(%f, %f) device=%s>' \
            % (self.id, self.sx, self.sy, self.device)


_instance = None


class MacMotionEventProvider(MotionEventProvider):

    def __init__(self, *largs, **kwargs):
        global _instance
        if _instance is not None:
            raise Exception('Only one MacMotionEvent provider is allowed.')
        _instance = self
        super(MacMotionEventProvider, self).__init__(*largs, **kwargs)

    def start(self):
        # global uid
        self.uid = 0
        # touches will be per devices
        self.touches = {}
        # lock needed to access on uid
        self.lock = threading.Lock()
        # event queue to dispatch in main thread
        self.queue = collections.deque()

        # ok, listing devices, and attach !
        devices = MultitouchSupport.MTDeviceCreateList()
        num_devices = CFArrayGetCount(devices)
        for i in range(num_devices):
            device = CFArrayGetValueAtIndex(devices, i)
            # create touch dict for this device
            data_id = str(device)
            self.touches[data_id] = {}
            # start !
            MTRegisterContactFrameCallback(device, self._mts_callback)
            MTDeviceStart(device, 0)

    def update(self, dispatch_fn):
        # dispatch all event from threads
        try:
            while True:
                event_type, touch = self.queue.popleft()
                dispatch_fn(event_type, touch)
        except:
            pass

    def stop(self):
        # i don't known how to stop it...
        pass

    @MTContactCallbackFunction
    def _mts_callback(device, data_ptr, n_fingers, timestamp, frame):
        global _instance
        devid = str(device)

        # XXX create live touch, we get one case that
        # the device announced by macosx don't match the device
        # in _mts_callback....
        if devid not in _instance.touches:
            _instance.touches[devid] = {}

        touches = _instance.touches[devid]
        actives = []

        for i in range(n_fingers):
            # get pointer on data
            data = data_ptr[i]

            # add this touch as an active touch
            actives.append(data.identifier)

            # extract identifier
            data_id = data.identifier

            # prepare argument position
            norm_pos = data.normalized.position
            args = (norm_pos.x, norm_pos.y, data.size)

            if data_id not in touches:
                # increment uid
                _instance.lock.acquire()
                _instance.uid += 1
                # create a touch
                touch = MacMotionEvent(_instance.device, _instance.uid, args)
                _instance.lock.release()
                # create event
                _instance.queue.append(('begin', touch))
                # store touch
                touches[data_id] = touch
            else:
                touch = touches[data_id]
                # check if he really moved
                if data.normalized.position.x == touch.sx and \
                   data.normalized.position.y == touch.sy:
                    continue
                touch.move(args)
                _instance.queue.append(('update', touch))

        # delete old touchs
        for tid in list(touches.keys())[:]:
            if tid not in actives:
                touch = touches[tid]
                touch.update_time_end()
                _instance.queue.append(('end', touch))
                del touches[tid]

        return 0


MotionEventFactory.register('mactouch', MacMotionEventProvider)
