'''
Support for WM_TOUCH messages (Windows platform)
================================================
'''

__all__ = ('WM_MotionEventProvider', 'WM_MotionEvent')

import os
from kivy.input.providers.wm_common import WNDPROC, \
    SetWindowLong_WndProc_wrapper, RECT, POINT, WM_TABLET_QUERYSYSTEMGESTURE, \
    QUERYSYSTEMGESTURE_WNDPROC, WM_TOUCH, WM_MOUSEMOVE, WM_MOUSELAST, \
    TOUCHINPUT, PEN_OR_TOUCH_MASK, PEN_OR_TOUCH_SIGNATURE, PEN_EVENT_TOUCH_MASK
from kivy.input.motionevent import MotionEvent
from kivy.input.shape import ShapeRect

Window = None


class WM_MotionEvent(MotionEvent):
    '''MotionEvent representing the WM_MotionEvent event.
       Supports pos, shape and size profiles.
    '''
    __attrs__ = ('size', )

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

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

    def __str__(self):
        args = (self.id, self.uid, str(self.spos), self.device)
        return '<WMMotionEvent id:%d uid:%d pos:%s device:%s>' % args


if 'KIVY_DOC' in os.environ:
    # documentation hack
    WM_MotionEventProvider = None

else:
    from ctypes.wintypes import HANDLE
    from ctypes import (windll, sizeof, byref)
    from collections import deque
    from kivy.input.provider import MotionEventProvider
    from kivy.input.factory import MotionEventFactory

    class WM_MotionEventProvider(MotionEventProvider):

        def start(self):
            global Window
            if not Window:
                from kivy.core.window import Window

            self.touch_events = deque()
            self.touches = {}
            self.uid = 0

            # get window handle, and register to receive WM_TOUCH messages
            self.hwnd = windll.user32.GetActiveWindow()
            windll.user32.RegisterTouchWindow(self.hwnd, 1)

            # inject our own wndProc to handle messages
            # before window manager does
            self.new_windProc = WNDPROC(self._touch_wndProc)
            self.old_windProc = SetWindowLong_WndProc_wrapper(
                self.hwnd, self.new_windProc)

        def update(self, dispatch_fn):
            c_rect = RECT()
            windll.user32.GetClientRect(self.hwnd, byref(c_rect))
            pt = POINT(x=0, y=0)
            windll.user32.ClientToScreen(self.hwnd, byref(pt))
            x_offset, y_offset = pt.x, pt.y
            usable_w, usable_h = float(c_rect.w), float(c_rect.h)

            while True:
                try:
                    t = self.touch_events.pop()
                except:
                    break

                # adjust x,y to window coordinates (0.0 to 1.0)
                x = (t.screen_x() - x_offset) / usable_w
                y = 1.0 - (t.screen_y() - y_offset) / usable_h

                # actually dispatch input
                if t.event_type == 'begin':
                    self.uid += 1
                    self.touches[t.id] = WM_MotionEvent(
                        self.device, self.uid, [x, y, t.size()])
                    dispatch_fn('begin', self.touches[t.id])

                if t.event_type == 'update' and t.id in self.touches:
                    self.touches[t.id].move([x, y, t.size()])
                    dispatch_fn('update', self.touches[t.id])

                if t.event_type == 'end' and t.id in self.touches:
                    touch = self.touches[t.id]
                    touch.move([x, y, t.size()])
                    touch.update_time_end()
                    dispatch_fn('end', touch)
                    del self.touches[t.id]

        def stop(self):
            windll.user32.UnregisterTouchWindow(self.hwnd)
            self.new_windProc = SetWindowLong_WndProc_wrapper(
                self.hwnd, self.old_windProc)

        # we inject this wndProc into our main window, to process
        # WM_TOUCH and mouse messages before the window manager does
        def _touch_wndProc(self, hwnd, msg, wParam, lParam):
            done = False
            if msg == WM_TABLET_QUERYSYSTEMGESTURE:
                return QUERYSYSTEMGESTURE_WNDPROC

            if msg == WM_TOUCH:
                done = self._touch_handler(msg, wParam, lParam)

            if msg >= WM_MOUSEMOVE and msg <= WM_MOUSELAST:
                done = self._mouse_handler(msg, wParam, lParam)

            if not done:
                return windll.user32.CallWindowProcW(self.old_windProc,
                                                     hwnd, msg, wParam,
                                                     lParam)
            return 1

        # this on pushes WM_TOUCH messages onto our event stack
        def _touch_handler(self, msg, wParam, lParam):
            touches = (TOUCHINPUT * wParam)()
            windll.user32.GetTouchInputInfo(HANDLE(lParam),
                                            wParam,
                                            touches,
                                            sizeof(TOUCHINPUT))
            for i in range(wParam):
                self.touch_events.appendleft(touches[i])
            windll.user32.CloseTouchInputHandle(HANDLE(lParam))
            return True

        # filter fake mouse events, because touch and stylus
        # also make mouse events
        def _mouse_handler(self, msg, wparam, lParam):
            info = windll.user32.GetMessageExtraInfo()
            # its a touch or a pen
            if (info & PEN_OR_TOUCH_MASK) == PEN_OR_TOUCH_SIGNATURE:
                if info & PEN_EVENT_TOUCH_MASK:
                    return True

    MotionEventFactory.register('wm_touch', WM_MotionEventProvider)
