#!/usr/bin/env python
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>

from enum import IntEnum
from functools import lru_cache
from typing import NamedTuple

from . import fast_data_types as defines
from .fast_data_types import KeyEvent as WindowSystemKeyEvent
from .key_names import character_key_name_aliases, functional_key_name_aliases
from .types import ParsedShortcut

# number name mappings {{{
# start csi mapping (auto generated by gen-key-constants.py do not edit)
functional_key_number_to_name_map = {
    57344: 'ESCAPE',
    57345: 'ENTER',
    57346: 'TAB',
    57347: 'BACKSPACE',
    57348: 'INSERT',
    57349: 'DELETE',
    57350: 'LEFT',
    57351: 'RIGHT',
    57352: 'UP',
    57353: 'DOWN',
    57354: 'PAGE_UP',
    57355: 'PAGE_DOWN',
    57356: 'HOME',
    57357: 'END',
    57358: 'CAPS_LOCK',
    57359: 'SCROLL_LOCK',
    57360: 'NUM_LOCK',
    57361: 'PRINT_SCREEN',
    57362: 'PAUSE',
    57363: 'MENU',
    57364: 'F1',
    57365: 'F2',
    57366: 'F3',
    57367: 'F4',
    57368: 'F5',
    57369: 'F6',
    57370: 'F7',
    57371: 'F8',
    57372: 'F9',
    57373: 'F10',
    57374: 'F11',
    57375: 'F12',
    57376: 'F13',
    57377: 'F14',
    57378: 'F15',
    57379: 'F16',
    57380: 'F17',
    57381: 'F18',
    57382: 'F19',
    57383: 'F20',
    57384: 'F21',
    57385: 'F22',
    57386: 'F23',
    57387: 'F24',
    57388: 'F25',
    57389: 'F26',
    57390: 'F27',
    57391: 'F28',
    57392: 'F29',
    57393: 'F30',
    57394: 'F31',
    57395: 'F32',
    57396: 'F33',
    57397: 'F34',
    57398: 'F35',
    57399: 'KP_0',
    57400: 'KP_1',
    57401: 'KP_2',
    57402: 'KP_3',
    57403: 'KP_4',
    57404: 'KP_5',
    57405: 'KP_6',
    57406: 'KP_7',
    57407: 'KP_8',
    57408: 'KP_9',
    57409: 'KP_DECIMAL',
    57410: 'KP_DIVIDE',
    57411: 'KP_MULTIPLY',
    57412: 'KP_SUBTRACT',
    57413: 'KP_ADD',
    57414: 'KP_ENTER',
    57415: 'KP_EQUAL',
    57416: 'KP_SEPARATOR',
    57417: 'KP_LEFT',
    57418: 'KP_RIGHT',
    57419: 'KP_UP',
    57420: 'KP_DOWN',
    57421: 'KP_PAGE_UP',
    57422: 'KP_PAGE_DOWN',
    57423: 'KP_HOME',
    57424: 'KP_END',
    57425: 'KP_INSERT',
    57426: 'KP_DELETE',
    57427: 'KP_BEGIN',
    57428: 'MEDIA_PLAY',
    57429: 'MEDIA_PAUSE',
    57430: 'MEDIA_PLAY_PAUSE',
    57431: 'MEDIA_REVERSE',
    57432: 'MEDIA_STOP',
    57433: 'MEDIA_FAST_FORWARD',
    57434: 'MEDIA_REWIND',
    57435: 'MEDIA_TRACK_NEXT',
    57436: 'MEDIA_TRACK_PREVIOUS',
    57437: 'MEDIA_RECORD',
    57438: 'LOWER_VOLUME',
    57439: 'RAISE_VOLUME',
    57440: 'MUTE_VOLUME',
    57441: 'LEFT_SHIFT',
    57442: 'LEFT_CONTROL',
    57443: 'LEFT_ALT',
    57444: 'LEFT_SUPER',
    57445: 'LEFT_HYPER',
    57446: 'LEFT_META',
    57447: 'RIGHT_SHIFT',
    57448: 'RIGHT_CONTROL',
    57449: 'RIGHT_ALT',
    57450: 'RIGHT_SUPER',
    57451: 'RIGHT_HYPER',
    57452: 'RIGHT_META',
    57453: 'ISO_LEVEL3_SHIFT',
    57454: 'ISO_LEVEL5_SHIFT'}
csi_number_to_functional_number_map = {
    2: 57348,
    3: 57349,
    5: 57354,
    6: 57355,
    7: 57356,
    8: 57357,
    9: 57346,
    11: 57364,
    12: 57365,
    13: 57345,
    14: 57367,
    15: 57368,
    17: 57369,
    18: 57370,
    19: 57371,
    20: 57372,
    21: 57373,
    23: 57374,
    24: 57375,
    27: 57344,
    127: 57347}
letter_trailer_to_csi_number_map = {'A': 57352, 'B': 57353, 'C': 57351, 'D': 57350, 'E': 57427, 'F': 8, 'H': 7, 'P': 11, 'Q': 12, 'S': 14}
tilde_trailers = {57348, 57349, 57354, 57355, 57366, 57368, 57369, 57370, 57371, 57372, 57373, 57374, 57375}
# end csi mapping
# }}}


@lru_cache(2)
def get_name_to_functional_number_map() -> dict[str, int]:
    return {v: k for k, v in functional_key_number_to_name_map.items()}


@lru_cache(2)
def get_functional_to_csi_number_map() -> dict[int, int]:
    return {v: k for k, v in csi_number_to_functional_number_map.items()}


@lru_cache(2)
def get_csi_number_to_letter_trailer_map() -> dict[int, str]:
    return {v: k for k, v in letter_trailer_to_csi_number_map.items()}


PRESS: int = 1
REPEAT: int = 2
RELEASE: int = 4


class EventType(IntEnum):
    PRESS = PRESS
    REPEAT = REPEAT
    RELEASE = RELEASE


@lru_cache(maxsize=128)
def parse_shortcut(spec: str) -> ParsedShortcut:
    if spec.endswith('+'):
        spec = f'{spec[:-1]}plus'
    parts = spec.split('+')
    key_name = parts[-1]
    key_name = functional_key_name_aliases.get(key_name.upper(), key_name)
    is_functional_key = key_name.upper() in get_name_to_functional_number_map()
    if is_functional_key:
        key_name = key_name.upper()
    else:
        key_name = character_key_name_aliases.get(key_name.upper(), key_name)
    mod_val = 0
    if len(parts) > 1:
        mods = tuple(config_mod_map.get(x.upper(), META << 8) for x in parts[:-1])
        for x in mods:
            mod_val |= x
    return ParsedShortcut(mod_val, key_name)


class KeyEvent(NamedTuple):
    type: EventType = EventType.PRESS
    mods: int = 0
    key: str = ''
    text: str = ''
    shifted_key: str = ''
    alternate_key: str = ''
    shift: bool = False
    alt: bool = False
    ctrl: bool = False
    super: bool = False
    hyper: bool = False
    meta: bool = False
    caps_lock: bool = False
    num_lock: bool = False

    def matches(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | EventType.REPEAT) -> bool:
        mods = self.mods_without_locks
        if not self.type & types:
            return False
        if isinstance(spec, str):
            spec = parse_shortcut(spec)
        if (mods, self.key) == spec:
            return True
        is_shifted = bool(self.shifted_key and self.shift)
        if is_shifted and (mods & ~SHIFT, self.shifted_key) == spec:
            return True
        if self.alternate_key and self.key and len(self.key) == 1 and 127 < ord(self.key) < 0xE000 and (mods, self.alternate_key) == spec:
            return True
        return False

    def matches_without_mods(self, spec: str | ParsedShortcut, types: int = EventType.PRESS | EventType.REPEAT) -> bool:
        if not self.type & types:
            return False
        if isinstance(spec, str):
            spec = parse_shortcut(spec)
        return self.key == spec[1]

    def matches_text(self, text: str, case_sensitive: bool = False) -> bool:
        if case_sensitive:
            return self.text == text
        return self.text.lower() == text.lower()

    @property
    def is_release(self) -> bool:
        return self.type is EventType.RELEASE

    @property
    def mods_without_locks(self) -> int:
        return self.mods & ~(NUM_LOCK | CAPS_LOCK)

    @property
    def has_mods(self) -> bool:
        return bool(self.mods_without_locks)

    def as_window_system_event(self) -> WindowSystemKeyEvent:
        action = defines.GLFW_PRESS
        if self.type is EventType.REPEAT:
            action = defines.GLFW_REPEAT
        elif self.type is EventType.RELEASE:
            action = defines.GLFW_RELEASE
        mods = 0
        if self.mods:
            if self.shift:
                mods |= defines.GLFW_MOD_SHIFT
            if self.alt:
                mods |= defines.GLFW_MOD_ALT
            if self.ctrl:
                mods |= defines.GLFW_MOD_CONTROL
            if self.super:
                mods |= defines.GLFW_MOD_SUPER
            if self.hyper:
                mods |= defines.GLFW_MOD_HYPER
            if self.meta:
                mods |= defines.GLFW_MOD_META
            if self.caps_lock:
                mods |= defines.GLFW_MOD_CAPS_LOCK
            if self.num_lock:
                mods |= defines.GLFW_MOD_NUM_LOCK

        fnm = get_name_to_functional_number_map()

        def as_num(key: str) -> int:
            return (fnm.get(key) or ord(key)) if key else 0

        return WindowSystemKeyEvent(
            key=as_num(self.key), shifted_key=as_num(self.shifted_key),
            alternate_key=as_num(self.alternate_key), mods=mods,
            action=action, text=self.text)


SHIFT, ALT, CTRL, SUPER, HYPER, META, CAPS_LOCK, NUM_LOCK = 1, 2, 4, 8, 16, 32, 64, 128
enter_key = KeyEvent(key='ENTER')
backspace_key = KeyEvent(key='BACKSPACE')
config_mod_map = {
    'SHIFT': SHIFT,
    '⇧': SHIFT,
    'ALT': ALT,
    'OPTION': ALT,
    'OPT': ALT,
    '⌥': ALT,
    'SUPER': SUPER,
    'COMMAND': SUPER,
    'CMD': SUPER,
    '⌘': SUPER,
    'CONTROL': CTRL,
    'CTRL': CTRL,
    '⌃': CTRL,
    'HYPER': HYPER,
    'META': META,
    'NUM_LOCK': NUM_LOCK,
    'CAPS_LOCK': CAPS_LOCK,
}


def decode_key_event(csi: str, csi_type: str) -> KeyEvent:
    parts = csi.split(';')

    def get_sub_sections(x: str, missing: int = 0) -> tuple[int, ...]:
        return tuple(int(y) if y else missing for y in x.split(':'))

    first_section = get_sub_sections(parts[0])
    second_section = get_sub_sections(parts[1], 1) if len(parts) > 1 else ()
    third_section = get_sub_sections(parts[2]) if len(parts) > 2 else ()
    mods = (second_section[0] - 1) if second_section else 0
    action = second_section[1] if len(second_section) > 1 else 1
    keynum = first_section[0]
    if csi_type in 'ABCDEHFPQRS':
        keynum = letter_trailer_to_csi_number_map[csi_type]

    def key_name(num: int) -> str:
        if not num:
            return ''
        if num != 13:
            num = csi_number_to_functional_number_map.get(num, num)
            ans = functional_key_number_to_name_map.get(num)
        else:
            ans = 'ENTER' if csi_type == 'u' else 'F3'
        if ans is None:
            ans = chr(num)
        return ans

    return KeyEvent(
        mods=mods, shift=bool(mods & SHIFT), alt=bool(mods & ALT),
        ctrl=bool(mods & CTRL), super=bool(mods & SUPER),
        hyper=bool(mods & HYPER), meta=bool(mods & META),
        caps_lock=bool(mods & CAPS_LOCK), num_lock=bool(mods & NUM_LOCK),
        key=key_name(keynum),
        shifted_key=key_name(first_section[1] if len(first_section) > 1 else 0),
        alternate_key=key_name(first_section[2] if len(first_section) > 2 else 0),
        type={1: EventType.PRESS, 2: EventType.REPEAT, 3: EventType.RELEASE}[action],
        text=''.join(map(chr, third_section))
    )


def csi_number_for_name(key_name: str) -> int:
    if not key_name:
        return 0
    if key_name in ('F3', 'ENTER'):
        return 13
    fn = get_name_to_functional_number_map().get(key_name)
    if fn is None:
        return ord(key_name)
    return get_functional_to_csi_number_map().get(fn, fn)


def encode_key_event(key_event: KeyEvent) -> str:
    key = csi_number_for_name(key_event.key)
    shifted_key = csi_number_for_name(key_event.shifted_key)
    alternate_key = csi_number_for_name(key_event.alternate_key)
    lt = get_csi_number_to_letter_trailer_map()
    if key_event.key == 'ENTER':
        trailer = 'u'
    else:
        trailer = lt.get(key, 'u')
    if trailer != 'u':
        key = 1
    mods = key_event.mods
    text = key_event.text
    ans = '\033['
    if key != 1 or mods or shifted_key or alternate_key or text:
        ans += f'{key}'
    if shifted_key or alternate_key:
        ans += ':' + (f'{shifted_key}' if shifted_key else '')
        if alternate_key:
            ans += f':{alternate_key}'
    action = 1
    if key_event.type is EventType.REPEAT:
        action = 2
    elif key_event.type is EventType.RELEASE:
        action = 3
    if mods or action > 1 or text:
        m = 0
        if key_event.shift:
            m |= 1
        if key_event.alt:
            m |= 2
        if key_event.ctrl:
            m |= 4
        if key_event.super:
            m |= 8
        if key_event.hyper:
            m |= 16
        if key_event.meta:
            m |= 32
        if key_event.caps_lock:
            m |= 64
        if key_event.num_lock:
            m |= 128
        if action > 1 or m:
            ans += f';{m+1}'
            if action > 1:
                ans += f':{action}'
        elif text:
            ans += ';'
    if text:
        ans += ';' + ':'.join(map(str, map(ord, text)))
    fn = get_name_to_functional_number_map().get(key_event.key)
    if fn is not None and fn in tilde_trailers:
        trailer = '~'
    return ans + trailer


def decode_key_event_as_window_system_key(text: str) -> WindowSystemKeyEvent | None:
    csi, trailer = text[2:-1], text[-1]
    try:
        k = decode_key_event(csi, trailer)
    except Exception:
        return None
    return k.as_window_system_event()
