import time
import sys

import Quartz
import AppKit

import pyautogui


if sys.platform !=  'darwin':
    raise Exception('The pyautogui_osx module should only be loaded on an OS X system.')



""" Taken from events.h
/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h

The *KB dictionaries in pyautogui map a string that can be passed to keyDown(),
keyUp(), or press() into the code used for the OS-specific keyboard function.

They should always be lowercase, and the same keys should be used across all OSes."""
keyboardMapping = dict([(key, None) for key in pyautogui.KEY_NAMES])
keyboardMapping.update({
    'a': 0x00, # kVK_ANSI_A
    's': 0x01, # kVK_ANSI_S
    'd': 0x02, # kVK_ANSI_D
    'f': 0x03, # kVK_ANSI_F
    'h': 0x04, # kVK_ANSI_H
    'g': 0x05, # kVK_ANSI_G
    'z': 0x06, # kVK_ANSI_Z
    'x': 0x07, # kVK_ANSI_X
    'c': 0x08, # kVK_ANSI_C
    'v': 0x09, # kVK_ANSI_V
    'b': 0x0b, # kVK_ANSI_B
    'q': 0x0c, # kVK_ANSI_Q
    'w': 0x0d, # kVK_ANSI_W
    'e': 0x0e, # kVK_ANSI_E
    'r': 0x0f, # kVK_ANSI_R
    'y': 0x10, # kVK_ANSI_Y
    't': 0x11, # kVK_ANSI_T
    '1': 0x12, # kVK_ANSI_1
    '!': 0x12, # kVK_ANSI_1
    '2': 0x13, # kVK_ANSI_2
    '@': 0x13, # kVK_ANSI_2
    '3': 0x14, # kVK_ANSI_3
    '#': 0x14, # kVK_ANSI_3
    '4': 0x15, # kVK_ANSI_4
    '$': 0x15, # kVK_ANSI_4
    '6': 0x16, # kVK_ANSI_6
    '^': 0x16, # kVK_ANSI_6
    '5': 0x17, # kVK_ANSI_5
    '%': 0x17, # kVK_ANSI_5
    '=': 0x18, # kVK_ANSI_Equal
    '+': 0x18, # kVK_ANSI_Equal
    '9': 0x19, # kVK_ANSI_9
    '(': 0x19, # kVK_ANSI_9
    '7': 0x1a, # kVK_ANSI_7
    '&': 0x1a, # kVK_ANSI_7
    '-': 0x1b, # kVK_ANSI_Minus
    '_': 0x1b, # kVK_ANSI_Minus
    '8': 0x1c, # kVK_ANSI_8
    '*': 0x1c, # kVK_ANSI_8
    '0': 0x1d, # kVK_ANSI_0
    ')': 0x1d, # kVK_ANSI_0
    ']': 0x1e, # kVK_ANSI_RightBracket
    '}': 0x1e, # kVK_ANSI_RightBracket
    'o': 0x1f, # kVK_ANSI_O
    'u': 0x20, # kVK_ANSI_U
    '[': 0x21, # kVK_ANSI_LeftBracket
    '{': 0x21, # kVK_ANSI_LeftBracket
    'i': 0x22, # kVK_ANSI_I
    'p': 0x23, # kVK_ANSI_P
    'l': 0x25, # kVK_ANSI_L
    'j': 0x26, # kVK_ANSI_J
    "'": 0x27, # kVK_ANSI_Quote
    '"': 0x27, # kVK_ANSI_Quote
    'k': 0x28, # kVK_ANSI_K
    ';': 0x29, # kVK_ANSI_Semicolon
    ':': 0x29, # kVK_ANSI_Semicolon
    '\\': 0x2a, # kVK_ANSI_Backslash
    '|': 0x2a, # kVK_ANSI_Backslash
    ',': 0x2b, # kVK_ANSI_Comma
    '<': 0x2b, # kVK_ANSI_Comma
    '/': 0x2c, # kVK_ANSI_Slash
    '?': 0x2c, # kVK_ANSI_Slash
    'n': 0x2d, # kVK_ANSI_N
    'm': 0x2e, # kVK_ANSI_M
    '.': 0x2f, # kVK_ANSI_Period
    '>': 0x2f, # kVK_ANSI_Period
    '`': 0x32, # kVK_ANSI_Grave
    '~': 0x32, # kVK_ANSI_Grave
    ' ': 0x31, # kVK_Space
    'space': 0x31,
    '\r': 0x24, # kVK_Return
    '\n': 0x24, # kVK_Return
    'enter': 0x24, # kVK_Return
    'return': 0x24, # kVK_Return
    '\t': 0x30, # kVK_Tab
    'tab': 0x30, # kVK_Tab
    'backspace': 0x33, # kVK_Delete, which is "Backspace" on OS X.
    '\b': 0x33, # kVK_Delete, which is "Backspace" on OS X.
    'esc': 0x35, # kVK_Escape
    'escape': 0x35, # kVK_Escape
    'command': 0x37, # kVK_Command
    'shift': 0x38, # kVK_Shift
    'shiftleft': 0x38, # kVK_Shift
    'capslock': 0x39, # kVK_CapsLock
    'option': 0x3a, # kVK_Option
    'optionleft': 0x3a, # kVK_Option
    'alt': 0x3a, # kVK_Option
    'altleft': 0x3a, # kVK_Option
    'ctrl': 0x3b, # kVK_Control
    'ctrlleft': 0x3b, # kVK_Control
    'shiftright': 0x3c, # kVK_RightShift
    'optionright': 0x3d, # kVK_RightOption
    'ctrlright': 0x3e, # kVK_RightControl
    'fn': 0x3f, # kVK_Function
    'f17': 0x40, # kVK_F17
    'volumeup': 0x48, # kVK_VolumeUp
    'volumedown': 0x49, # kVK_VolumeDown
    'volumemute': 0x4a, # kVK_Mute
    'f18': 0x4f, # kVK_F18
    'f19': 0x50, # kVK_F19
    'f20': 0x5a, # kVK_F20
    'f5': 0x60, # kVK_F5
    'f6': 0x61, # kVK_F6
    'f7': 0x62, # kVK_F7
    'f3': 0x63, # kVK_F3
    'f8': 0x64, # kVK_F8
    'f9': 0x65, # kVK_F9
    'f11': 0x67, # kVK_F11
    'f13': 0x69, # kVK_F13
    'f16': 0x6a, # kVK_F16
    'f14': 0x6b, # kVK_F14
    'f10': 0x6d, # kVK_F10
    'f12': 0x6f, # kVK_F12
    'f15': 0x71, # kVK_F15
    'help': 0x72, # kVK_Help
    'home': 0x73, # kVK_Home
    'pageup': 0x74, # kVK_PageUp
    'pgup': 0x74, # kVK_PageUp
    'del': 0x75, # kVK_ForwardDelete
    'delete': 0x75, # kVK_ForwardDelete
    'f4': 0x76, # kVK_F4
    'end': 0x77, # kVK_End
    'f2': 0x78, # kVK_F2
    'pagedown': 0x79, # kVK_PageDown
    'pgdn': 0x79, # kVK_PageDown
    'f1': 0x7a, # kVK_F1
    'left': 0x7b, # kVK_LeftArrow
    'right': 0x7c, # kVK_RightArrow
    'down': 0x7d, # kVK_DownArrow
    'up': 0x7e, # kVK_UpArrow
    'yen': 0x5d, # kVK_JIS_Yen
    #'underscore' : 0x5e, # kVK_JIS_Underscore (only applies to Japanese keyboards)
    #'comma': 0x5f, # kVK_JIS_KeypadComma (only applies to Japanese keyboards)
    'eisu': 0x66, # kVK_JIS_Eisu
    'kana': 0x68, # kVK_JIS_Kana
})

"""
# TODO - additional key codes to add
  kVK_ANSI_KeypadDecimal        = 0x41,
  kVK_ANSI_KeypadMultiply       = 0x43,
  kVK_ANSI_KeypadPlus           = 0x45,
  kVK_ANSI_KeypadClear          = 0x47,
  kVK_ANSI_KeypadDivide         = 0x4B,
  kVK_ANSI_KeypadEnter          = 0x4C,
  kVK_ANSI_KeypadMinus          = 0x4E,
  kVK_ANSI_KeypadEquals         = 0x51,
  kVK_ANSI_Keypad0              = 0x52,
  kVK_ANSI_Keypad1              = 0x53,
  kVK_ANSI_Keypad2              = 0x54,
  kVK_ANSI_Keypad3              = 0x55,
  kVK_ANSI_Keypad4              = 0x56,
  kVK_ANSI_Keypad5              = 0x57,
  kVK_ANSI_Keypad6              = 0x58,
  kVK_ANSI_Keypad7              = 0x59,
  kVK_ANSI_Keypad8              = 0x5B,
  kVK_ANSI_Keypad9              = 0x5C,
"""

# add mappings for uppercase letters
for c in 'abcdefghijklmnopqrstuvwxyz':
    keyboardMapping[c.upper()] = keyboardMapping[c]

# Taken from ev_keymap.h
# http://www.opensource.apple.com/source/IOHIDFamily/IOHIDFamily-86.1/IOHIDSystem/IOKit/hidsystem/ev_keymap.h
special_key_translate_table = {
    'KEYTYPE_SOUND_UP': 0,
    'KEYTYPE_SOUND_DOWN': 1,
    'KEYTYPE_BRIGHTNESS_UP': 2,
    'KEYTYPE_BRIGHTNESS_DOWN': 3,
    'KEYTYPE_CAPS_LOCK': 4,
    'KEYTYPE_HELP': 5,
    'POWER_KEY': 6,
    'KEYTYPE_MUTE': 7,
    'UP_ARROW_KEY': 8,
    'DOWN_ARROW_KEY': 9,
    'KEYTYPE_NUM_LOCK': 10,
    'KEYTYPE_CONTRAST_UP': 11,
    'KEYTYPE_CONTRAST_DOWN': 12,
    'KEYTYPE_LAUNCH_PANEL': 13,
    'KEYTYPE_EJECT': 14,
    'KEYTYPE_VIDMIRROR': 15,
    'KEYTYPE_PLAY': 16,
    'KEYTYPE_NEXT': 17,
    'KEYTYPE_PREVIOUS': 18,
    'KEYTYPE_FAST': 19,
    'KEYTYPE_REWIND': 20,
    'KEYTYPE_ILLUMINATION_UP': 21,
    'KEYTYPE_ILLUMINATION_DOWN': 22,
    'KEYTYPE_ILLUMINATION_TOGGLE': 23
}

def _keyDown(key):
    if key not in keyboardMapping or keyboardMapping[key] is None:
        return

    if key in special_key_translate_table:
        _specialKeyEvent(key, 'down')
    else:
        _normalKeyEvent(key, 'down')

def _keyUp(key):
    if key not in keyboardMapping or keyboardMapping[key] is None:
        return

    if key in special_key_translate_table:
        _specialKeyEvent(key, 'up')
    else:
        _normalKeyEvent(key, 'up')


def _normalKeyEvent(key, upDown):
    assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'"

    try:
        if pyautogui.isShiftCharacter(key):
            key_code = keyboardMapping[key.lower()]

            event = Quartz.CGEventCreateKeyboardEvent(None,
                        keyboardMapping['shift'], upDown == 'down')
            Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
            # Tiny sleep to let OS X catch up on us pressing shift
            time.sleep(0.01)

        else:
            key_code = keyboardMapping[key]

        event = Quartz.CGEventCreateKeyboardEvent(None, key_code, upDown == 'down')
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
        time.sleep(0.01)

    # TODO - wait, is the shift key's keyup not done?
    # TODO - get rid of this try-except.

    except KeyError:
        raise RuntimeError("Key %s not implemented." % (key))

def _specialKeyEvent(key, upDown):
    """ Helper method for special keys.

    Source: http://stackoverflow.com/questions/11045814/emulate-media-key-press-on-mac
    """
    assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'"

    key_code = special_key_translate_table[key]

    ev = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
            Quartz.NSSystemDefined, # type
            (0,0), # location
            0xa00 if upDown == 'down' else 0xb00, # flags
            0, # timestamp
            0, # window
            0, # ctx
            8, # subtype
            (key_code << 16) | ((0xa if upDown == 'down' else 0xb) << 8), # data1
            -1 # data2
        )

    Quartz.CGEventPost(0, ev.CGEvent())









def _position():
    loc = AppKit.NSEvent.mouseLocation()
    return int(loc.x), int(Quartz.CGDisplayPixelsHigh(0) - loc.y)


def _size():
    return Quartz.CGDisplayPixelsWide(Quartz.CGMainDisplayID()), Quartz.CGDisplayPixelsHigh(Quartz.CGMainDisplayID())



def _scroll(clicks, x=None, y=None):
    _vscroll(clicks, x, y)


"""
According to https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html#//apple_ref/c/func/Quartz.CGEventCreateScrollWheelEvent
"Scrolling movement is generally represented by small signed integer values, typically in a range from -10 to +10. Large values may have unexpected results, depending on the application that processes the event."
The scrolling functions will create multiple events that scroll 10 each, and then scroll the remainder.
"""

def _vscroll(clicks, x=None, y=None):
    _moveTo(x, y)
    clicks = int(clicks)
    for _ in range(abs(clicks) // 10):
        scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
            None, # no source
            Quartz.kCGScrollEventUnitLine, # units
            1, # wheelCount (number of dimensions)
            10 if clicks >= 0 else -10) # vertical movement
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)

    scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
        None, # no source
        Quartz.kCGScrollEventUnitLine, # units
        1, # wheelCount (number of dimensions)
        clicks % 10 if clicks >= 0 else -1 * (-clicks % 10)) # vertical movement
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)


def _hscroll(clicks, x=None, y=None):
    _moveTo(x, y)
    clicks = int(clicks)
    for _ in range(abs(clicks) // 10):
        scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
            None, # no source
            Quartz.kCGScrollEventUnitLine, # units
            2, # wheelCount (number of dimensions)
            0, # vertical movement
            10 if clicks >= 0 else -10) # horizontal movement
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)

    scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
        None, # no source
        Quartz.kCGScrollEventUnitLine, # units
        2, # wheelCount (number of dimensions)
        0, # vertical movement
        (clicks % 10) if clicks >= 0 else (-1 * clicks % 10)) # horizontal movement
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)


def _mouseDown(x, y, button):
    if button == 'left':
        _sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft)
    elif button == 'middle':
        _sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter)
    elif button == 'right':
        _sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight)
    else:
        assert False, "button argument not in ('left', 'middle', 'right')"


def _mouseUp(x, y, button):
    if button == 'left':
        _sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft)
    elif button == 'middle':
        _sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter)
    elif button == 'right':
        _sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight)
    else:
        assert False, "button argument not in ('left', 'middle', 'right')"


def _click(x, y, button):
    if button == 'left':
        _sendMouseEvent(Quartz.kCGEventLeftMouseDown, x, y, Quartz.kCGMouseButtonLeft)
        _sendMouseEvent(Quartz.kCGEventLeftMouseUp, x, y, Quartz.kCGMouseButtonLeft)
    elif button == 'middle':
        _sendMouseEvent(Quartz.kCGEventOtherMouseDown, x, y, Quartz.kCGMouseButtonCenter)
        _sendMouseEvent(Quartz.kCGEventOtherMouseUp, x, y, Quartz.kCGMouseButtonCenter)
    elif button == 'right':
        _sendMouseEvent(Quartz.kCGEventRightMouseDown, x, y, Quartz.kCGMouseButtonRight)
        _sendMouseEvent(Quartz.kCGEventRightMouseUp, x, y, Quartz.kCGMouseButtonRight)
    else:
        assert False, "button argument not in ('left', 'middle', 'right')"


def _sendMouseEvent(ev, x, y, button):
    mouseEvent = Quartz.CGEventCreateMouseEvent(None, ev, (x, y), button)
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, mouseEvent)


def _dragTo(x, y, button):
    if button == 'left':
        _sendMouseEvent(Quartz.kCGEventLeftMouseDragged , x, y, Quartz.kCGMouseButtonLeft)
    elif button == 'middle':
        _sendMouseEvent(Quartz.kCGEventOtherMouseDragged , x, y, Quartz.kCGMouseButtonCenter)
    elif button == 'right':
        _sendMouseEvent(Quartz.kCGEventRightMouseDragged , x, y, Quartz.kCGMouseButtonRight)
    else:
        assert False, "button argument not in ('left', 'middle', 'right')"


def _moveTo(x, y):
    _sendMouseEvent(Quartz.kCGEventMouseMoved, x, y, 0)
    time.sleep(0.01) # needed to allow OS time to catch up.
