import glob
import logging
import os
import re
import subprocess
from typing import Mapping, Sequence, TypeVar, cast, Collection

import evdev
from evdev import ecodes, ff

from hhd.controller import Axis, Button, Consumer, Event, Producer, can_read
from hhd.controller.base import Event
from hhd.controller.lib.common import hexify, matches_patterns
from hhd.controller.lib.hide import hide_gamepad, unhide_gamepad, unhide_all
from hhd.controller.const import AbsAxis, GamepadButton, KeyboardButton

logger = logging.getLogger(__name__)


def B(b: str):
    return cast(int, getattr(evdev.ecodes, b))


A = TypeVar("A")


def to_map(b: dict[A, Sequence[int]]) -> dict[int, A]:
    out = {}
    for btn, seq in b.items():
        for s in seq:
            out[s] = btn
    return out


CapabilityMatch = Mapping[int, Collection[int]]

XBOX_BUTTON_MAP: dict[int, GamepadButton] = to_map(
    {
        # Gamepad
        "a": [B("BTN_A")],
        "b": [B("BTN_B")],
        "x": [B("BTN_X")],
        "y": [B("BTN_Y")],
        # Sticks
        "ls": [B("BTN_THUMBL")],
        "rs": [B("BTN_THUMBR")],
        # Bumpers
        "lb": [B("BTN_TL")],
        "rb": [B("BTN_TR")],
        # Select
        "start": [B("BTN_START")],
        "select": [B("BTN_SELECT")],
        # Misc
        "mode": [B("BTN_MODE")],
    }
)

XBOX_AXIS_MAP: dict[int, AbsAxis] = to_map(
    {
        # Sticks
        # Values should range from -1 to 1
        "ls_x": [B("ABS_X")],
        "ls_y": [B("ABS_Y")],
        "rs_x": [B("ABS_RX")],
        "rs_y": [B("ABS_RY")],
        # Triggers
        # Values should range from -1 to 1
        "rt": [B("ABS_Z")],
        "lt": [B("ABS_RZ")],
        # Hat, implemented as axis. Either -1, 0, or 1
        "hat_x": [B("ABS_HAT0X")],
        "hat_y": [B("ABS_HAT0Y")],
    }
)


def list_joysticks(input_device_dir="/dev/input"):
    return glob.glob(f"{input_device_dir}/js*")


def get_path(ev: str):
    path = None
    for line in subprocess.run(
        ["udevadm", "info", "-n", ev], capture_output=True
    ).stdout.splitlines():
        if line.startswith(b"P: "):
            path = line[3 : -len(ev[ev.rindex("/") :])]
            break
    return path


def find_joystick(ev: str):
    path = get_path(ev)
    for other in list_joysticks():
        if path == get_path(other):
            return other


class GenericGamepadEvdev(Producer, Consumer):
    def __init__(
        self,
        vid: Sequence[int],
        pid: Sequence[int],
        name: Sequence[str | re.Pattern] = "",
        capabilities: CapabilityMatch = {},
        btn_map: Mapping[int, Button] = XBOX_BUTTON_MAP,
        axis_map: Mapping[int, Axis] = XBOX_AXIS_MAP,
        aspect_ratio: float | None = None,
        required: bool = True,
        hide: bool = False,
        grab: bool = True,
    ) -> None:
        self.vid = vid
        self.pid = pid
        self.name = name
        self.capabilities = capabilities

        self.btn_map = btn_map
        self.axis_map = axis_map
        self.aspect_ratio = aspect_ratio

        self.dev: evdev.InputDevice | None = None
        self.fd = 0
        self.required = required
        self.hide = hide
        self.grab = grab
        self.hidden = False

    def open(self) -> Sequence[int]:
        for d in evdev.list_devices():
            dev = evdev.InputDevice(d)
            if not matches_patterns(dev.info.vendor, self.vid):
                continue
            if not matches_patterns(dev.info.product, self.pid):
                continue
            if not matches_patterns(dev.name, self.name):
                continue
            if self.capabilities:
                matches = True
                dev_cap = cast(dict[int, Sequence[int]], dev.capabilities())
                for cap_id, caps in self.capabilities.items():
                    if cap_id not in dev_cap:
                        matches = False
                        break
                    for cap in caps:
                        if cap not in dev_cap[cap_id]:
                            matches = False
                        break
                if not matches:
                    continue

            if self.hide:
                # Check we are root
                if not os.getuid():
                    self.hidden = hide_gamepad(
                        dev.path, dev.info.vendor, dev.info.product
                    )
                    if not self.hidden:
                        logger.warning(f"Could not hide device:\n{dev}")
                else:
                    logger.warning(
                        f"Not running as root, device '{dev.name}' could not be hid."
                    )

            self.dev = dev
            if self.grab:
                self.dev.grab()
            self.ranges = {
                a: (i.min, i.max) for a, i in self.dev.capabilities().get(B("EV_ABS"), [])  # type: ignore
            }
            self.fd = dev.fd
            self.started = True
            self.effect_id = -1
            return [self.fd]

        err = f"Device with the following not found:\n"
        if self.vid:
            err += f"Vendor ID: {hexify(self.vid)}\n"
        if self.pid:
            err += f"Product ID: {hexify(self.pid)}\n"
        if self.name:
            err += f"Name: {self.name}\n"
        if self.capabilities:
            err += f"Capabilities: {self.capabilities}\n"
        logger.error(err)
        if self.required:
            raise RuntimeError()
        return []

    def close(self, exit: bool) -> bool:
        if self.dev:
            if self.hidden:
                unhide_gamepad(self.dev.path)
            self.dev.close()
            self.dev = None
            self.fd = 0
        return True

    def consume(self, events: Sequence[Event]):
        if not self.dev:
            return

        for ev in events:
            match ev["type"]:
                case "rumble":
                    # Erase old effect
                    if self.effect_id != -1:
                        self.dev.erase_effect(self.effect_id)
                        self.effect_id = -1

                    # Install new effect
                    if ev["strong_magnitude"] > 0 or ev["weak_magnitude"] > 0:
                        rumble = ff.Rumble(
                            strong_magnitude=min(
                                int(ev["strong_magnitude"] * 0xFFFF), 0xFFFF
                            ),
                            weak_magnitude=min(
                                int(ev["weak_magnitude"] * 0xFFFF), 0xFFFF
                            ),
                        )
                        duration_ms = 10000

                        effect = ff.Effect(
                            getattr(ecodes, "FF_RUMBLE"),
                            -1,
                            0,
                            ff.Trigger(0, 0),
                            ff.Replay(duration_ms, 0),
                            ff.EffectType(ff_rumble_effect=rumble),
                        )
                        self.effect_id = self.dev.upload_effect(effect)
                        self.dev.write(getattr(ecodes, "EV_FF"), self.effect_id, 1)

    def produce(self, fds: Sequence[int]) -> Sequence[Event]:
        if not self.dev or not self.fd in fds:
            return []

        out: list[Event] = []
        if self.started and self.aspect_ratio is not None:
            self.started = False
            out.append(
                {
                    "type": "configuration",
                    "code": "touchpad_aspect_ratio",
                    "value": self.aspect_ratio,
                }
            )

        while can_read(self.fd):
            for e in self.dev.read():
                if e.type == B("EV_KEY"):
                    if e.code in self.btn_map:
                        # Only 1 is valid for press (look at sysrq)
                        if e.value == 0 or e.value == 1:
                            out.append(
                                {
                                    "type": "button",
                                    "code": self.btn_map[e.code],
                                    "value": bool(e.value),
                                }
                            )
                elif e.type == B("EV_ABS"):
                    if e.code in self.axis_map:
                        # Normalize
                        val = e.value / abs(
                            self.ranges[e.code][1 if e.value >= 0 else 0]
                        )

                        out.append(
                            {
                                "type": "axis",
                                "code": self.axis_map[e.code],
                                "value": val,
                            }
                        )
        return out


_kbd_raw: dict[KeyboardButton, Sequence[int]] = {
    "key_esc": [B("KEY_ESC")],  # 1
    "key_enter": [B("KEY_ENTER")],  # 28
    "key_leftctrl": [B("KEY_LEFTCTRL")],  # 29
    "key_leftshift": [B("KEY_LEFTSHIFT")],  # 42
    "key_leftalt": [B("KEY_LEFTALT")],  # 56
    "key_rightctrl": [B("KEY_RIGHTCTRL")],  # 97
    "key_rightshift": [B("KEY_RIGHTSHIFT")],  # 54
    "key_rightalt": [B("KEY_RIGHTALT")],  # 100
    "key_leftmeta": [B("KEY_LEFTMETA")],  # 125
    "key_rightmeta": [B("KEY_RIGHTMETA")],  # 126
    "key_capslock": [B("KEY_CAPSLOCK")],  # 58
    "key_numlock": [B("KEY_NUMLOCK")],  # 69
    "key_scrolllock": [B("KEY_SCROLLLOCK")],  # 70
    "key_sysrq": [B("KEY_SYSRQ")],  # 99
    "key_minus": [B("KEY_MINUS")],  # 12
    "key_equal": [B("KEY_EQUAL")],  # 13
    "key_backspace": [B("KEY_BACKSPACE")],  # 14
    "key_tab": [B("KEY_TAB")],  # 15
    "key_leftbrace": [B("KEY_LEFTBRACE")],  # 26
    "key_rightbrace": [B("KEY_RIGHTBRACE")],  # 27
    "key_space": [B("KEY_SPACE")],  # 57
    "key_up": [B("KEY_UP")],  # 103
    "key_left": [B("KEY_LEFT")],  # 105
    "key_right": [B("KEY_RIGHT")],  # 106
    "key_down": [B("KEY_DOWN")],  # 108
    "key_home": [B("KEY_HOME")],  # 102
    "key_end": [B("KEY_END")],  # 107
    "key_pageup": [B("KEY_PAGEUP")],  # 104
    "key_pagedown": [B("KEY_PAGEDOWN")],  # 109
    "key_insert": [B("KEY_INSERT")],  # 110
    "key_delete": [B("KEY_DELETE")],  # 111
    "key_semicolon": [B("KEY_SEMICOLON")],  # 39
    "key_apostrophe": [B("KEY_APOSTROPHE")],  # 40
    "key_grave": [B("KEY_GRAVE")],  # 41
    "key_backslash": [B("KEY_BACKSLASH")],  # 43
    "key_comma": [B("KEY_COMMA")],  # 51
    "key_dot": [B("KEY_DOT")],  # 52
    "key_slash": [B("KEY_SLASH")],  # 53
    "key_102nd": [B("KEY_102ND")],  # 86
    "key_ro": [B("KEY_RO")],  # 89
    "key_power": [B("KEY_POWER")],  # 116
    "key_compose": [B("KEY_COMPOSE")],  # 127
    "key_stop": [B("KEY_STOP")],  # 128
    "key_again": [B("KEY_AGAIN")],  # 129
    "key_props": [B("KEY_PROPS")],  # 130
    "key_undo": [B("KEY_UNDO")],  # 131
    "key_front": [B("KEY_FRONT")],  # 132
    "key_copy": [B("KEY_COPY")],  # 133
    "key_open": [B("KEY_OPEN")],  # 134
    "key_paste": [B("KEY_PASTE")],  # 135
    "key_cut": [B("KEY_CUT")],  # 137
    "key_find": [B("KEY_FIND")],  # 136
    "key_help": [B("KEY_HELP")],  # 138
    "key_calc": [B("KEY_CALC")],  # 140
    "key_sleep": [B("KEY_SLEEP")],  # 142
    "key_www": [B("KEY_WWW")],  # 150
    "key_screenlock": [B("KEY_SCREENLOCK")],  # 152
    "key_back": [B("KEY_BACK")],  # 158
    "key_refresh": [B("KEY_REFRESH")],  # 173
    "key_edit": [B("KEY_EDIT")],  # 176
    "key_scrollup": [B("KEY_SCROLLUP")],  # 177
    "key_scrolldown": [B("KEY_SCROLLDOWN")],  # 178
    "key_1": [B("KEY_1")],  # 2
    "key_2": [B("KEY_2")],  # 3
    "key_3": [B("KEY_3")],  # 4
    "key_4": [B("KEY_4")],  # 5
    "key_5": [B("KEY_5")],  # 6
    "key_6": [B("KEY_6")],  # 7
    "key_7": [B("KEY_7")],  # 8
    "key_8": [B("KEY_8")],  # 9
    "key_9": [B("KEY_9")],  # 10
    "key_0": [B("KEY_0")],  # 11
    "key_a": [B("KEY_A")],  # 30
    "key_b": [B("KEY_B")],  # 48
    "key_c": [B("KEY_C")],  # 46
    "key_d": [B("KEY_D")],  # 32
    "key_e": [B("KEY_E")],  # 18
    "key_f": [B("KEY_F")],  # 33
    "key_g": [B("KEY_G")],  # 34
    "key_h": [B("KEY_H")],  # 35
    "key_i": [B("KEY_I")],  # 23
    "key_j": [B("KEY_J")],  # 36
    "key_k": [B("KEY_K")],  # 37
    "key_l": [B("KEY_L")],  # 38
    "key_m": [B("KEY_M")],  # 50
    "key_n": [B("KEY_N")],  # 49
    "key_o": [B("KEY_O")],  # 24
    "key_p": [B("KEY_P")],  # 25
    "key_q": [B("KEY_Q")],  # 16
    "key_r": [B("KEY_R")],  # 19
    "key_s": [B("KEY_S")],  # 31
    "key_t": [B("KEY_T")],  # 20
    "key_u": [B("KEY_U")],  # 22
    "key_v": [B("KEY_V")],  # 47
    "key_w": [B("KEY_W")],  # 17
    "key_x": [B("KEY_X")],  # 45
    "key_y": [B("KEY_Y")],  # 21
    "key_z": [B("KEY_Z")],  # 44
    "key_kpasterisk": [B("KEY_KPASTERISK")],  # 55
    "key_kpminus": [B("KEY_KPMINUS")],  # 74
    "key_kpplus": [B("KEY_KPPLUS")],  # 78
    "key_kpdot": [B("KEY_KPDOT")],  # 83
    "key_kpjpcomma": [B("KEY_KPJPCOMMA")],  # 95
    "key_kpenter": [B("KEY_KPENTER")],  # 96
    "key_kpslash": [B("KEY_KPSLASH")],  # 98
    "key_kpequal": [B("KEY_KPEQUAL")],  # 117
    "key_kpcomma": [B("KEY_KPCOMMA")],  # 121
    "key_kpleftparen": [B("KEY_KPLEFTPAREN")],  # 179
    "key_kprightparen": [B("KEY_KPRIGHTPAREN")],  # 180
    "key_kp0": [B("KEY_KP0")],  # 82
    "key_kp1": [B("KEY_KP1")],  # 79
    "key_kp2": [B("KEY_KP2")],  # 80
    "key_kp3": [B("KEY_KP3")],  # 81
    "key_kp4": [B("KEY_KP4")],  # 75
    "key_kp5": [B("KEY_KP5")],  # 76
    "key_kp6": [B("KEY_KP6")],  # 77
    "key_kp7": [B("KEY_KP7")],  # 71
    "key_kp8": [B("KEY_KP8")],  # 72
    "key_kp9": [B("KEY_KP9")],  # 73
    "key_f1": [B("KEY_F1")],  # 59
    "key_f2": [B("KEY_F2")],  # 60
    "key_f3": [B("KEY_F3")],  # 61
    "key_f4": [B("KEY_F4")],  # 62
    "key_f5": [B("KEY_F5")],  # 63
    "key_f6": [B("KEY_F6")],  # 64
    "key_f7": [B("KEY_F7")],  # 65
    "key_f8": [B("KEY_F8")],  # 66
    "key_f9": [B("KEY_F9")],  # 67
    "key_f11": [B("KEY_F11")],  # 87
    "key_f12": [B("KEY_F12")],  # 88
    "key_f10": [B("KEY_F10")],  # 68
    "key_f13": [B("KEY_F13")],  # 183
    "key_f14": [B("KEY_F14")],  # 184
    "key_f15": [B("KEY_F15")],  # 185
    "key_f16": [B("KEY_F16")],  # 186
    "key_f17": [B("KEY_F17")],  # 187
    "key_f18": [B("KEY_F18")],  # 188
    "key_f19": [B("KEY_F19")],  # 189
    "key_f20": [B("KEY_F20")],  # 190
    "key_f21": [B("KEY_F21")],  # 191
    "key_f22": [B("KEY_F22")],  # 192
    "key_f23": [B("KEY_F23")],  # 193
    "key_f24": [B("KEY_F24")],  # 194
    "key_playpause": [B("KEY_PLAYPAUSE")],  # 164
    "key_pause": [B("KEY_PAUSE")],  # 119
    "key_mute": [B("KEY_MUTE")],  # 113
    "key_stopcd": [B("KEY_STOPCD")],  # 166
    "key_forward": [B("KEY_FORWARD")],  # 159
    "key_ejectcd": [B("KEY_EJECTCD")],  # 161
    "key_nextsong": [B("KEY_NEXTSONG")],  # 163
    "key_previoussong": [B("KEY_PREVIOUSSONG")],  # 165
    "key_volumedown": [B("KEY_VOLUMEDOWN")],  # 114
    "key_volumeup": [B("KEY_VOLUMEUP")],  # 115
    "key_katakana": [B("KEY_KATAKANA")],  # 90
    "key_hiragana": [B("KEY_HIRAGANA")],  # 91
    "key_henkan": [B("KEY_HENKAN")],  # 92
    "key_katakanahiragana": [B("KEY_KATAKANAHIRAGANA")],  # 93
    "key_muhenkan": [B("KEY_MUHENKAN")],  # 94
    "key_zenkakuhankaku": [B("KEY_ZENKAKUHANKAKU")],  # 85
    "key_hanguel": [B("KEY_HANGUEL")],  # 122
    "key_hanja": [B("KEY_HANJA")],  # 123
    "key_yen": [B("KEY_YEN")],  # 124
    "key_unknown": [B("KEY_UNKNOWN")],  # 240,
    "key_prog1": [B("KEY_PROG1")],  # 148
    "key_prog2": [B("KEY_PROG2")],  # 149,
}

KEYBOARD_MAP_REV: dict[KeyboardButton, int] = {k: v[0] for k, v in _kbd_raw.items()}

KEYBOARD_MAP: dict[int, KeyboardButton] = to_map(_kbd_raw)

__all__ = ["GenericGamepadEvdev", "XBOX_BUTTON_MAP", "XBOX_AXIS_MAP", "B", "to_map"]
