import logging
import re
import select
import time
from threading import Event as TEvent
from typing import Sequence

import evdev

from hhd.controller import DEBUG_MODE, Event, Multiplexer, can_read
from hhd.controller.base import Event
from hhd.controller.lib.hide import unhide_all
from hhd.controller.physical.evdev import B as EC
from hhd.controller.physical.evdev import GenericGamepadEvdev
from hhd.controller.physical.hidraw import GenericGamepadHidraw
from hhd.controller.physical.imu import CombinedImu, HrtimerTrigger
from hhd.controller.virtual.uinput import UInputDevice
from hhd.plugins import Config, Context, Emitter, get_gyro_state, get_outputs

from .const import (
    GPD_TOUCHPAD_AXIS_MAP,
    GPD_TOUCHPAD_BUTTON_MAP,
    GPD_WIN_DEFAULT_MAPPINGS,
)

ERROR_DELAY = 0.3
SELECT_TIMEOUT = 1
ERROR_DELAY = 0.3
LONGER_ERROR_DELAY = 3
LONGER_ERROR_MARGIN = 1.3

logger = logging.getLogger(__name__)

# Old devices were 2f24:0135
# Win 5 new firmware uses 2f24:0137
# New 2025 Win mini uses 045e:002d
GPD_WIN_VIDS = [0x2F24, 0x045E]
GPD_WIN_PIDS = [0x0135, 0x0137, 0x002D]
GAMEPAD_VID = 0x045E
GAMEPAD_PID = 0x028E

# Win Max 2
TOUCHPAD_VID = 0x093A
TOUCHPAD_PID = 0x0255
# Win Minis
TOUCHPAD_VID_2 = 0x0911
TOUCHPAD_PID_2 = 0x5288

KBD_VID = 0x0001
KBD_PID = 0x0001

BACK_BUTTON_DELAY = 0.025
DIRECT_BUTTONS = {
    EC("KEY_F13"): "share",
}

# /dev/input/event17 Microsoft X-Box 360 pad usb-0000:73:00.3-4.1/input0
# bus: 0003, vendor 045e, product 028e, version 0101

# back buttons
# /dev/input/event15   Mouse for Windows usb-0000:73:00.3-4.2/input1
# bus: 0003, vendor 2f24, product 0135, version 0110

# physical keyboard
# /dev/input/event13   Mouse for Windows usb-0000:73:00.3-4.2/input0
# bus: 0003, vendor 2f24, product 0135, version 0110

# hidraw back buttons  {'path': b'/dev/hidraw6',
#    'vendor_id': 12068, 'product_id': 309, 'serial_number': '',
#    'release_number': 256, 'manufacturer_string': ' ',
#    'product_string': 'Mouse for Windows',
#    'usage_page': 1, 'usage': 6, 'interface_number': 1},

LEFT_BUTTONS = {
    EC("KEY_SYSRQ"),
    EC("KEY_F20"),
    EC("KEY_F14"),
    EC("KEY_L"),
}

RIGHT_BUTTONS = {
    EC("KEY_PAUSE"),
    EC("KEY_F21"),
    EC("KEY_F15"),
    EC("KEY_R")
}

class BackbuttonsEvdev(GenericGamepadEvdev):
    def __init__(self, *args, **kwargs) -> None:
        self.left_pressed = False
        self.left_released = None
        self.right_pressed = False
        self.right_released = None
        super().__init__(*args, **kwargs)

    def produce(self, fds: Sequence[int]):
        if not self.dev:
            return []

        # GPD events execute micro sequences
        # Inbetween the sequences, there is a ~20ms gap in which the
        # button is not pressed. Therefore, record when the button was
        # pressed and if more than ~25ms has passed, consider it released.
        curr = time.perf_counter()
        out = []
        while self.fd in fds and can_read(self.dev):
            for e in self.dev.read():
                if e.type != EC("EV_KEY"):
                    continue

                pressed = e.value != 0

                if e.code in DIRECT_BUTTONS:
                    out.append(
                        {
                            "type": "button",
                            "code": DIRECT_BUTTONS[e.code],
                            "value": pressed,
                        }
                    )
                    continue

                if e.code in LEFT_BUTTONS:
                    if pressed:
                        if not self.left_pressed:
                            out.append(
                                {"type": "button", "code": "extra_l1", "value": True}
                            )
                        self.left_pressed = True
                        self.left_released = None
                    else:
                        self.left_released = curr
                if e.code in RIGHT_BUTTONS:
                    if pressed:
                        if not self.right_pressed:
                            out.append(
                                {
                                    "type": "button",
                                    "code": "extra_r1",
                                    "value": True,
                                }
                            )
                        self.right_pressed = True
                        self.right_released = None
                    else:
                        self.right_released = curr

        if self.left_released and curr - self.left_released > BACK_BUTTON_DELAY:
            out.append({"type": "button", "code": "extra_l1", "value": False})
            self.left_released = None
            self.left_pressed = False
        if self.right_released and curr - self.right_released > BACK_BUTTON_DELAY:
            out.append({"type": "button", "code": "extra_r1", "value": False})
            self.right_released = None
            self.right_pressed = False

        return out


def plugin_run(
    conf: Config,
    emit: Emitter,
    context: Context,
    should_exit: TEvent,
    updated: TEvent,
    dconf: dict,
):
    first = True
    first_disabled = True
    init = time.perf_counter()
    repeated_fail = False
    while not should_exit.is_set():
        if conf["controller_mode.mode"].to(str) == "disabled":
            time.sleep(ERROR_DELAY)
            if first_disabled:
                unhide_all()
            first_disabled = False
            continue
        else:
            first_disabled = True

        found_gamepad = False
        try:
            for d in evdev.list_devices():
                dev = evdev.InputDevice(d)
                if dev.info.vendor == GAMEPAD_VID and dev.info.product == GAMEPAD_PID:
                    found_gamepad = True
                    break
        except Exception:
            logger.warning("Failed finding device, skipping check.")
            found_gamepad = True

        if not found_gamepad:
            if first:
                logger.info("Controller in Mouse mode. Waiting...")
            time.sleep(ERROR_DELAY)
            first = False
            continue

        try:
            logger.info("Launching emulated controller.")
            updated.clear()
            init = time.perf_counter()
            controller_loop(conf.copy(), should_exit, updated, dconf, emit)
            repeated_fail = False
        except Exception as e:
            failed_fast = init + LONGER_ERROR_MARGIN > time.perf_counter()
            sleep_time = (
                LONGER_ERROR_DELAY if repeated_fail and failed_fast else ERROR_DELAY
            )
            repeated_fail = failed_fast
            logger.exception(
                f"Assuming controllers disconnected, restarting after {sleep_time}s."
            )
            # Raise exception
            if DEBUG_MODE:
                import traceback

                logger.error(traceback.format_exc())
            time.sleep(sleep_time)

    # Unhide all devices before exiting
    unhide_all()


def controller_loop(
    conf: Config, should_exit: TEvent, updated: TEvent, dconf: dict, emit: Emitter
):
    debug = DEBUG_MODE
    has_touchpad = dconf.get("touchpad", False)

    # Output
    d_producers, d_outs, d_params = get_outputs(
        conf["controller_mode"],
        conf["touchpad"] if has_touchpad else None,
        conf["imu"].to(bool),
        emit=emit,
        extra_buttons=dconf.get("extra_buttons", "dual"),
    )
    motion = d_params.get("uses_motion", True)

    # Imu
    d_imu = CombinedImu(
        conf["imu_hz"].to(int),
        get_gyro_state(
            conf["imu_axis"], dconf.get("mapping", GPD_WIN_DEFAULT_MAPPINGS)
        ),
    )
    d_timer = HrtimerTrigger(conf["imu_hz"].to(int), [HrtimerTrigger.IMU_NAMES])

    # Inputs
    d_xinput = GenericGamepadEvdev(
        vid=[GAMEPAD_VID],
        pid=[GAMEPAD_PID],
        # name=["Generic X-Box pad"],
        capabilities={EC("EV_KEY"): [EC("BTN_A")]},
        required=True,
        hide=True,
    )

    # "PNP0C50:00 0911:5288 Touchpad" on Win Max 2 2023
    # "PNP0C50:00 093A:0255 Touchpad" on Win Mini
    d_touch = GenericGamepadEvdev(
        vid=[TOUCHPAD_VID, TOUCHPAD_VID_2],
        pid=[TOUCHPAD_PID, TOUCHPAD_PID_2],
        name=[re.compile(".+Touchpad")],
        capabilities={EC("EV_KEY"): [EC("BTN_MOUSE")]},
        btn_map=GPD_TOUCHPAD_BUTTON_MAP,
        axis_map=GPD_TOUCHPAD_AXIS_MAP,
        aspect_ratio=1.333,
        required=False,
    )

    # Vendor back buttons
    d_kbd_1 = BackbuttonsEvdev(
        vid=GPD_WIN_VIDS,
        pid=GPD_WIN_PIDS,
        capabilities={EC("EV_KEY"): [EC("KEY_SYSRQ"), EC("KEY_PAUSE")]},
        required=True,
        grab=True,
    )

    grab_at = dconf.get("grab_at", True)
    d_kbd_2 = None
    dedicated_hhd_button = dconf.get("dedicated_hhd_button", False)
    share_to_qam = dedicated_hhd_button
    btn_map = dconf.get("btn_mapping", None)
    if btn_map:
        d_kbd_2 = GenericGamepadEvdev(
            vid=[KBD_VID],
            pid=[KBD_PID],
            required=False,
            grab=grab_at,
            btn_map=btn_map,
        )
        share_to_qam = True

    if dconf.get("manage_l4r4", True):
        match conf["l4r4"].to(str):
            case "l4":
                qam_button = "extra_l1"
                l4r4_enabled = True
                qam_hold = "hhd"
            case "r4":
                qam_button = "extra_r1"
                l4r4_enabled = True
                qam_hold = "hhd"
            case "menu":
                qam_button = "mode"
                l4r4_enabled = True
                qam_hold = "mode"
            case "disabled":
                qam_button = None
                l4r4_enabled = False
                qam_hold = "hhd"
            case _:
                qam_button = None
                l4r4_enabled = True
                qam_hold = "hhd"
    else:
        qam_button = None
        l4r4_enabled = True
        qam_hold = "hhd"

    keyboard_is = "steam_qam" if dedicated_hhd_button else "keyboard"

    if has_touchpad:
        touch_actions = (
            conf["touchpad.controller"]
            if conf.get("touchpad.mode", None) == "controller"
            else conf["touchpad.emulation"]
        )

        multiplexer = Multiplexer(
            trigger="analog_to_discrete",
            dpad="analog_to_discrete",
            touchpad_short=touch_actions.get("short", "disabled"),
            touchpad_hold=touch_actions.get("hold", "disabled"),
            nintendo_mode=conf["nintendo_mode"].to(bool),
            qam_button=qam_button,
            share_to_qam=share_to_qam,
            emit=emit,
            params=d_params,
            # qam_multi_tap=qam_multi_tap, # supports it now
            qam_hold=qam_hold,
            keyboard_is=keyboard_is,
            qam_hhd=dedicated_hhd_button,
            startselect_chord=conf.get("main_chords", "disabled"),
        )
    else:
        multiplexer = Multiplexer(
            trigger="analog_to_discrete",
            dpad="analog_to_discrete",
            nintendo_mode=conf["nintendo_mode"].to(bool),
            qam_button=qam_button,
            share_to_qam=share_to_qam,
            emit=emit,
            params=d_params,
            # qam_multi_tap=qam_multi_tap, # supports it now
            qam_hold=qam_hold,
            keyboard_is=keyboard_is,
            qam_hhd=dedicated_hhd_button,
            startselect_chord=conf.get("main_chords", "disabled"),
        )

    d_volume_btn = UInputDevice(
        name="Handheld Daemon Volume Keyboard",
        phys="phys-hhd-vbtn",
        capabilities={EC("EV_KEY"): [EC("KEY_VOLUMEUP"), EC("KEY_VOLUMEDOWN")]},
        btn_map={
            "key_volumeup": EC("KEY_VOLUMEUP"),
            "key_volumedown": EC("KEY_VOLUMEDOWN"),
        },
        pid=KBD_PID,
        vid=KBD_VID,
        output_timestamps=True,
    )

    REPORT_FREQ_MIN = 25
    REPORT_FREQ_MAX = 400

    if motion:
        REPORT_FREQ_MAX = max(REPORT_FREQ_MAX, conf["imu_hz"].to(float))

    REPORT_DELAY_MAX = 1 / REPORT_FREQ_MIN
    REPORT_DELAY_MIN = 1 / REPORT_FREQ_MAX

    fds = []
    devs = []
    fd_to_dev = {}

    def prepare(m):
        devs.append(m)
        fs = m.open()
        fds.extend(fs)
        for f in fs:
            fd_to_dev[f] = m

    try:
        hid_btn_fds = []
        if l4r4_enabled:
            kbd_fds = d_kbd_1.open()
            fds.extend(kbd_fds)
        else:
            kbd_fds = []
        prepare(d_xinput)
        if motion:
            start_imu = True
            try:
                if dconf.get("hrtimer", False):
                    start_imu = d_timer.open()
                if start_imu:
                    prepare(d_imu)
            except Exception as e:
                motion = False
                try:
                    d_timer.close()
                except Exception:
                    pass

                logger.error("Failed to open IMU, disabling motion support. Error:")
                # Print stacktrace
                import traceback

                traceback.print_exc()
        if has_touchpad and d_params["uses_touch"]:
            prepare(d_touch)
        if grab_at and d_kbd_2:
            prepare(d_volume_btn)
        if d_kbd_2:
            prepare(d_kbd_2)
        for d in d_producers:
            prepare(d)

        logger.info("Emulated controller launched, have fun!")
        while not should_exit.is_set() and not updated.is_set():
            start = time.perf_counter()
            # Add timeout to call consumers a minimum amount of times per second
            r, _, _ = select.select(fds, [], [], REPORT_DELAY_MAX)
            evs = []
            to_run = set()
            for f in r:
                # skip kbd_1 and hid_btns to always run them
                if f not in kbd_fds and f not in hid_btn_fds:
                    to_run.add(id(fd_to_dev[f]))

            for d in devs:
                if id(d) in to_run:
                    evs.extend(d.produce(r))
            evs.extend(d_kbd_1.produce(r))

            evs = multiplexer.process(evs)
            if evs:
                if debug:
                    logger.info(evs)
                d_volume_btn.consume(evs)
                d_xinput.consume(evs)

            for d in d_outs:
                d.consume(evs)

            # If unbounded, the total number of events per second is the sum of all
            # events generated by the producers.
            # For Legion go, that would be 100 + 100 + 500 + 30 = 730
            # Since the controllers of the legion go only update at 500hz, this is
            # wasteful.
            # By setting a target refresh rate for the report and sleeping at the
            # end, we ensure that even if multiple fds become ready close to each other
            # they are combined to the same report, limiting resource use.
            # Ideally, this rate is smaller than the report rate of the hardware controller
            # to ensure there is always a report from that ready during refresh
            t = time.perf_counter()
            elapsed = t - start
            if elapsed < REPORT_DELAY_MIN:
                time.sleep(REPORT_DELAY_MIN - elapsed)

    except KeyboardInterrupt:
        raise
    finally:
        try:
            d_kbd_1.close(not updated.is_set())
        except Exception as e:
            logger.error(f"Error while closing device '{d}' with exception:\n{e}")
            if debug:
                raise e
        try:
            d_timer.close()
        except Exception as e:
            logger.error(f"Error while closing device '{d}' with exception:\n{e}")
            if debug:
                raise e
        for d in reversed(devs):
            try:
                d.close(not updated.is_set())
            except Exception as e:
                logger.error(f"Error while closing device '{d}' with exception:\n{e}")
                if debug:
                    raise e
