"""Form element classes for Winston FormDialogs.

This module provides specialized element types for form-based dialogs including
text input, lists, buttons, booleans, and numbers.

Key Classes:
    FormElement: Base class for form elements
    TextElement: Single or multi-line text input
    SingleTextElement: Single-line text input with clipboard support
    ListElement: Dropdown-style list selection
    ButtonElement: Clickable button with on_press callback
    BooleanElement: Two-value toggle (Yes/No)
    NumberElement: Numeric input with validation
    InputPromptElement: Text input with Return key dispatch (for InputPromptDialog)

All form elements support tab navigation within FormDialog.

Note:
    InputPromptElement is used by InputPromptDialog (defined in subdialogs.py)
    to provide command-line style input with history navigation.
"""

from __future__ import annotations
from typing import Any, TYPE_CHECKING
from collections.abc import Callable, Sequence
import sys
import re
import pyperclip  # type: ignore[import]
from .elements import Element, ValuesElement
from .key_handler import KeyHandler
from . import punctuator
from . import framework
from .framework import beep

if TYPE_CHECKING:
    from .dialog import Dialog

class FormElement(Element):
    """Base class for form input elements.

    Provides common functionality for form elements including validation,
    focus callbacks, and keyboard accelerators.

    Attributes:
        on_focus: Optional callback invoked when element receives focus.
        validate: Optional validation function for element value.
        accelerator: Optional keyboard shortcut (e.g., "F5", "Delete").
    """

    def __init__(
        self,
        owner: Dialog,
        name: str,
        value: Any,
        on_focus: Callable[[FormElement], None] | None = None,
        validate: Callable[[Any], bool] | None = None,
        accelerator: str | None = None
    ) -> None:
        """Initialize form element.

        Args:
            owner: Parent dialog containing this element.
            name: Unique element identifier.
            value: Initial element value.
            on_focus: Callback when element gains focus.
            validate: Function to validate element value.
            accelerator: Keyboard shortcut for this element.
        """
        super().__init__(owner, name, value)
        self.on_focus = on_focus
        self.validate = validate
        self.accelerator = accelerator

    def char_sink(self, char: str) -> None:
        """Handle character input.

        Default implementation beeps to indicate input not accepted.
        Subclasses override to implement text input.

        Args:
            char: Single character from keyboard input.
        """
        beep()
class LabelElement(FormElement):
    """Read-only text label in a form.

    Displays static text without accepting input.
    """
    pass


class ListElement(FormElement, ValuesElement):
    """Dropdown-style list selection element.

    Combines FormElement and ValuesElement to provide list selection behavior
    similar to Windows ListBox. Supports keyboard navigation and type-ahead search.

    Attributes:
        index: Current selection index in values list.
        values: List of selectable items.
        content_cookie: Optional cookie passed to generate_content for dynamic lists.
    """

    def __init__(
        self,
        owner: Dialog,
        name: str,
        default: str = '',
        values: Sequence[str] = ('Yes', 'No', 'cancel'),
        on_focus: Callable[[FormElement], None] | None = None,
        content_cookie: Any = None,
        accelerator: str | None = None,
        **kwargs: Any
    ) -> None:
        """Initialize list element.

        Args:
            owner: Parent dialog containing this element.
            name: Unique element identifier.
            default: Initially selected value.
            values: List of selectable items.
            on_focus: Callback when element gains focus.
            content_cookie: Cookie for dynamic content generation.
            accelerator: Keyboard shortcut for this element.
            **kwargs: Additional arguments (for future expansion).
        """
        FormElement.__init__(self, owner, name, default, on_focus=on_focus, accelerator=accelerator)
        ValuesElement.__init__(self, owner, name, default, values=list(values), content_cookie=content_cookie)
    def char_sink(self, char: str) -> None:
        """Type-ahead search for matching list item.

        Args:
            char: Character to search for at start of items.
        """
        self.set_from_value(char, match_case=False, match_type='startswith')

    def set_from_value(
        self,
        match_string: str,
        match_case: bool = False,
        match_type: str = 'startswith',
        interrupt: bool = False
    ) -> None:
        """Search for and select matching item in list.

        Searches from current position forward, wrapping to beginning.
        Beeps if no match found.

        Args:
            match_string: String to search for.
            match_case: Whether search is case-sensitive.
            match_type: Type of match ('startswith', 'contains', etc).
            interrupt: Whether to interrupt speech during rotation.
        """
        matches = [e for e in self.values[self.index+1:] + self.values[:self.index+1]
                   if framework.test_match(e, match_string, match_case, match_type)]
        if len(matches):
            index = self.values.index(matches[0])
            self.rotate(index - self.index, wrap=False, interrupt=interrupt)
        else:
            beep()

    @KeyHandler.hotkey("Up")
    def previous_value(self, modified_name: str) -> None:
        """Move to previous item in list."""
        self.rotate(-1)

    @KeyHandler.hotkey("Down")
    def next_value(self, modified_name: str) -> None:
        """Move to next item in list."""
        self.rotate(1)

    @KeyHandler.hotkey("Control+End")
    def bottom(self, modified_name: str) -> None:
        """Jump to last item in list."""
        self.rotate(len(self.values) - 1 - self.index)

    @KeyHandler.hotkey("Control+Home")
    def top(self, modified_name: str) -> None:
        """Jump to first item in list."""
        self.rotate(-self.index)

    @KeyHandler.hotkey("End")
    def end(self, modified_name: str) -> None:
        """Restore normal End key behavior (not ValuesElement behavior)."""
        Element.end(self, modified_name)

    @KeyHandler.hotkey("Home")
    def home(self, modified_name: str) -> None:
        """Restore normal Home key behavior (not ValuesElement behavior)."""
        Element.home(self, modified_name)
class BooleanElement(ListElement):
    """Two-value toggle element (Yes/No, True/False, etc).

    Specialized ListElement constrained to exactly two values.

    Raises:
        ValueError: If values sequence doesn't have exactly 2 items.
    """

    def __init__(
        self,
        owner: Dialog,
        name: str,
        default: str = 'Yes',
        values: Sequence[str] = ('Yes', 'No'),
        on_focus: Callable[[FormElement], None] | None = None
    ) -> None:
        """Initialize boolean element.

        Args:
            owner: Parent dialog containing this element.
            name: Unique element identifier.
            default: Initially selected value (must be in values).
            values: Tuple of exactly two values to toggle between.
            on_focus: Callback when element gains focus.

        Raises:
            ValueError: If values doesn't contain exactly 2 items.
        """
        if len(values) != 2:
            raise ValueError(f'boolean Element can only have 2 possible values, {len(values)} provided')
        super().__init__(owner, name, default, values=values, on_focus=on_focus)


class ButtonElement(FormElement):
    """Clickable button element with callback.

    Pressing Return activates the button, calling on_press callback if provided.
    If no callback, activates parent dialog's OK handler.

    Attributes:
        on_press: Optional callback receiving button instance when pressed.
        empty_value: Display string for empty button (always '').
    """

    def __init__(
        self,
        owner: Dialog,
        name: str,
        on_press: Callable[[ButtonElement], None] | None = None,
        on_focus: Callable[[FormElement], None] | None = None,
        accelerator: str | None = None
    ) -> None:
        """Initialize button element.

        Args:
            owner: Parent dialog containing this element.
            name: Unique element identifier (also used as button label).
            on_press: Callback when button pressed, receives button instance.
            on_focus: Callback when element gains focus.
            accelerator: Keyboard shortcut for this button.
        """
        super().__init__(owner, name, value='', on_focus=on_focus, accelerator=accelerator)
        if on_press is not None:
            self.on_press = on_press
        self.empty_value = ''

    @KeyHandler.hotkey("Return")
    def command_press(self, modified_name: str) -> Any:
        """Activate button via Return key.

        If button has on_press callback, executes it and keeps dialog open.
        Otherwise, invokes parent dialog's OK handler which closes dialog.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            None if on_press executed, otherwise dialog.ok() result.
        """
        if hasattr(self, 'on_press'):
            self.on_press(self)
        else:
            return self.dialog.ok(modified_name)
class TextElement(FormElement):
    """Multi-line or single-line text input element.

    Provides text editing functionality with caret positioning, selection,
    and speech feedback. Supports word completion feedback and dirty tracking.

    Attributes:
        block: Optional block mode setting.
        empty_value: Speech text when element is empty (default "blank line").
    """

    def __init__(
        self,
        owner: Dialog,
        name: str,
        value: str,
        on_focus: Callable[[FormElement], None] | None = None,
        validate: Callable[[Any], bool] | None = None,
        block: Any = None,
        empty_value: str | None = None,
        accelerator: str | None = None
    ) -> None:
        """Initialize text element.

        Args:
            owner: Parent dialog containing this element.
            name: Unique element identifier.
            value: Initial text content.
            on_focus: Callback when element gains focus.
            validate: Function to validate text value.
            block: Optional block mode configuration.
            empty_value: Speech text for empty element (default from dialog).
            accelerator: Keyboard shortcut for this element.
        """
        super().__init__(owner, name, value, on_focus=on_focus, validate=validate, accelerator=accelerator)
        self.block = block
        if empty_value is None:
            empty_value = getattr(self.dialog, 'empty_value', 'blank line')
        self.empty_value = empty_value

    def is_splittable(self) -> bool:
        """Check if element can be split across multiple lines.

        Returns:
            True for multi-line text elements.
        """
        return True

    def char_sink(self, char: str) -> None:
        """Insert character at current caret position.

        Delegates to dialog.insert_text which handles selection excision.

        Args:
            char: Single character from keyboard input.
        """
        self.dialog.insert_text(char)

    def insert(self, s: str) -> None:
        """Insert string at caret position with speech feedback.

        Args:
            s: String to insert (may be single char or multiple).
        """
        if hasattr(self.dialog, 'set_dirty'):
            self.dialog.set_dirty(True)
        self.value = self.value[:self.caret] + s + self.value[self.caret:]
        if (len(s) == 1) and (not (self.previous_char().isalnum() and not s.isalnum())):
            self.say_current_char()
        else:
            completed_word = self.on_word_completion()
            self.dialog.say_strings(completed_word + s, substitutions=punctuator.all)
        self.caret += len(s)

    def on_word_completion(self) -> str:
        """Get completed word for speech feedback.

        Returns:
            The previous word when inserting non-alphanumeric character.
        """
        return self.previous_word()

    def excise_char(self, backwards: bool = False) -> None:
        """Delete character at or before caret.

        Args:
            backwards: If True, delete before caret; if False, delete at caret.
        """
        if hasattr(self.dialog, 'set_dirty'):
            self.dialog.set_dirty(True)
        if not self.caret == len(self.value):
            if backwards:
                self.say_current_char(action='')
            self.value = self.value[:self.caret] + self.value[self.caret + 1:]
            if not backwards:
                self.say_current_char(action='')
        else:
            if self.dialog.can_wrap_forward():
                self.dialog.join_next()  # type: ignore[attr-defined]
            else:
                beep()

    @KeyHandler.hotkey("Back")
    def backspace(self, modified_name: str) -> None:
        """Delete character before caret.

        Args:
            modified_name: Full key name including modifiers.
        """
        self.delete(modified_name)

    @KeyHandler.hotkey("Delete")
    def delete(self, modified_name: str) -> None:
        """Delete character at caret, or entire line if Shift+Delete.

        Args:
            modified_name: Full key name including modifiers.
        """
        if 'Shift' in modified_name:
            # Delete entire line
            dialog = self.dialog
            if len(dialog) > 1:
                del dialog[dialog.cursor]  # type: ignore[has-type]
                dialog.cursor = min(dialog.cursor, len(dialog) - 1)  # type: ignore[has-type]
                dialog.say_current()
            else:
                dialog.say_strings('cannot delete the only line in a file')
        else:
            # Delete character or selection
            if not self.dialog.selection.is_empty():
                self.dialog.excise_selection()
            else:
                backwards = modified_name.endswith('Back')
                if backwards:
                    if not (moved := self.dialog.move(self.dialog.cursor, self.caret - 1,  # type: ignore[has-type]
                                                      modified_name.startswith('Shift'), caret_only=True)):
                        return
                self.dialog.current().excise_char(backwards=backwards)  # type: ignore[attr-defined]
class SingleTextElement(TextElement):
    """Single-line text input element.

    Prevents line splitting and provides clipboard operations (cut/copy/paste).
    Used for single-line input fields in forms.
    """

    def is_splittable(self) -> bool:
        """Single-line elements cannot be split.

        Returns:
            Always False.
        """
        return False

    @KeyHandler.hotkey("Control+X")
    def command_cut(self, modified_name: str) -> None:
        """Cut selected text to clipboard.

        Args:
            modified_name: Full key name including modifiers.
        """
        self.dialog.command_copy('Control+C')
        s = self.dialog.selection
        excision = self.value[s.start_caret:s.end_caret]
        self.value = self.value[:s.start_caret] + self.value[s.end_caret:]
        self.dialog.say_short_action(excision, 'cut')

    @KeyHandler.hotkey("Control+V")
    def command_paste(self, modified_name: str) -> None:
        """Paste text from clipboard.

        Args:
            modified_name: Full key name including modifiers.
        """
        s = str(pyperclip.paste())
        if self.dialog.insert_text(s):
            self.dialog.say_short_action(s, 'inserted')

    @KeyHandler.hotkey("Control+A")
    def command_select_all(self, modified_name: str, silent: bool = False) -> None:
        """Select all text in element.

        Args:
            modified_name: Full key name including modifiers.
            silent: If True, suppress speech feedback.
        """
        self.home(modified_name, silent=True)
        self.end('Shift+' + modified_name, silent=True)
        if not silent:
            self.dialog.say_short_action(self.value, 'selected')

    def initialize_value(self, val: str) -> None:
        """Reset element value and caret position.

        Args:
            val: New value for element.
        """
        self.value = val
        self.caret = 0
        # Clear selection if this element was selected
        if self.dialog.selection.start == self.dialog.index(self):
            self.dialog.selection.reset(0, 0)
class PermeableTextElement(SingleTextElement):
    """Single-line text element that allows arrow key navigation to adjacent fields.

    When placed at top of form, Down arrow moves to next field.
    When placed elsewhere, Up arrow moves to previous field.
    Provides convenient keyboard navigation for single-field forms.
    """

    @KeyHandler.hotkey("Up")
    def escape_upwards(self, modified_name: str) -> Any:
        """Move to previous field if not at top.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result of move_focus if successful, None otherwise.
        """
        if self.dialog.index(self) != 0:
            return self.dialog.move_focus("Shift+Tab")  # type: ignore[attr-defined]
        else:
            beep()

    @KeyHandler.hotkey("Down")
    def escape_downwards(self, modified_name: str) -> Any:
        """Move to next field if at top.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result of move_focus if successful, None otherwise.
        """
        if self.dialog.index(self) == 0:
            return self.dialog.move_focus("Tab")  # type: ignore[attr-defined]
        else:
            beep()
class NumberElement(FormElement):
    """Numeric input element with range validation.

    Provides arrow key increment/decrement and digit-by-digit entry.
    Validates input against min/max range and speaks digits individually.

    Attributes:
        min: Minimum allowed value.
        max: Maximum allowed value.
        is_selected: Whether entire number is selected (for replacement).
    """

    def __init__(
        self,
        owner: Dialog,
        name: str,
        value: str | int | None = '0',
        min: int = 0,
        max: int = sys.maxsize,
        on_focus: Callable[[FormElement], None] | None = None
    ) -> None:
        """Initialize number element.

        Args:
            owner: Parent dialog containing this element.
            name: Unique element identifier.
            value: Initial numeric value (string, int, or None for empty).
            min: Minimum allowed value (inclusive).
            max: Maximum allowed value (inclusive).
            on_focus: Callback when element gains focus.
        """
        super().__init__(owner, name, str(value) if value is not None else '', on_focus=on_focus)
        self.min = min
        self.max = max
        self.value = str("" if value is None else value)
        self.is_selected = False

    def increment(self, amount: int, shift: bool, silent: bool = False) -> None:
        """Increment or decrement value by amount.

        Args:
            amount: Delta to add to current value (negative to decrement).
            shift: Whether Shift key is pressed (currently unused).
            silent: If True, suppress speech feedback.
        """
        self.is_selected = False
        if (amount < 0) and (self.value == ''):
            self.value = str(self.min)
        elif ((amount > 0) and (self.value_as_int() == self.max)) or \
             ((amount < 0) and (self.value_as_int() == self.min)):
            beep()
        else:
            self.value = str(self.value_as_int() + amount)
        if not silent:
            self.dialog.say_strings(list(self.value))

    def value_as_int(self) -> int:
        """Get current value as integer.

        Returns:
            Current value as int, or min if value is empty.
        """
        return self.min if self.value == '' else int(self.value)

    @KeyHandler.hotkey("Right")
    def right(self, modified_name: str) -> None:
        """Increment value by 1."""
        self.increment(1, modified_name.startswith('Shift'))

    @KeyHandler.hotkey("Left")
    def left(self, modified_name: str) -> None:
        """Decrement value by 1."""
        self.increment(-1, modified_name.startswith('Shift'))

    @KeyHandler.hotkey("Home")
    def home(self, modified_name: str, silent: bool = False) -> None:
        """Jump to minimum value.

        Args:
            modified_name: Full key name including modifiers.
            silent: If True, suppress speech feedback.
        """
        self.increment(self.min - self.value_as_int(), modified_name.startswith('Shift'), silent=silent)

    @KeyHandler.hotkey("End")
    def end(self, modified_name: str, silent: bool = False) -> None:
        """Jump to maximum value.

        Args:
            modified_name: Full key name including modifiers.
            silent: If True, suppress speech feedback.
        """
        self.increment(self.max - self.value_as_int(), modified_name.startswith('Shift'), silent=silent)

    def char_sink(self, char: str) -> None:
        """Handle digit input with range validation.

        Args:
            char: Single character (must be digit).
        """
        utterance = ""
        if not char.isdigit():
            beep()
            utterance = f'{char} is an invalid character: must be a digit'
        else:
            if self.is_selected:
                self.value = ''
                self.is_selected = False
            i = int(self.value + char)
            if (i > self.max) or (i < self.min):
                beep()
                utterance = f'{i} is outside range {self.min} to {self.max}'
            else:
                self.value = str(i)
                utterance = self.value[-1]
        self.dialog.say_strings(utterance)

    @KeyHandler.hotkey("Delete")
    def delete(self, modified_name: str) -> None:
        """Clear value to empty string.

        Args:
            modified_name: Full key name including modifiers.
        """
        self.value = ""
        self.is_selected = False
        self.dialog.say_current()

    @KeyHandler.hotkey("Back")
    def backspace(self, modified_name: str) -> None:
        """Delete last digit, or clear if selected.

        Args:
            modified_name: Full key name including modifiers.
        """
        if (not self.is_selected) and len(self.value) > 1:
            self.value = self.value[:-1]
        else:
            self.value = ''
            self.is_selected = False
        self.dialog.say_current()

    @KeyHandler.hotkey("Control+A")
    def command_select_all(self, modified_name: str, silent: bool = False) -> None:
        """Select entire number for replacement.

        Args:
            modified_name: Full key name including modifiers.
            silent: If True, suppress speech feedback.
        """
        self.is_selected = True
        if not silent:
            self.dialog.say_short_action(str(self), 'selected')
class InputPromptElement(SingleTextElement):
    """Text input element with history for InputPromptDialog.

    Provides Return key handling to dispatch input to parent dialog
    and clear the input field.

    Attributes:
        empty_value: Speech text when empty ("input prompt").
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize input prompt element.

        Args:
            *args: Positional arguments passed to SingleTextElement.
            **kwargs: Keyword arguments passed to SingleTextElement.
        """
        super().__init__(*args, **kwargs)
        self.empty_value = 'input prompt'

    def clear_input(self) -> None:
        """Clear input field and reset caret to start."""
        self.caret = 0
        self.value = ""

    @KeyHandler.hotkey("Return")
    def dispatch_input(self, modified_name: str) -> None:
        """Dispatch input to dialog and clear field.

        Calls dialog.accept_input() which ingests input and adds to history.
        Override ingest_input() in dialog subclass for custom handling.

        Args:
            modified_name: Full key name including modifiers.
        """
        self.dialog.accept_input(self.value)  # type: ignore[attr-defined]
        self.clear_input()
        self.dialog.say_current()
        
class SurrogateTextElement(InputPromptElement):
    """Input  element whose value is proxied to parent dialog's current element.

    Used for autocomplete dialogs where input line modifies value in parent.
    Value attribute reads/writes to self.dialog.parent.current().value.
    """

    @property
    def value(self) -> str:
        """Get value from parent dialog's current element.

        Returns:
            Value of parent's current element.
        """
        return self.dialog.parent.current().value  # type: ignore[union-attr]

    @value.setter
    def value(self, val: str) -> None:
        """Set value on parent dialog's current element.

        Args:
            val: New value to set on parent's element.
        """
        self.dialog.parent.current().value = val  # type: ignore[union-attr]    
