import threading
import functools
import platform
from ctypes import *
from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *


class KeyboardHook(QThread):
    """
    This class is responsible for creating a global keyboard hook and
    forwarding key events to QT when the game process has focus.
    """

    def __init__(self, window):
        QThread.__init__(self)
        self.tid = None
        self.pid = None
        self.running = True
        self.window = window
        self.window.installEventFilter(self)
        QCoreApplication.instance().aboutToQuit.connect(self.stop)

    def stop(self):
        self.window.removeEventFilter(self)
        self.running = False
        if platform.system() == 'Windows':
            self.stop_windows()
        elif platform.system() == 'Darwin':
            self.stop_mac()

    def eventFilter(self, object, event):
        if event.type() == QEvent.ActivationChange:
            self.window.setFocus(Qt.OtherFocusReason)
            QApplication.setActiveWindow(self.window)
        return False

    def setPid(self, pid):
        self.pid = pid

    def run(self):
        self.tid = threading.get_ident()
        if platform.system() == 'Windows':
            self.run_windows()
        elif platform.system() == 'Darwin':
            self.run_mac()

    def stop_windows(self):
        windll.user32.PostThreadMessageA(self.tid, 18, 0, 0)

    def run_windows(self):
        from ctypes.wintypes import DWORD, WPARAM, LPARAM, MSG

        class KBDLLHOOKSTRUCT(Structure):
            _fields_ = [
                ("vk_code", DWORD),
                ("scan_code", DWORD),
                ("flags", DWORD),
                ("time", c_int),
                ("dwExtraInfo", POINTER(DWORD))
            ]

        def callback(nCode, wParam, lParam):
            pid = c_ulong()
            windll.user32.GetWindowThreadProcessId(windll.user32.GetForegroundWindow(), byref(pid))
            if pid.value == self.pid:
                windll.user32.SendMessageA(self.window.winId(), wParam, lParam.contents.vk_code, 0)
            return windll.user32.CallNextHookEx(None, nCode, wParam, lParam)
 
        function = CFUNCTYPE(c_int, WPARAM, LPARAM, POINTER(KBDLLHOOKSTRUCT))(callback)
        hook = windll.user32.SetWindowsHookExW(13, function, windll.kernel32.GetModuleHandleW(None), 0)

        msg = POINTER(MSG)()
        while self.running:
            try:
                windll.user32.GetMessageW(msg, 0, 0, 0)
                windll.user32.TranslateMessage(msg)
                windll.user32.DispatchMessageA(msg)
            except: pass

        windll.user32.UnhookWindowsHookEx(hook)

    def stop_mac(self):
        from Quartz import CFRunLoopStop
        if hasattr(self, 'runLoop'):
            CFRunLoopStop(self.runLoop)

    def run_mac(self):
        from Quartz import (
            CGEventTapCreate,
            CFMachPortCreateRunLoopSource,
            CFRunLoopAddSource,
            CFRunLoopGetCurrent,
            CGEventTapEnable,
            CGEventMaskBit,
            CFRunLoopRun,
            CGEventGetIntegerValueField,
            CGEventPostToPid,
            kCGEventKeyDown,
            kCGEventKeyUp,
            kCGEventFlagsChanged,
            kCGSessionEventTap,
            kCGHeadInsertEventTap,
            kCGEventTargetUnixProcessID,
            kCFAllocatorDefault,
            kCFRunLoopDefaultMode,
        )
        pid = QCoreApplication.applicationPid()

        def callback(proxy, type, event, refcon):
            if self.pid == CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID):
                CGEventPostToPid(pid, event)
            return event

        tap = CGEventTapCreate(
            kCGSessionEventTap,
            kCGHeadInsertEventTap,
            0,
            CGEventMaskBit(kCGEventKeyDown) |
            CGEventMaskBit(kCGEventKeyUp) |
            CGEventMaskBit(kCGEventFlagsChanged),
            callback,
            None
        )
        if tap:
            source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
            self.runLoop = CFRunLoopGetCurrent()
            CFRunLoopAddSource(self.runLoop, source, kCFRunLoopDefaultMode)
            CGEventTapEnable(tap, True)
            CFRunLoopRun()


class Bindings(QObject):
    triggered = Signal(str)

    def __init__(self, window, bindings, options):
        QObject.__init__(self)
        self.labels = {name : label for name, label, _ in options}
        self.shortcuts = {}
        self.defaults = {}
        for name, _, default in options:
            if name in bindings:
                sequence = QKeySequence(bindings[name])
            else:
                sequence = QKeySequence(default)
            shortcut = QShortcut(sequence, window)
            shortcut.setContext(Qt.WindowShortcut)
            shortcut.setAutoRepeat(True)
            shortcut.activated.connect(functools.partial(self.activated, name))
            shortcut.activatedAmbiguously.connect(functools.partial(self.activated, name))
            self.shortcuts[name] = shortcut
            self.defaults[name] = default
        self.hook = KeyboardHook(window)
        self.hook.start()

    def activated(self, name):
        sequence = self.shortcuts[name].key()
        for name, shortcut in self.shortcuts.items():
            if shortcut.key() == sequence:
                self.triggered.emit(name)

    def getBindings(self):
        return {name : shortcut.key().toString() for name, shortcut in self.shortcuts.items()}

    def getOptions(self):
        return [(name, label, self.shortcuts[name].key().toString()) for name, label, default in self.options]

    def setBinding(self, name, sequence):
        self.shortcuts[name].setKey(QKeySequence(sequence))

    def getLabel(self, name):
        return self.labels[name]

    def setGamePid(self, pid):
        self.hook.setPid(pid)