import os
import datetime
import threading
import Quartz
from ._mouse_event import ButtonEvent, WheelEvent, MoveEvent, LEFT, RIGHT, MIDDLE, X, X2, UP, DOWN

_button_mapping = {
    LEFT: (Quartz.kCGMouseButtonLeft, Quartz.kCGEventLeftMouseDown, Quartz.kCGEventLeftMouseUp, Quartz.kCGEventLeftMouseDragged),
    RIGHT: (Quartz.kCGMouseButtonRight, Quartz.kCGEventRightMouseDown, Quartz.kCGEventRightMouseUp, Quartz.kCGEventRightMouseDragged),
    MIDDLE: (Quartz.kCGMouseButtonCenter, Quartz.kCGEventOtherMouseDown, Quartz.kCGEventOtherMouseUp, Quartz.kCGEventOtherMouseDragged)
}
_button_state = {
    LEFT: False,
    RIGHT: False,
    MIDDLE: False
}
_last_click = {
    "time": None,
    "button": None,
    "position": None,
    "click_count": 0
}

class MouseEventListener(object):
    def __init__(self, callback, blocking=False):
        self.blocking = blocking
        self.callback = callback
        self.listening = True

    def run(self):
        """ Creates a listener and loops while waiting for an event. Intended to run as
        a background thread. """
        self.tap = Quartz.CGEventTapCreate(
            Quartz.kCGSessionEventTap,
            Quartz.kCGHeadInsertEventTap,
            Quartz.kCGEventTapOptionDefault,
            Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) |
            Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) |
            Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) |
            Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) |
            Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) |
            Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) |
            Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) |
            Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel),
            self.handler,
            None)
        loopsource = Quartz.CFMachPortCreateRunLoopSource(None, self.tap, 0)
        loop = Quartz.CFRunLoopGetCurrent()
        Quartz.CFRunLoopAddSource(loop, loopsource, Quartz.kCFRunLoopDefaultMode)
        Quartz.CGEventTapEnable(self.tap, True)

        while self.listening:
            Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False)

    def handler(self, proxy, e_type, event, refcon):
        # TODO Separate event types by button/wheel/move
        scan_code = Quartz.CGEventGetIntegerValueField(event, Quartz.kCGKeyboardEventKeycode)
        key_name = name_from_scancode(scan_code)
        flags = Quartz.CGEventGetFlags(event)
        event_type = ""
        is_keypad = (flags & Quartz.kCGEventFlagMaskNumericPad)
        if e_type == Quartz.kCGEventKeyDown:
            event_type = "down"
        elif e_type == Quartz.kCGEventKeyUp:
            event_type = "up"

        if self.blocking:
            return None

        self.callback(KeyboardEvent(event_type, scan_code, name=key_name, is_keypad=is_keypad))
        return event

# Exports

def init():
    """ Initializes mouse state """
    pass

def listen(queue):
    """ Appends events to the queue (ButtonEvent, WheelEvent, and MoveEvent). """
    if not os.geteuid() == 0:
        raise OSError("Error 13 - Must be run as administrator")
    listener = MouseEventListener(lambda e: queue.put(e) or is_allowed(e.name, e.event_type == KEY_UP))
    t = threading.Thread(target=listener.run, args=())
    t.daemon = True
    t.start()

def press(button=LEFT):
    """ Sends a down event for the specified button, using the provided constants """
    location = get_position()
    button_code, button_down, _, _ = _button_mapping[button]
    e = Quartz.CGEventCreateMouseEvent(
        None,
        button_down,
        location,
        button_code)

    # Check if this is a double-click (same location within the last 300ms)
    if _last_click["time"] is not None and datetime.datetime.now() - _last_click["time"] < datetime.timedelta(seconds=0.3) and _last_click["button"] == button and _last_click["position"] == location:
        # Repeated Click
        _last_click["click_count"] = min(3, _last_click["click_count"]+1)
    else:
        # Not a double-click - Reset last click
        _last_click["click_count"] = 1
    Quartz.CGEventSetIntegerValueField(
        e,
        Quartz.kCGMouseEventClickState,
        _last_click["click_count"])
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
    _button_state[button] = True
    _last_click["time"] = datetime.datetime.now()
    _last_click["button"] = button
    _last_click["position"] = location

def release(button=LEFT):
    """ Sends an up event for the specified button, using the provided constants """
    location = get_position()
    button_code, _, button_up, _ = _button_mapping[button]
    e = Quartz.CGEventCreateMouseEvent(
        None,
        button_up,
        location,
        button_code)

    if _last_click["time"] is not None and _last_click["time"] > datetime.datetime.now() - datetime.timedelta(microseconds=300000) and _last_click["button"] == button and _last_click["position"] == location:
        # Repeated Click
        Quartz.CGEventSetIntegerValueField(
            e,
            Quartz.kCGMouseEventClickState,
            _last_click["click_count"])
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
    _button_state[button] = False

def wheel(delta=1):
    """ Sends a wheel event for the provided number of clicks. May be negative to reverse
    direction. """
    location = get_position()
    e = Quartz.CGEventCreateMouseEvent(
        None,
        Quartz.kCGEventScrollWheel,
        location,
        Quartz.kCGMouseButtonLeft)
    e2 = Quartz.CGEventCreateScrollWheelEvent(
        None,
        Quartz.kCGScrollEventUnitLine,
        1,
        delta)
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, e2)

def move_to(x, y):
    """ Sets the mouse's location to the specified coordinates. """
    for b in _button_state:
        if _button_state[b]:
            e = Quartz.CGEventCreateMouseEvent(
                None,
                _button_mapping[b][3], # Drag Event
                (x, y),
                _button_mapping[b][0])
            break
    else:
        e = Quartz.CGEventCreateMouseEvent(
            None,
            Quartz.kCGEventMouseMoved,
            (x, y),
            Quartz.kCGMouseButtonLeft)
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, e)

def get_position():
    """ Returns the mouse's location as a tuple of (x, y). """
    e = Quartz.CGEventCreate(None)
    point = Quartz.CGEventGetLocation(e)
    return (point.x, point.y)