"""Keyboard event handling and utility classes for Winston dialogs.

This module provides the base KeyHandler class that enables @KeyHandler.hotkey()
decorator functionality, along with utility classes for dialog management.

Key Classes:
    KeyHandler: Base class for keyboard event handling via decorator
    DialogException: Exception for dialog lifecycle control
    Selector: Text selection management for dialogs
    ElementTemplate: Template for lazy element construction

The @KeyHandler.hotkey() decorator requires actual inheritance from KeyHandler
to function correctly. All Dialog and Element classes inherit from KeyHandler.
"""

from __future__ import annotations
from typing import Any, Callable, TYPE_CHECKING
import inspect
from .framework import beep

if TYPE_CHECKING:
    from .dialog import Dialog


def get_commands(key_handler_object: Any) -> list[tuple[str, str]]:
    """Extract command methods from KeyHandler object.

    Finds all methods with names starting with "command_" that have an
    _associated_key attribute set by @KeyHandler.hotkey() decorator.

    Args:
        key_handler_object: Dialog or Element instance with command methods.

    Returns:
        List of (command_name, hotkey) tuples without "command_" prefix.
    """
    methods = inspect.getmembers(key_handler_object, predicate=inspect.ismethod)
    # Filter for methods with names of the form command_xxx
    command_methods = [(name, method) for name, method in methods if name.startswith("command_")]
    result = []
    for name, method in command_methods:
        # Check if the method has the _associated_key attribute
        if hasattr(method, "_associated_key"):
            result.append((name[8:], getattr(method, "_associated_key")))
    return result


class DialogException(Exception):
    """Exception thrown at end of inner_run loop for dialog lifecycle control.

    Raised when catching forced quit exceptions (BdbQuit or WinQuit) to allow
    setting an inner_run() return code that can be handled by run() method.
    """
    pass


class Selector:
    """Manages text selection within dialog elements.

    Tracks selection start/end positions (line and caret) for text editing
    operations. Supports marking mode and selection inversion.

    Attributes:
        dialog: Owner dialog containing the selected text.
        is_marking: True if in marking mode (persistent selection).
        start: Starting line index of selection.
        end: Ending line index of selection.
        start_caret: Caret position within start line.
        end_caret: Caret position within end line.
        at_end: True if cursor is at end of selection.
    """

    def __init__(self, owner: Dialog) -> None:
        """Initialize selector for given dialog.

        Args:
            owner: Dialog instance that owns this selector.
        """
        self.reset(0, 0)
        self.dialog = owner
        self.is_marking = False

    def line_count(self) -> int:
        """Count number of lines in selection.

        Returns:
            Number of lines selected, or 0 if selection is empty.
        """
        if self.is_empty():
            return 0
        else:
            return self.end - self.start + 1

    def is_empty(self) -> bool:
        """Check if selection is empty.

        Returns:
            True if selection has no content.
        """
        return (self.start == self.end) and (self.start_caret == self.end_caret)

    def reset(self, cursor: int, caret: int) -> None:
        """Reset selection to empty at given position.

        Args:
            cursor: Line index to reset to.
            caret: Caret position within line to reset to.

        Raises:
            ValueError: If cursor or caret is None.
        """
        if (cursor is None) or (caret is None):
            raise ValueError(f'reset of selection with cursor = {cursor} and caret = {caret}')
        self.start = self.end = cursor
        self.start_caret = self.end_caret = caret
        self.at_end = True

    def update(self, cursor: int | None = None, caret: int | None = None, shift: bool = False) -> None:
        """Update selection based on cursor movement.

        Args:
            cursor: New cursor line index.
            caret: New caret position.
            shift: True if shift key held (extend selection).
        """
        if not (shift or self.is_marking):
            if cursor is None or caret is None:
                return
            self.reset(cursor, caret)
        else:
            if self.at_end:
                self.end, self.end_caret = cursor, caret  # type: ignore[assignment]
            else:
                self.start, self.start_caret = cursor, caret  # type: ignore[assignment]
            if self.is_inverted():
                self.invert()

    def is_inverted(self) -> bool:
        """Check if selection end is before start.

        Returns:
            True if selection is inverted.
        """
        return (self.end < self.start) or ((self.end == self.start) and (self.end_caret < self.start_caret))

    def invert(self) -> None:
        """Swap start and end of selection."""
        _ = self.start, self.start_caret
        self.start, self.start_caret = self.end, self.end_caret
        self.end, self.end_caret = _
        self.at_end = not self.at_end

    def __len__(self) -> int:
        """Get length of selected text.

        Returns:
            Character count of selection.
        """
        return len(self.get())

    def get(self) -> str:
        """Get selected text content.

        Returns:
            Selected text joined with newlines.
        """
        fragments = []
        if not len(self.dialog):
            return ''
        for i in range(self.start, self.end + 1):
            start = 0 if (i != self.start) else self.start_caret
            end = len(self.dialog[i].value) if (i != self.end) else self.end_caret
            fragments.append(self.dialog[i].value[start:end])
        return '\n'.join(fragments)


class KeyHandler:
    """Base class for keyboard event handling via @KeyHandler.hotkey() decorator.

    Any class that implements @KeyHandler.hotkey(modified_name) should subclass
    KeyHandler. Dialog and Element are both subclasses of KeyHandler.

    The decorator associates methods with hotkeys, which are registered in the
    _key_methods dictionary. Dialog.inner_run() invokes handle_hotkey() which
    looks for a handler in the current element first, then the dialog.

    Important rules for overriding hotkey assignments:
        1. When overriding a method with an associated hotkey, apply the
           @KeyHandler.hotkey decorator to the overriding method.
        2. Applying a hotkey already defined on a superclass to a differently
           named method on a subclass has unpredictable results.
        3. Some base class methods apply hotkeys to wrappers, allowing override
           of the core method without respecifying the hotkey.
        4. Check for xxx_wrapper methods on superclasses to find wrappers.

    Attributes:
        _key_methods: Dictionary mapping hotkey strings to handler methods.
    """

    def __init__(self) -> None:
        """Initialize KeyHandler and register hotkey methods."""
        # Initialize the _key_methods dictionary for each instance (if not already initialized)
        if not hasattr(self, '_key_methods'):
            self._key_methods: dict[str, Callable[[str], Any]] = {}
        self.register_keys()

    @staticmethod
    def hotkey(key: str) -> Callable[[Callable], Callable]:
        """Decorator to associate a method with a hotkey.

        Args:
            key: Modified key name (e.g., "Control+S", "Alt+F4").

        Returns:
            Decorator function that sets _associated_key attribute on method.

        Example:
            @KeyHandler.hotkey("Control+S")
            def command_save(self, modified_name):
                # Save logic here
                pass
        """
        def decorator(func: Callable) -> Callable:
            func._associated_key = key  # type: ignore[attr-defined]
            return func
        return decorator

    def supports_undo(self) -> None:
        """Decorator marker for methods that support undo.

        Not yet implemented. Will wrap methods similar to @online_only or @block.
        """
        # TODO: implement wrapping like in @online_only or block
        pass

    def register_keys(self) -> None:
        """Register all methods associated with keys in _key_methods.

        Scans all methods on this instance for _associated_key attribute set
        by @KeyHandler.hotkey() decorator and adds them to _key_methods dict.

        Raises:
            ValueError: If method hotkey conflicts with element accelerator key.
        """
        for name in dir(self):
            method = getattr(self, name)
            if callable(method) and hasattr(method, "_associated_key"):
                # Raise exception if overwriting an accelerator key (only for Dialog instances)
                if (hasattr(self, '_accelerator_keys') and
                    method._associated_key in getattr(self, '_accelerator_keys', set())):
                    raise ValueError(
                        f"@KeyHandler.hotkey('{method._associated_key}') on {self.__class__.__name__}.{name}() "
                        f"conflicts with element accelerator key. Choose a different key for either the dialog method or the element accelerator."
                    )
                self._key_methods[method._associated_key] = method

    def no_method(self, modified_name: str) -> None:
        """Default handler for unrecognized hotkeys.

        Args:
            modified_name: The hotkey that was pressed but has no handler.
        """
        beep()
        return None


class ElementTemplate:
    """Template for lazy construction of dialog elements.

    Stores element type, name, value, and additional kwargs for later
    instantiation by FormDialog.generate_content().

    Attributes:
        type: Element class (e.g., TextElement, ListElement).
        name: Element name identifier.
        value: Initial value for element.
        kwargs: Additional keyword arguments for element constructor.
    """

    def __init__(self, e_type: type, e_name: str, e_value: Any, **kwargs: Any) -> None:
        """Initialize element template.

        Args:
            e_type: Element class to instantiate.
            e_name: Name identifier for the element.
            e_value: Initial value for the element.
            **kwargs: Additional arguments passed to element constructor.
        """
        self.type = e_type
        self.name = e_name
        self.value = e_value
        self.kwargs = kwargs
