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

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

import os
import sys
import time

from fenrirscreenreader.core import debug
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.inputDriver import InputDriver as inputDriver

try:
    from Xlib import X
    from Xlib import XK
    from Xlib import display
    from Xlib.error import BadAccess
    from Xlib.error import BadWindow
except Exception as x_error:
    X = None
    XK = None
    display = None
    BadAccess = Exception
    BadWindow = Exception
    _x_error = x_error
else:
    _x_error = None


def build_keysym_name_map():
    if XK is None:
        return {}
    names = {}
    for attr_name in dir(XK):
        if not attr_name.startswith("XK_"):
            continue
        value = getattr(XK, attr_name)
        if not isinstance(value, int):
            continue
        names.setdefault(value, attr_name[3:])
    return names


KEYSYM_NAME_MAP = build_keysym_name_map()


class X11DriverError(RuntimeError):
    pass


class driver(inputDriver):
    """X11 terminal-scoped keyboard input driver."""

    key_name_overrides = {
        "BackSpace": "KEY_BACKSPACE",
        "Return": "KEY_ENTER",
        "ISO_Enter": "KEY_ENTER",
        "Tab": "KEY_TAB",
        "Escape": "KEY_ESC",
        "space": "KEY_SPACE",
        "minus": "KEY_MINUS",
        "equal": "KEY_EQUAL",
        "bracketleft": "KEY_LEFTBRACE",
        "bracketright": "KEY_RIGHTBRACE",
        "backslash": "KEY_BACKSLASH",
        "semicolon": "KEY_SEMICOLON",
        "apostrophe": "KEY_APOSTROPHE",
        "grave": "KEY_GRAVE",
        "comma": "KEY_COMMA",
        "period": "KEY_DOT",
        "slash": "KEY_SLASH",
        "Shift_L": "KEY_LEFTSHIFT",
        "Shift_R": "KEY_RIGHTSHIFT",
        "Control_L": "KEY_LEFTCTRL",
        "Control_R": "KEY_RIGHTCTRL",
        "Alt_L": "KEY_LEFTALT",
        "Alt_R": "KEY_RIGHTALT",
        "Meta_L": "KEY_LEFTMETA",
        "Meta_R": "KEY_RIGHTMETA",
        "Super_L": "KEY_LEFTMETA",
        "Super_R": "KEY_RIGHTMETA",
        "Multi_key": "KEY_COMPOSE",
        "Caps_Lock": "KEY_CAPSLOCK",
        "Num_Lock": "KEY_NUMLOCK",
        "Scroll_Lock": "KEY_SCROLLLOCK",
        "Insert": "KEY_INSERT",
        "Delete": "KEY_DELETE",
        "Home": "KEY_HOME",
        "End": "KEY_END",
        "Prior": "KEY_PAGEUP",
        "Next": "KEY_PAGEDOWN",
        "Page_Up": "KEY_PAGEUP",
        "Page_Down": "KEY_PAGEDOWN",
        "Up": "KEY_UP",
        "Down": "KEY_DOWN",
        "Left": "KEY_LEFT",
        "Right": "KEY_RIGHT",
        "KP_0": "KEY_KP0",
        "KP_Insert": "KEY_KP0",
        "KP_1": "KEY_KP1",
        "KP_End": "KEY_KP1",
        "KP_2": "KEY_KP2",
        "KP_Down": "KEY_KP2",
        "KP_3": "KEY_KP3",
        "KP_Page_Down": "KEY_KP3",
        "KP_4": "KEY_KP4",
        "KP_Left": "KEY_KP4",
        "KP_5": "KEY_KP5",
        "KP_Begin": "KEY_KP5",
        "KP_6": "KEY_KP6",
        "KP_Right": "KEY_KP6",
        "KP_7": "KEY_KP7",
        "KP_Home": "KEY_KP7",
        "KP_8": "KEY_KP8",
        "KP_Up": "KEY_KP8",
        "KP_9": "KEY_KP9",
        "KP_Page_Up": "KEY_KP9",
        "KP_Decimal": "KEY_KPDOT",
        "KP_Delete": "KEY_KPDOT",
        "KP_Add": "KEY_KPPLUS",
        "KP_Subtract": "KEY_KPMINUS",
        "KP_Multiply": "KEY_KPASTERISK",
        "KP_Divide": "KEY_KPSLASH",
        "KP_Enter": "KEY_KPENTER",
        "KP_Equal": "KEY_KPEQUAL",
    }

    modifier_masks = {
        "KEY_SHIFT": X.ShiftMask if X else 1,
        "KEY_LEFTSHIFT": X.ShiftMask if X else 1,
        "KEY_RIGHTSHIFT": X.ShiftMask if X else 1,
        "KEY_CTRL": X.ControlMask if X else 4,
        "KEY_LEFTCTRL": X.ControlMask if X else 4,
        "KEY_RIGHTCTRL": X.ControlMask if X else 4,
        "KEY_ALT": X.Mod1Mask if X else 8,
        "KEY_LEFTALT": X.Mod1Mask if X else 8,
        "KEY_RIGHTALT": X.Mod1Mask if X else 8,
        "KEY_META": X.Mod4Mask if X else 64,
        "KEY_LEFTMETA": X.Mod4Mask if X else 64,
        "KEY_RIGHTMETA": X.Mod4Mask if X else 64,
    }

    modifier_key_names = set(modifier_masks.keys())

    def __init__(self):
        inputDriver.__init__(self)
        self.display = None
        self.root = None
        self.window = None
        self.window_id = None
        self.active = True
        self.num_lock_mask = 0
        self.grabbed = set()
        self.grab_signature = None
        self.interesting_keys = set()
        self.fenrir_keys = set()
        self.failed_grabs = 0

    def initialize(self, environment):
        self.env = environment
        if display is None:
            self.fail_startup("python-xlib is not available: " + str(_x_error))
        self.display = display.Display()
        self.root = self.display.screen().root
        self.window_id = self.resolve_window_id()
        if self.window_id is None:
            self.fail_startup(
                "No X11 target window found. Use --x11-window-id or launch from an X11 terminal."
            )
        self.write_debug(
            "x11Driver target window "
            + self.format_window_id(self.window_id)
            + ", active window "
            + self.format_window_id(self.get_active_window_id())
            + ", WINDOWID="
            + os.environ.get("WINDOWID", ""),
            debug.DebugLevel.INFO,
        )
        self.window = self.display.create_resource_object("window", self.window_id)
        self.window.change_attributes(
            event_mask=(
                X.KeyPressMask
                | X.KeyReleaseMask
                | X.FocusChangeMask
                | X.StructureNotifyMask
            )
        )
        self.num_lock_mask = self.find_num_lock_mask()
        self.refresh_interesting_keys()
        self.refresh_grabs(force=True)
        self.env["runtime"]["ProcessManager"].add_custom_event_thread(
            self.input_watchdog
        )
        self._initialized = True

    def shutdown(self):
        self.ungrab_all_devices()
        try:
            if self.display:
                self.display.close()
        except Exception:
            pass
        self._initialized = False

    def fail_startup(self, message):
        print("Fenrir X11 driver error: " + message, file=sys.stderr)
        raise SystemExit(1)

    def resolve_window_id(self):
        configured_window = self.env["runtime"]["SettingsManager"].get_setting(
            "keyboard", "x11_window_id"
        )
        for candidate in [configured_window, os.environ.get("WINDOWID", "")]:
            window_id = self.parse_window_id(candidate)
            if window_id is not None:
                return window_id
        return self.get_active_window_id()

    def parse_window_id(self, value):
        if value is None:
            return None
        value = str(value).strip()
        if value == "":
            return None
        try:
            return int(value, 0)
        except ValueError:
            raise X11DriverError("Invalid X11 window id: " + value)

    def get_active_window_id(self):
        atom = self.display.intern_atom("_NET_ACTIVE_WINDOW")
        prop = self.root.get_full_property(atom, X.AnyPropertyType)
        if prop is None or len(prop.value) == 0:
            return None
        return int(prop.value[0])

    def format_window_id(self, window_id):
        if window_id is None:
            return "None"
        return hex(int(window_id))

    def find_num_lock_mask(self):
        num_lock_keysym = XK.string_to_keysym("Num_Lock")
        if not num_lock_keysym:
            return 0
        num_lock_keycode = self.display.keysym_to_keycode(num_lock_keysym)
        if not num_lock_keycode:
            return 0
        modifier_map = self.display.get_modifier_mapping()
        masks = [
            X.ShiftMask,
            X.LockMask,
            X.ControlMask,
            X.Mod1Mask,
            X.Mod2Mask,
            X.Mod3Mask,
            X.Mod4Mask,
            X.Mod5Mask,
        ]
        for index, keycodes in enumerate(modifier_map):
            if num_lock_keycode in keycodes:
                return masks[index]
        return 0

    def input_watchdog(self, active, event_queue):
        while active.value:
            try:
                self.refresh_grabs()
                if not self.display.pending_events():
                    time.sleep(0.01)
                    continue
                event = self.display.next_event()
                self.handle_x_event(event, event_queue)
            except BadWindow:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "x11Driver target window disappeared",
                    debug.DebugLevel.ERROR,
                )
                break
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "x11Driver input watchdog error: " + str(e),
                    debug.DebugLevel.ERROR,
                )
                time.sleep(0.1)

    def handle_x_event(self, event, event_queue):
        event_type = getattr(event, "type", None)
        if event_type == X.FocusIn:
            self.active = True
            self.clear_event_buffer()
            return
        if event_type == X.FocusOut:
            self.active = False
            self.reset_input_state()
            return
        if event_type not in [X.KeyPress, X.KeyRelease]:
            return
        if not self.active and not self.event_is_in_target_tree(event):
            return
        input_event = self.map_event(event)
        if input_event is None:
            return
        key_name = input_event["event_name"]
        if not self.should_emit_key(key_name):
            return
        self.write_debug(
            "x11Driver key event "
            + key_name
            + " state "
            + str(input_event["event_state"])
            + " raw state "
            + str(getattr(event, "state", 0)),
            debug.DebugLevel.INFO,
        )
        self.env["input"]["event_buffer"].append(input_event.copy())
        event_queue.put(
            {
                "Type": FenrirEventType.keyboard_input,
                "data": input_event,
            }
        )

    def event_is_in_target_tree(self, event):
        event_window = getattr(event, "event", None)
        if event_window is None:
            return False
        window_id = getattr(event_window, "id", None)
        if window_id is None:
            return False
        return window_id == self.window_id or self.window_has_ancestor(
            window_id, self.window_id
        )

    def window_has_ancestor(self, window_id, ancestor_id):
        try:
            window = self.display.create_resource_object("window", window_id)
            while window.id != self.root.id:
                if window.id == ancestor_id:
                    return True
                parent = window.query_tree().parent
                if parent is None or parent.id == window.id:
                    return False
                window = parent
        except Exception:
            return False
        return False

    def map_event(self, event):
        key_name = self.keycode_to_key_name(event.detail, event.state)
        if key_name is None:
            return None
        event_time = time.time()
        return {
            "event_name": key_name,
            "event_value": event.detail,
            "event_sec": int(event_time),
            "event_usec": int((event_time % 1) * 1000000),
            "event_state": 1 if event.type == X.KeyPress else 0,
            "event_type": event.type,
        }

    def keycode_to_key_name(self, keycode, state=0):
        key_names = []
        for keysym_name in self.keycode_to_keysym_names(keycode):
            key_name = self.keysym_name_to_key_name(keysym_name)
            if key_name is None or key_name in key_names:
                continue
            key_names.append(key_name)
        for key_name in key_names:
            if key_name.startswith("KEY_KP"):
                return key_name
        if key_names:
            return key_names[0]
        return None

    def keycode_to_keysym_names(self, keycode):
        names = []
        for column in range(8):
            keysym = self.display.keycode_to_keysym(keycode, column)
            keysym_name = self.keysym_to_name(keysym)
            if keysym_name and keysym_name not in names:
                names.append(keysym_name)
        return names

    def keysym_to_name(self, keysym):
        keysym_name = XK.keysym_to_string(keysym)
        if keysym_name:
            return keysym_name
        return KEYSYM_NAME_MAP.get(keysym)

    def keysym_name_to_key_name(self, keysym_name):
        if not keysym_name:
            return None
        if keysym_name in self.key_name_overrides:
            return self.key_name_overrides[keysym_name]
        if len(keysym_name) == 1:
            if keysym_name.isalpha():
                return "KEY_" + keysym_name.upper()
            if keysym_name.isdigit():
                return "KEY_" + keysym_name
        if keysym_name.startswith("F") and keysym_name[1:].isdigit():
            return "KEY_" + keysym_name
        return None

    def should_emit_key(self, key_name):
        return key_name is not None

    def refresh_interesting_keys(self):
        input_manager = self.env["runtime"]["InputManager"]
        self.fenrir_keys = set(self.env["input"]["fenrir_key"])
        interesting = set(self.fenrir_keys)
        interesting.update(self.env["input"]["script_key"])
        interesting.update(self.modifier_key_names)
        for shortcut in self.env.get("rawBindings", {}).values():
            for key_name in shortcut[1]:
                if key_name == "KEY_FENRIR":
                    interesting.update(self.fenrir_keys)
                elif key_name == "KEY_SCRIPT":
                    interesting.update(self.env["input"]["script_key"])
                else:
                    interesting.add(input_manager.convert_event_name(key_name))
                    interesting.add(key_name)
        self.interesting_keys = interesting

    def refresh_grabs(self, force=False):
        signature = (
            tuple(sorted(self.env.get("rawBindings", {}).keys())),
            tuple(sorted(self.env["input"]["fenrir_key"])),
            tuple(sorted(self.env["input"]["script_key"])),
        )
        if not force and signature == self.grab_signature:
            return
        self.ungrab_all_devices()
        self.refresh_interesting_keys()
        passive_grabs = self.build_passive_grabs()
        failed_before = self.failed_grabs
        for key_name, modifier_mask, include_num_lock in passive_grabs:
            self.grab_key_name(key_name, modifier_mask, include_num_lock)
        self.display.flush()
        self.grab_signature = signature
        self.write_debug(
            "x11Driver passive grabs planned "
            + str(len(passive_grabs))
            + ", installed "
            + str(len(self.grabbed))
            + ", failed "
            + str(self.failed_grabs - failed_before),
            debug.DebugLevel.INFO,
        )

    def build_passive_grabs(self):
        grabs = set()
        for fenrir_key in self.env["input"]["fenrir_key"]:
            grabs.add((fenrir_key, 0, True))
        for script_key in self.env["input"]["script_key"]:
            grabs.add((script_key, 0, True))
        grabs.add(("KEY_NUMLOCK", 0, True))
        for shortcut in self.env.get("rawBindings", {}).values():
            keys = shortcut[1]
            expanded_keys = self.expand_special_keys(keys)
            modifier_mask = self.shortcut_modifier_mask(expanded_keys)
            non_modifier_keys = [
                key for key in expanded_keys
                if key not in self.modifier_key_names
            ]
            if not non_modifier_keys:
                continue
            final_key = non_modifier_keys[-1]
            if "KEY_FENRIR" in keys:
                for fenrir_key in self.env["input"]["fenrir_key"]:
                    grabs.add((fenrir_key, modifier_mask, True))
                    fenrir_modifier_mask = self.modifier_masks.get(
                        fenrir_key, 0
                    )
                    if fenrir_modifier_mask:
                        grabs.add(
                            (
                                final_key,
                                modifier_mask | fenrir_modifier_mask,
                                not final_key.startswith("KEY_KP"),
                            )
                        )
            elif "KEY_SCRIPT" in keys:
                for script_key in self.env["input"]["script_key"]:
                    grabs.add((script_key, modifier_mask, True))
                    script_modifier_mask = self.modifier_masks.get(
                        script_key, 0
                    )
                    if script_modifier_mask:
                        grabs.add(
                            (
                                final_key,
                                modifier_mask | script_modifier_mask,
                                not final_key.startswith("KEY_KP"),
                            )
                        )
            else:
                grabs.add(
                    (
                        final_key,
                        modifier_mask,
                        not final_key.startswith("KEY_KP"),
                    )
                )
        return grabs

    def expand_special_keys(self, keys):
        expanded = []
        for key_name in keys:
            if key_name == "KEY_FENRIR":
                expanded.extend(self.env["input"]["fenrir_key"])
            elif key_name == "KEY_SCRIPT":
                expanded.extend(self.env["input"]["script_key"])
            else:
                expanded.append(key_name)
        return expanded

    def shortcut_modifier_mask(self, keys):
        modifier_mask = 0
        for key_name in keys:
            modifier_mask |= self.modifier_masks.get(key_name, 0)
        return modifier_mask

    def grab_key_name(
        self, key_name, modifier_mask=0, include_num_lock=True
    ):
        keysym_names = self.key_name_to_keysym_names(key_name)
        for keysym_name in keysym_names:
            keysym = XK.string_to_keysym(keysym_name)
            if not keysym:
                continue
            keycode = self.display.keysym_to_keycode(keysym)
            if not keycode:
                continue
            for effective_mask in self.optional_modifier_masks(
                modifier_mask, include_num_lock
            ):
                try:
                    self.window.grab_key(
                        keycode,
                        effective_mask,
                        False,
                        X.GrabModeAsync,
                        X.GrabModeAsync,
                    )
                    self.grabbed.add((keycode, effective_mask))
                except BadAccess:
                    self.failed_grabs += 1
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "x11Driver could not grab "
                        + key_name
                        + " with modifier mask "
                        + str(effective_mask),
                        debug.DebugLevel.WARNING,
                    )

    def optional_modifier_masks(self, modifier_mask, include_num_lock=True):
        optional_masks = [0, X.LockMask]
        if include_num_lock and self.num_lock_mask:
            optional_masks += [self.num_lock_mask, self.num_lock_mask | X.LockMask]
        return {modifier_mask | optional for optional in optional_masks}

    def key_name_to_keysym_names(self, key_name):
        reverse_map = {
            value: key for key, value in self.key_name_overrides.items()
        }
        aliases = {
            "KEY_META": ["Super_L", "Super_R", "Meta_L", "Meta_R"],
            "KEY_LEFTMETA": ["Super_L", "Meta_L"],
            "KEY_RIGHTMETA": ["Super_R", "Meta_R"],
            "KEY_COMPOSE": ["Multi_key"],
            "KEY_PAGEUP": ["Page_Up", "Prior"],
            "KEY_PAGEDOWN": ["Page_Down", "Next"],
            "KEY_KP0": ["KP_0", "KP_Insert"],
            "KEY_KP1": ["KP_1", "KP_End"],
            "KEY_KP2": ["KP_2", "KP_Down"],
            "KEY_KP3": ["KP_3", "KP_Page_Down"],
            "KEY_KP4": ["KP_4", "KP_Left"],
            "KEY_KP5": ["KP_5", "KP_Begin"],
            "KEY_KP6": ["KP_6", "KP_Right"],
            "KEY_KP7": ["KP_7", "KP_Home"],
            "KEY_KP8": ["KP_8", "KP_Up"],
            "KEY_KP9": ["KP_9", "KP_Page_Up"],
            "KEY_KPDOT": ["KP_Decimal", "KP_Delete"],
        }
        if key_name in aliases:
            return aliases[key_name]
        if key_name in reverse_map:
            return [reverse_map[key_name]]
        if key_name.startswith("KEY_F") and key_name[5:].isdigit():
            return [key_name[4:]]
        if key_name.startswith("KEY_") and len(key_name) == 5:
            return [key_name[4:].lower()]
        if key_name.startswith("KEY_") and key_name[4:].isdigit():
            return [key_name[4:]]
        return []

    def reset_input_state(self):
        try:
            self.env["runtime"]["InputManager"].reset_input_state()
        except Exception:
            self.clear_event_buffer()

    def write_event_buffer(self):
        self.clear_event_buffer()

    def clear_event_buffer(self):
        if not self._initialized:
            return
        del self.env["input"]["event_buffer"][:]

    def write_debug(self, message, level):
        try:
            self.env["runtime"]["DebugManager"].write_debug_out(message, level)
        except Exception:
            pass

    def update_input_devices(self, new_devices=None, init=False):
        return

    def grab_all_devices(self):
        return True

    def ungrab_all_devices(self):
        if not self.display or not self.window:
            return True
        for keycode, modifier_mask in list(self.grabbed):
            try:
                self.window.ungrab_key(keycode, modifier_mask)
            except Exception:
                pass
        self.grabbed.clear()
        try:
            self.display.flush()
        except Exception:
            pass
        return True

    def remove_all_devices(self):
        self.ungrab_all_devices()

    def get_led_state(self, led=0):
        try:
            pointer = self.root.query_pointer()
            mask = getattr(pointer, "mask", 0)
            if led == 0:
                return bool(self.num_lock_mask and mask & self.num_lock_mask)
            if led == 1:
                return bool(mask & X.LockMask)
        except Exception:
            pass
        return False

    def set_led_state(self, led_dict):
        return False
