# -*- coding: utf-8 -*-
import ctypes
import time
from ctypes import c_short, c_char, c_uint8, c_int32, c_int, c_uint, c_uint32, c_long, byref, Structure, CFUNCTYPE, POINTER
from ctypes.wintypes import DWORD, BOOL, HHOOK, MSG, LPWSTR, WCHAR, WPARAM, LPARAM
LPMSG = POINTER(MSG)

import atexit

from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN, DOUBLE, WHEEL, HORIZONTAL, VERTICAL

#user32 = ctypes.windll.user32
user32 = ctypes.WinDLL('user32', use_last_error = True)

class MSLLHOOKSTRUCT(Structure):
    _fields_ = [("x", c_long),
                ("y", c_long),
                ('data', c_int32),
                ('reserved', c_int32),
                ("flags", DWORD),
                ("time", c_int),
                ]

LowLevelMouseProc = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(MSLLHOOKSTRUCT))

SetWindowsHookEx = user32.SetWindowsHookExA
#SetWindowsHookEx.argtypes = [c_int, LowLevelMouseProc, c_int, c_int]
SetWindowsHookEx.restype = HHOOK

CallNextHookEx = user32.CallNextHookEx
#CallNextHookEx.argtypes = [c_int , c_int, c_int, POINTER(MSLLHOOKSTRUCT)]
CallNextHookEx.restype = c_int

UnhookWindowsHookEx = user32.UnhookWindowsHookEx
UnhookWindowsHookEx.argtypes = [HHOOK]
UnhookWindowsHookEx.restype = BOOL

GetMessage = user32.GetMessageW
GetMessage.argtypes = [LPMSG, c_int, c_int, c_int]
GetMessage.restype = BOOL

TranslateMessage = user32.TranslateMessage
TranslateMessage.argtypes = [LPMSG]
TranslateMessage.restype = BOOL

DispatchMessage = user32.DispatchMessageA
DispatchMessage.argtypes = [LPMSG]

GetDoubleClickTime = user32.GetDoubleClickTime

# Beware, as of 2016-01-30 the official docs have a very incomplete list.
# This one was compiled from experience and may be incomplete.
WM_MOUSEMOVE = 0x200
WM_LBUTTONDOWN = 0x201
WM_LBUTTONUP = 0x202
WM_LBUTTONDBLCLK = 0x203
WM_RBUTTONDOWN = 0x204
WM_RBUTTONUP = 0x205
WM_RBUTTONDBLCLK = 0x206
WM_MBUTTONDOWN = 0x207
WM_MBUTTONUP = 0x208
WM_MBUTTONDBLCLK = 0x209
WM_MOUSEWHEEL = 0x20A
WM_XBUTTONDOWN = 0x20B
WM_XBUTTONUP = 0x20C
WM_XBUTTONDBLCLK = 0x20D
WM_NCXBUTTONDOWN = 0x00AB
WM_NCXBUTTONUP = 0x00AC
WM_NCXBUTTONDBLCLK = 0x00AD
WM_MOUSEHWHEEL = 0x20E
WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP = 0x0202
WM_MOUSEMOVE = 0x0200
WM_MOUSEWHEEL = 0x020A
WM_MOUSEHWHEEL = 0x020E
WM_RBUTTONDOWN = 0x0204
WM_RBUTTONUP = 0x0205

buttons_by_wm_code = {
    WM_LBUTTONDOWN: (DOWN, LEFT),
    WM_LBUTTONUP: (UP, LEFT),
    WM_LBUTTONDBLCLK: (DOUBLE, LEFT),

    WM_RBUTTONDOWN: (DOWN, RIGHT),
    WM_RBUTTONUP: (UP, RIGHT),
    WM_RBUTTONDBLCLK: (DOUBLE, RIGHT),

    WM_MBUTTONDOWN: (DOWN, MIDDLE),
    WM_MBUTTONUP: (UP, MIDDLE),
    WM_MBUTTONDBLCLK: (DOUBLE, MIDDLE),

    WM_XBUTTONDOWN: (DOWN, X),
    WM_XBUTTONUP: (UP, X),
    WM_XBUTTONDBLCLK: (DOUBLE, X),
}

MOUSEEVENTF_ABSOLUTE = 0x8000
MOUSEEVENTF_MOVE = 0x1
MOUSEEVENTF_WHEEL = 0x800
MOUSEEVENTF_HWHEEL = 0x1000
MOUSEEVENTF_LEFTDOWN = 0x2
MOUSEEVENTF_LEFTUP = 0x4
MOUSEEVENTF_RIGHTDOWN = 0x8
MOUSEEVENTF_RIGHTUP = 0x10
MOUSEEVENTF_MIDDLEDOWN = 0x20
MOUSEEVENTF_MIDDLEUP = 0x40
MOUSEEVENTF_XDOWN = 0x0080
MOUSEEVENTF_XUP = 0x0100

simulated_mouse_codes = {
    (WHEEL, HORIZONTAL): MOUSEEVENTF_HWHEEL,
    (WHEEL, VERTICAL): MOUSEEVENTF_WHEEL,

    (DOWN, LEFT): MOUSEEVENTF_LEFTDOWN,
    (UP, LEFT): MOUSEEVENTF_LEFTUP,

    (DOWN, RIGHT): MOUSEEVENTF_RIGHTDOWN,
    (UP, RIGHT): MOUSEEVENTF_RIGHTUP,

    (DOWN, MIDDLE): MOUSEEVENTF_MIDDLEDOWN,
    (UP, MIDDLE): MOUSEEVENTF_MIDDLEUP,

    (DOWN, X): MOUSEEVENTF_XDOWN,
    (UP, X): MOUSEEVENTF_XUP,
}

NULL = c_int(0)

WHEEL_DELTA = 120

init = lambda: None

previous_button_event = None # defined in global scope
def listen(queue):

    def low_level_mouse_handler(nCode, wParam, lParam):
        global previous_button_event

        struct = lParam.contents
        # Can't use struct.time because it's usually zero.
        t = time.time()

        if wParam == WM_MOUSEMOVE:
            event = MoveEvent(struct.x, struct.y, t)
        elif wParam == WM_MOUSEWHEEL:
            event = WheelEvent(struct.data / (WHEEL_DELTA * (2<<15)), t)
        elif wParam in buttons_by_wm_code:
            type, button = buttons_by_wm_code.get(wParam, ('?', '?'))
            if wParam >= WM_XBUTTONDOWN:
                button = {0x10000: X, 0x20000: X2}[struct.data]
            event = ButtonEvent(type, button, t)

            if (event.event_type == DOWN) and previous_button_event is not None:
                # https://msdn.microsoft.com/en-us/library/windows/desktop/gg153548%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
                if event.time - previous_button_event.time <= GetDoubleClickTime() / 1000.0:
                    event = ButtonEvent(DOUBLE, event.button, event.time)

            previous_button_event = event
        else:
            # Unknown event type.
            return CallNextHookEx(NULL, nCode, wParam, lParam)

        queue.put(event)
        return CallNextHookEx(NULL, nCode, wParam, lParam)

    WH_MOUSE_LL = c_int(14)
    mouse_callback = LowLevelMouseProc(low_level_mouse_handler)
    mouse_hook = SetWindowsHookEx(WH_MOUSE_LL, mouse_callback, NULL, NULL)

    # Register to remove the hook when the interpreter exits. Unfortunately a
    # try/finally block doesn't seem to work here.
    atexit.register(UnhookWindowsHookEx, mouse_hook)

    msg = LPMSG()
    while not GetMessage(msg, NULL, NULL, NULL):
        TranslateMessage(msg)
        DispatchMessage(msg)

def _translate_button(button):
    if button.startswith(X):
        return X, 1 if X == button else 2
    else:
        return button, 0

def press(button=LEFT):
    button, data = _translate_button(button)
    code = simulated_mouse_codes[(DOWN, button)]
    user32.mouse_event(code, 0, 0, data, 0)

def release(button=LEFT):
    button, data = _translate_button(button)
    code = simulated_mouse_codes[(UP, button)]
    user32.mouse_event(code, 0, 0, data, 0)

def wheel(delta=1):
    code = simulated_mouse_codes[(WHEEL, VERTICAL)]
    user32.mouse_event(code, 0, 0, int(delta * WHEEL_DELTA), 0)

def move_to(x, y):
    user32.SetCursorPos(int(x), int(y))

def move_relative(x, y):
    user32.mouse_event(MOUSEEVENTF_MOVE, int(x), int(y), 0, 0)

class POINT(Structure):
    _fields_ = [("x", c_long), ("y", c_long)]

def get_position():
    point = POINT()
    user32.GetCursorPos(byref(point))
    return (point.x, point.y)

if __name__ == '__main__':
    def p(e):
        print(e)
    listen(p)
