#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.

import inspect
import os
import time
import traceback

from fenrirscreenreader.core import debug
from fenrirscreenreader.core import inputData
from fenrirscreenreader.core.i18n import _

currentdir = os.path.dirname(
    os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))
)
fenrir_path = os.path.dirname(currentdir)


class InputManager:
    def __init__(self):
        self.executeDeviceGrab = False
        self.lastDetectedDevices = None

    def initialize(self, environment):
        self.env = environment
        self.env["runtime"]["SettingsManager"].load_driver(
            self.env["runtime"]["SettingsManager"].get_setting(
                "keyboard", "driver"
            ),
            "InputDriver",
        )
        self.update_input_devices()

        # init LEDs with current state
        self.env["input"]["new_num_lock"] = self.env["runtime"][
            "InputDriver"
        ].get_led_state()
        self.env["input"]["old_num_lock"] = self.env["input"]["new_num_lock"]
        self.env["input"]["new_caps_lock"] = self.env["runtime"][
            "InputDriver"
        ].get_led_state(1)
        self.env["input"]["old_caps_lock"] = self.env["input"]["new_caps_lock"]
        self.env["input"]["new_scroll_lock"] = self.env["runtime"][
            "InputDriver"
        ].get_led_state(2)
        self.env["input"]["old_scroll_lock"] = self.env["input"]["new_scroll_lock"]
        self.lastDeepestInput = []
        self.lastEvent = None
        self.env["input"]["shortcut_repeat"] = 1
        self.lastInputTime = time.time()

    def shutdown(self):
        self.remove_all_devices()
        self.env["runtime"]["SettingsManager"].shutdown_driver("InputDriver")

    def get_input_event(self):
        event = None
        try:
            event = self.env["runtime"]["InputDriver"].get_input_event()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputManager get_input_event: Error getting input event: "
                + str(e),
                debug.DebugLevel.ERROR,
            )
        return event

    def set_execute_device_grab(self, newExecuteDeviceGrab=True):
        self.executeDeviceGrab = newExecuteDeviceGrab

    def handle_device_grab(self, force=False):
        if force:
            self.set_execute_device_grab()
        if not self.executeDeviceGrab:
            return
        if self.env["input"]["event_buffer"] != []:
            return
        if not self.no_key_pressed():
            return
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "keyboard", "grab_devices"
        ):
            self.executeDeviceGrab = False
            return

        # Add maximum retries to prevent infinite loops
        max_retries = 5
        retry_count = 0
        grab_timeout = 3  # Timeout in seconds
        start_time = time.time()

        if self.env["runtime"]["ScreenManager"].get_curr_screen_ignored():
            while not self.ungrab_all_devices():
                retry_count += 1
                if (
                    retry_count >= max_retries
                    or (time.time() - start_time) > grab_timeout
                ):
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Failed to ungrab devices after multiple attempts",
                        debug.DebugLevel.ERROR,
                    )
                    # Force a release of devices if possible through
                    # alternative means
                    try:
                        self.env["runtime"]["InputDriver"].force_ungrab()
                    except Exception as e:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "InputManager handle_device_grab: Error forcing ungrab: "
                            + str(e),
                            debug.DebugLevel.ERROR,
                        )
                    break
                time.sleep(0.25)
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"retry ungrab_all_devices {retry_count}/{max_retries}",
                    debug.DebugLevel.WARNING,
                )
        else:
            while not self.grab_all_devices():
                retry_count += 1
                if (
                    retry_count >= max_retries
                    or (time.time() - start_time) > grab_timeout
                ):
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "Failed to grab devices after multiple attempts",
                        debug.DebugLevel.ERROR,
                    )
                    # Continue without grabbing input - limited functionality
                    # but not locked
                    break
                time.sleep(0.25)
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"retry grab_all_devices {retry_count}/{max_retries}",
                    debug.DebugLevel.WARNING,
                )

        self.executeDeviceGrab = False

    def send_keys(self, keyMacro):
        for e in keyMacro:
            key = ""
            value = 0
            if len(e) != 2:
                continue
            if isinstance(e[0], int) and isinstance(e[1], str):
                key = e[1].upper()
                value = e[0]
            elif isinstance(e[1], int) and isinstance(e[0], str):
                key = e[0].upper()
                value = e[1]
            else:
                continue
            if key.upper() == "SLEEP":
                time.sleep(value)
            else:
                self.env["runtime"]["InputDriver"].send_key(key, value)

    def get_last_event(self):
        return self.lastEvent

    def record_unmanaged_keypress(self, event_name):
        self.lastEvent = {
            "event_name": event_name,
            "event_state": 1,
        }
        self.set_last_deepest_input([event_name])
        self.lastInputTime = time.time()

    def handle_input_event(self, event_data):
        if not event_data:
            return
        self.lastEvent = event_data
        # a hang apears.. try to fix
        if self.env["input"]["event_buffer"] == []:
            if self.env["input"]["curr_input"] != []:
                self.env["input"]["curr_input"] = []
                self.env["input"]["shortcut_repeat"] = 1

        self.env["input"]["prev_input"] = self.env["input"]["curr_input"].copy()
        if event_data["event_state"] == 0:
            if event_data["event_name"] in self.env["input"]["curr_input"]:
                self.env["input"]["curr_input"].remove(event_data["event_name"])
                if len(self.env["input"]["curr_input"]) > 1:
                    self.env["input"]["curr_input"] = sorted(
                        self.env["input"]["curr_input"]
                    )
                elif len(self.env["input"]["curr_input"]) == 0:
                    self.env["input"]["shortcut_repeat"] = 1
                self.lastInputTime = time.time()
        elif event_data["event_state"] == 1:
            if not event_data["event_name"] in self.env["input"]["curr_input"]:
                self.env["input"]["curr_input"].append(event_data["event_name"])
                if len(self.env["input"]["curr_input"]) > 1:
                    self.env["input"]["curr_input"] = sorted(
                        self.env["input"]["curr_input"]
                    )
                if len(self.lastDeepestInput) < len(
                    self.env["input"]["curr_input"]
                ):
                    self.set_last_deepest_input(
                        self.env["input"]["curr_input"].copy()
                    )
                elif self.lastDeepestInput == self.env["input"]["curr_input"]:
                    if time.time() - self.lastInputTime <= self.env["runtime"][
                        "SettingsManager"
                    ].get_setting_as_float("keyboard", "double_tap_timeout"):
                        self.env["input"]["shortcut_repeat"] += 1
                    else:
                        self.env["input"]["shortcut_repeat"] = 1
                self.handle_led_states(event_data)
                self.lastInputTime = time.time()
        elif event_data["event_state"] == 2:
            self.lastInputTime = time.time()

        self.env["input"]["old_num_lock"] = self.env["input"]["new_num_lock"]
        self.env["input"]["new_num_lock"] = self.env["runtime"][
            "InputDriver"
        ].get_led_state()
        self.env["input"]["old_caps_lock"] = self.env["input"]["new_caps_lock"]
        self.env["input"]["new_caps_lock"] = self.env["runtime"][
            "InputDriver"
        ].get_led_state(1)
        self.env["input"]["old_scroll_lock"] = self.env["input"]["new_scroll_lock"]
        self.env["input"]["new_scroll_lock"] = self.env["runtime"][
            "InputDriver"
        ].get_led_state(2)
        self.env["runtime"]["DebugManager"].write_debug_out(
            "curr_input " + str(self.env["input"]["curr_input"]),
            debug.DebugLevel.INFO,
        )
        if self.no_key_pressed():
            self.env["input"]["prev_input"] = []

    def handle_led_states(self, m_event):
        if self.curr_key_is_modifier():
            return
        try:
            if m_event["event_name"] == "KEY_NUMLOCK":
                self.env["runtime"]["InputDriver"].toggle_led_state()
            elif m_event["event_name"] == "KEY_CAPSLOCK":
                self.env["runtime"]["InputDriver"].toggle_led_state(1)
            elif m_event["event_name"] == "KEY_SCROLLLOCK":
                self.env["runtime"]["InputDriver"].toggle_led_state(2)
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputManager handle_led_states: Error toggling LED state: "
                + str(e),
                debug.DebugLevel.ERROR,
            )

    def grab_all_devices(self):
        if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "keyboard", "grab_devices"
        ):
            try:
                return self.env["runtime"]["InputDriver"].grab_all_devices()
            except Exception as e:
                return False
        return True

    def ungrab_all_devices(self):
        if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "keyboard", "grab_devices"
        ):
            try:
                return self.env["runtime"]["InputDriver"].ungrab_all_devices()
            except Exception as e:
                return False
        return True

    def handle_plug_input_device(self, event_data):
        for deviceEntry in event_data:
            self.update_input_devices(deviceEntry["device"])

    def update_input_devices(self, newDevice=None):
        try:
            self.env["runtime"]["InputDriver"].update_input_devices(newDevice)
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputManager update_input_devices: Error updating input devices: "
                + str(e),
                debug.DebugLevel.ERROR,
            )
        try:
            if self.env["runtime"]["ScreenManager"]:
                self.handle_device_grab(force=True)
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputManager update_input_devices: Error handling device grab: "
                + str(e),
                debug.DebugLevel.ERROR,
            )

    def remove_all_devices(self):
        try:
            self.env["runtime"]["InputDriver"].remove_all_devices()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "InputManager remove_all_devices: Error removing devices: "
                + str(e),
                debug.DebugLevel.ERROR,
            )

    def convert_event_name(self, event_name):
        if not event_name:
            return ""
        if event_name == "":
            return ""
        event_name = event_name.upper()
        if event_name == "KEY_LEFTCTRL":
            event_name = "KEY_CTRL"
        elif event_name == "KEY_RIGHTCTRL":
            event_name = "KEY_CTRL"
        elif event_name == "KEY_LEFTSHIFT":
            event_name = "KEY_SHIFT"
        elif event_name == "KEY_RIGHTSHIFT":
            event_name = "KEY_SHIFT"
        elif event_name == "KEY_LEFTALT":
            event_name = "KEY_ALT"
        elif event_name == "KEY_RIGHTALT":
            event_name = "KEY_ALT"
        elif event_name == "KEY_LEFTMETA":
            event_name = "KEY_META"
        elif event_name == "KEY_RIGHTMETA":
            event_name = "KEY_META"
        if self.is_fenrir_key(event_name):
            event_name = "KEY_FENRIR"
        if self.is_script_key(event_name):
            event_name = "KEY_SCRIPT"
        return event_name

    def clear_event_buffer(self):
        try:
            self.env["runtime"]["InputDriver"].clear_event_buffer()
        except Exception as e:
            pass

    def reset_input_state(self):
        self.env["input"]["curr_input"] = []
        self.env["input"]["prev_input"] = []
        self.env["input"]["event_buffer"] = []
        self.env["input"]["shortcut_repeat"] = 1
        self.clear_last_deep_input()
        self.clear_event_buffer()

    def set_last_deepest_input(self, currentDeepestInput):
        self.lastDeepestInput = currentDeepestInput

    def clear_last_deep_input(self):
        self.lastDeepestInput = []

    def get_last_input_time(self):
        return self.lastInputTime

    def get_last_deepest_input(self):
        return self.lastDeepestInput

    def write_event_buffer(self):
        try:
            if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
                "keyboard", "grab_devices"
            ):
                self.env["runtime"]["InputDriver"].write_event_buffer()
            self.clear_event_buffer()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "Error while write_u_input", debug.DebugLevel.ERROR
            )
            self.env["runtime"]["DebugManager"].write_debug_out(
                str(e), debug.DebugLevel.ERROR
            )

    def no_key_pressed(self):
        return self.env["input"]["curr_input"] == []

    def is_key_press(self):
        return (self.env["input"]["prev_input"] == []) and (
            self.env["input"]["curr_input"] != []
        )

    def get_prev_deepest_shortcut(self):
        shortcut = []
        shortcut.append(self.env["input"]["shortcut_repeat"])
        shortcut.append(self.get_last_deepest_input())
        return str(shortcut)

    def get_prev_shortcut(self):
        shortcut = []
        shortcut.append(self.env["input"]["shortcut_repeat"])
        shortcut.append(self.env["input"]["prev_input"])
        return str(shortcut)

    def get_curr_shortcut(self, inputSequence=None):
        shortcut = []
        shortcut.append(self.env["input"]["shortcut_repeat"])

        numpad_keys = [
            "KEY_KP0",
            "KEY_KP1",
            "KEY_KP2",
            "KEY_KP3",
            "KEY_KP4",
            "KEY_KP5",
            "KEY_KP6",
            "KEY_KP7",
            "KEY_KP8",
            "KEY_KP9",
            "KEY_KPDOT",
            "KEY_KPPLUS",
            "KEY_KPMINUS",
            "KEY_KPASTERISK",
            "KEY_KPSLASH",
            "KEY_KPENTER",
            "KEY_KPEQUAL",
        ]
        if inputSequence:
            # Check if any key in the sequence is a numpad key and numlock is ON
            # If numlock is ON and any key in the sequence is a numpad key,
            # return an empty shortcut
            if not self.env["runtime"][
                "CursorManager"
            ].should_process_numpad_commands():
                for key in inputSequence:
                    if key in numpad_keys:
                        # Return an empty/invalid shortcut that won't match any
                        # command
                        return "[]"

            shortcut.append(inputSequence)
        else:
            # Same check for current input

            if not self.env["runtime"][
                "CursorManager"
            ].should_process_numpad_commands():
                for key in self.env["input"]["curr_input"]:
                    if key in numpad_keys:
                        # Return an empty/invalid shortcut that won't match any
                        # command
                        return "[]"

            shortcut.append(self.env["input"]["curr_input"])

        if len(self.env["input"]["prev_input"]) < len(
            self.env["input"]["curr_input"]
        ):
            if self.env["input"][
                "shortcut_repeat"
            ] > 1 and not self.shortcut_exists(str(shortcut)):
                shortcut = []
                self.env["input"]["shortcut_repeat"] = 1
                shortcut.append(self.env["input"]["shortcut_repeat"])
                shortcut.append(self.env["input"]["curr_input"])
        self.env["runtime"]["DebugManager"].write_debug_out(
            "curr_shortcut " + str(shortcut), debug.DebugLevel.INFO
        )
        return str(shortcut)

    def curr_key_is_modifier(self):
        if len(self.get_last_deepest_input()) != 1:
            return False
        return (self.env["input"]["curr_input"][0] == "KEY_FENRIR") or (
            self.env["input"]["curr_input"][0] == "KEY_SCRIPT"
        )

    def is_fenrir_key(self, event_name):
        return event_name in self.env["input"]["fenrir_key"]

    def is_script_key(self, event_name):
        return event_name in self.env["input"]["script_key"]

    def get_command_for_shortcut(self, shortcut):
        if not self.shortcut_exists(shortcut):
            return ""
        return self.env["bindings"][shortcut]

    def key_echo(self, event_data=None):
        if not event_data:
            event_data = self.get_last_event()
            if not event_data:
                return
        key_name = ""
        if event_data["event_state"] == 1:
            key_name = event_data["event_name"].lower()
            if key_name.startswith("key_"):
                key_name = key_name[4:]
                self.env["runtime"]["OutputManager"].present_text(
                    _(key_name), interrupt=True
                )

    def shortcut_exists(self, shortcut):
        return shortcut in self.env["bindings"]

    def load_shortcuts(
        self,
        kb_config_path=fenrir_path + "/../../config/keyboard/desktop.conf",
    ):
        kb_config = open(kb_config_path, "r")
        while True:
            invalid = False
            line = kb_config.readline()
            if not line:
                break
            line = line.replace("\n", "")
            if line.replace(" ", "") == "":
                continue
            if line.replace(" ", "").startswith("#"):
                continue
            if line.count("=") != 1:
                continue
            sep_line = line.split("=")
            command_name = sep_line[1].upper()
            sep_line[0] = sep_line[0].replace(" ", "")
            sep_line[0] = sep_line[0].replace("'", "")
            sep_line[0] = sep_line[0].replace('"', "")
            keys = sep_line[0].split(",")
            shortcut_keys = []
            shortcut_repeat = 1
            shortcut = []
            for key in keys:
                try:
                    shortcut_repeat = int(key)
                except Exception as e:
                    if not self.is_valid_key(key.upper()):
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "invalid key : "
                            + key.upper()
                            + " command:"
                            + command_name,
                            debug.DebugLevel.WARNING,
                        )
                        invalid = True
                        break
                    shortcut_keys.append(key.upper())
            if invalid:
                continue
            shortcut.append(shortcut_repeat)
            shortcut.append(sorted(shortcut_keys))
            if len(shortcut_keys) != 1 and "KEY_FENRIR" not in shortcut_keys:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "invalid shortcut (missing KEY_FENRIR): "
                    + str(shortcut)
                    + " command:"
                    + command_name,
                    debug.DebugLevel.ERROR,
                )
                continue
            self.env["runtime"]["DebugManager"].write_debug_out(
                "Shortcut: " + str(shortcut) + " command:" + command_name,
                debug.DebugLevel.INFO,
                on_any_level=True,
            )
            self.env["bindings"][str(shortcut)] = command_name
            self.env["rawBindings"][str(shortcut)] = shortcut
        kb_config.close()
        # fix bindings
        self.env["bindings"][
            str([1, ["KEY_F1", "KEY_FENRIR"]])
        ] = "TOGGLE_TUTORIAL_MODE"

    def is_valid_key(self, key):
        return key in inputData.key_names

    def set_last_detected_devices(self, devices):
        self.lastDetectedDevices = devices

    def get_last_detected_devices(self):
        return self.lastDetectedDevices

    def reload_shortcuts(self):
        """Reload keyboard shortcuts from current layout setting"""
        # Clear existing bindings
        self.env["bindings"].clear()
        self.env["rawBindings"].clear()

        # Get current layout path
        layout_setting = self.env["runtime"]["SettingsManager"].get_setting(
            "keyboard", "keyboard_layout"
        )

        # Resolve full path if needed
        if not os.path.exists(layout_setting):
            settings_root = "/etc/fenrirscreenreader/"
            if not os.path.exists(settings_root):
                import fenrirscreenreader

                fenrir_path = os.path.dirname(fenrirscreenreader.__file__)
                settings_root = fenrir_path + "/../../config/"

            layout_path = (
                settings_root + "keyboard/" + layout_setting + ".conf"
            )
            if not os.path.exists(layout_path):
                # Fallback to default if layout not found
                layout_path = settings_root + "keyboard/desktop.conf"
        else:
            layout_path = layout_setting

        # Reload shortcuts
        self.load_shortcuts(layout_path)

        self.env["runtime"]["DebugManager"].write_debug_out(
            "Reloaded shortcuts from: " + layout_path,
            debug.DebugLevel.INFO,
            on_any_level=True,
        )
