"""

Builtin events sent by Textual.

Events may be marked as "Bubbles" and "Verbose".
See the [events guide](/guide/events/#bubbling) for an explanation of bubbling.
Verbose events are excluded from the textual console, unless you explicitly request them with the `-v` switch as follows:

```
textual console -v
```
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Type, TypeVar

import rich.repr
from rich.style import Style
from typing_extensions import Self

from textual._types import CallbackType
from textual.geometry import Offset, Size
from textual.keys import _get_key_aliases
from textual.message import Message

MouseEventT = TypeVar("MouseEventT", bound="MouseEvent")

if TYPE_CHECKING:
    from textual.dom import DOMNode
    from textual.timer import Timer as TimerClass
    from textual.timer import TimerCallback
    from textual.widget import Widget


@rich.repr.auto
class Event(Message):
    """The base class for all events."""


@rich.repr.auto
class Callback(Event, bubble=False, verbose=True):
    """Sent by Textual to invoke a callback
    (see [call_next][textual.message_pump.MessagePump.call_next] and
    [call_later][textual.message_pump.MessagePump.call_later]).
    """

    def __init__(self, callback: CallbackType) -> None:
        self.callback = callback
        super().__init__()

    def __rich_repr__(self) -> rich.repr.Result:
        yield "callback", self.callback


@dataclass
class CursorPosition(Event, bubble=False):
    """Internal event used to retrieve the terminal's cursor position."""

    x: int
    y: int


class Load(Event, bubble=False):
    """
    Sent when the App is running but *before* the terminal is in application mode.

    Use this event to run any setup that doesn't require any visuals such as loading
    configuration and binding keys.

    - [ ] Bubbles
    - [ ] Verbose
    """


class Idle(Event, bubble=False):
    """Sent when there are no more items in the message queue.

    This is a pseudo-event in that it is created by the Textual system and doesn't go
    through the usual message queue.

    - [ ] Bubbles
    - [ ] Verbose
    """


class Action(Event):
    __slots__ = ["action"]

    def __init__(self, action: str) -> None:
        super().__init__()
        self.action = action

    def __rich_repr__(self) -> rich.repr.Result:
        yield "action", self.action


class Resize(Event, bubble=False):
    """Sent when the app or widget has been resized.

    - [ ] Bubbles
    - [ ] Verbose

    Args:
        size: The new size of the Widget.
        virtual_size: The virtual size (scrollable size) of the Widget.
        container_size: The size of the Widget's container widget.
    """

    __slots__ = ["size", "virtual_size", "container_size"]

    def __init__(
        self,
        size: Size,
        virtual_size: Size,
        container_size: Size | None = None,
        pixel_size: Size | None = None,
    ) -> None:
        self.size = size
        """The new size of the Widget."""
        self.virtual_size = virtual_size
        """The virtual size (scrollable size) of the Widget."""
        self.container_size = size if container_size is None else container_size
        """The size of the Widget's container widget."""
        self.pixel_size = pixel_size
        """Size of terminal window in pixels if known, or `None` if not known."""
        super().__init__()

    @classmethod
    def from_dimensions(
        cls, cells: tuple[int, int], pixels: tuple[int, int] | None
    ) -> Resize:
        """Construct from basic dimensions.

        Args:
            cells: tuple of (<width>, <height>) in cells.
            pixels: tuple of (<width>, <height>) in pixels if known, or `None` if not known.

        """
        size = Size(*cells)
        pixel_size = Size(*pixels) if pixels is not None else None
        return Resize(size, size, size, pixel_size)

    def can_replace(self, message: "Message") -> bool:
        return isinstance(message, Resize)

    def __rich_repr__(self) -> rich.repr.Result:
        yield "size", self.size
        yield "virtual_size", self.virtual_size, self.size
        yield "container_size", self.container_size, self.size
        yield "pixel_size", self.pixel_size, None


class Compose(Event, bubble=False, verbose=True):
    """Sent to a widget to request it to compose and mount children.

    This event is used internally by Textual.
    You won't typically need to explicitly handle it,

    - [ ] Bubbles
    - [X] Verbose
    """


class Mount(Event, bubble=False, verbose=False):
    """Sent when a widget is *mounted* and may receive messages.

    - [ ] Bubbles
    - [ ] Verbose
    """


class Unmount(Event, bubble=False, verbose=False):
    """Sent when a widget is unmounted and may no longer receive messages.

    - [ ] Bubbles
    - [ ] Verbose
    """


class Show(Event, bubble=False):
    """Sent when a widget is first displayed.

    - [ ] Bubbles
    - [ ] Verbose
    """


class Hide(Event, bubble=False):
    """Sent when a widget has been hidden.

    - [ ] Bubbles
    - [ ] Verbose

    Sent when any of the following conditions apply:

    - The widget is removed from the DOM.
    - The widget is no longer displayed because it has been scrolled or clipped from the terminal or its container.
    - The widget has its `display` attribute set to `False`.
    - The widget's `display` style is set to `"none"`.
    """


class Ready(Event, bubble=False):
    """Sent to the `App` when the DOM is ready and the first frame has been displayed.

    - [ ] Bubbles
    - [ ] Verbose
    """


@rich.repr.auto
class MouseCapture(Event, bubble=False):
    """Sent when the mouse has been captured.

    - [ ] Bubbles
    - [ ] Verbose

    When a mouse has been captured, all further mouse events will be sent to the capturing widget.

    Args:
        mouse_position: The position of the mouse when captured.
    """

    def __init__(self, mouse_position: Offset) -> None:
        super().__init__()
        self.mouse_position = mouse_position
        """The position of the mouse when captured."""

    def __rich_repr__(self) -> rich.repr.Result:
        yield None, self.mouse_position


@rich.repr.auto
class MouseRelease(Event, bubble=False):
    """Mouse has been released.

    - [ ] Bubbles
    - [ ] Verbose

    Args:
        mouse_position: The position of the mouse when released.
    """

    def __init__(self, mouse_position: Offset) -> None:
        super().__init__()
        self.mouse_position = mouse_position
        """The position of the mouse when released."""

    def __rich_repr__(self) -> rich.repr.Result:
        yield None, self.mouse_position


class InputEvent(Event):
    """Base class for input events."""


@rich.repr.auto
class Key(InputEvent):
    """Sent when the user hits a key on the keyboard.

    - [X] Bubbles
    - [ ] Verbose

    Args:
        key: The key that was pressed.
        character: A printable character or `None` if it is not printable.
    """

    __slots__ = ["key", "character", "aliases"]

    def __init__(self, key: str, character: str | None) -> None:
        super().__init__()
        self.key = key
        """The key that was pressed."""
        self.character = (
            (key if len(key) == 1 else None) if character is None else character
        )
        """A printable character or ``None`` if it is not printable."""
        self.aliases: list[str] = _get_key_aliases(key)
        """The aliases for the key, including the key itself."""

    def __rich_repr__(self) -> rich.repr.Result:
        yield "key", self.key
        yield "character", self.character
        yield "name", self.name
        yield "is_printable", self.is_printable
        yield "aliases", self.aliases, [self.key]

    @property
    def name(self) -> str:
        """Name of a key suitable for use as a Python identifier."""
        return _key_to_identifier(self.key).lower()

    @property
    def name_aliases(self) -> list[str]:
        """The corresponding name for every alias in `aliases` list."""
        return [_key_to_identifier(key) for key in self.aliases]

    @property
    def is_printable(self) -> bool:
        """Check if the key is printable (produces a unicode character).

        Returns:
            `True` if the key is printable.
        """
        return False if self.character is None else self.character.isprintable()


def _key_to_identifier(key: str) -> str:
    """Convert the key string to a name suitable for use as a Python identifier."""
    key_no_modifiers = key.split("+")[-1]
    if len(key_no_modifiers) == 1 and key_no_modifiers.isupper():
        if "+" in key:
            key = f"{key.rpartition('+')[0]}+upper_{key_no_modifiers}"
        else:
            key = f"upper_{key_no_modifiers}"
    return key.replace("+", "_").lower()


@rich.repr.auto
class MouseEvent(InputEvent, bubble=True):
    """Sent in response to a mouse event.

    - [X] Bubbles
    - [ ] Verbose

    Args:
        widget: The widget under the mouse.
        x: The relative x coordinate.
        y: The relative y coordinate.
        delta_x: Change in x since the last message.
        delta_y: Change in y since the last message.
        button: Indexed of the pressed button.
        shift: True if the shift key is pressed.
        meta: True if the meta key is pressed.
        ctrl: True if the ctrl key is pressed.
        screen_x: The absolute x coordinate.
        screen_y: The absolute y coordinate.
        style: The Rich Style under the mouse cursor.
    """

    __slots__ = [
        "widget",
        "_x",
        "_y",
        "_delta_x",
        "_delta_y",
        "button",
        "shift",
        "meta",
        "ctrl",
        "_screen_x",
        "_screen_y",
        "_style",
    ]

    def __init__(
        self,
        widget: Widget | None,
        x: float,
        y: float,
        delta_x: int,
        delta_y: int,
        button: int,
        shift: bool,
        meta: bool,
        ctrl: bool,
        screen_x: float | None = None,
        screen_y: float | None = None,
        style: Style | None = None,
    ) -> None:
        super().__init__()
        self.widget: Widget | None = widget
        """The widget under the mouse at the time of a click."""
        self._x = x
        """The relative x coordinate."""
        self._y = y
        """The relative y coordinate."""
        self._delta_x = delta_x
        """Change in x since the last message."""
        self._delta_y = delta_y
        """Change in y since the last message."""
        self.button = button
        """Indexed of the pressed button."""
        self.shift = shift
        """`True` if the shift key is pressed."""
        self.meta = meta
        """`True` if the meta key is pressed."""
        self.ctrl = ctrl
        """`True` if the ctrl key is pressed."""
        self._screen_x = x if screen_x is None else screen_x
        """The absolute x coordinate."""
        self._screen_y = y if screen_y is None else screen_y
        """The absolute y coordinate."""
        self._style = style or Style()

    @property
    def x(self) -> int:
        """The relative X coordinate of the cell under the mouse."""
        return int(self._x)

    @property
    def y(self) -> int:
        """The relative Y coordinate of the cell under the mouse."""
        return int(self._y)

    @property
    def delta_x(self) -> int:
        """Change in `x` since last message."""
        return self._delta_x

    @property
    def delta_y(self) -> int:
        """Change in `y` since the last message."""
        return self._delta_y

    @property
    def screen_x(self) -> int:
        """X coordinate of the cell relative to top left of screen."""
        return int(self._screen_x)

    @property
    def screen_y(self) -> int:
        """Y coordinate of the cell relative to top left of screen."""
        return int(self._screen_y)

    @property
    def pointer_x(self) -> float:
        """The relative X coordinate of the pointer."""
        return self._x

    @property
    def pointer_y(self) -> float:
        """The relative Y coordinate of the pointer."""
        return self._y

    @property
    def pointer_screen_x(self) -> float:
        """The X coordinate of the pointer relative to the screen."""
        return self._screen_x

    @property
    def pointer_screen_y(self) -> float:
        """The Y coordinate of the pointer relative to the screen."""
        return self._screen_y

    @classmethod
    def from_event(
        cls: Type[MouseEventT], widget: Widget, event: MouseEvent
    ) -> MouseEventT:
        new_event = cls(
            widget,
            event._x,
            event._y,
            event._delta_x,
            event._delta_y,
            event.button,
            event.shift,
            event.meta,
            event.ctrl,
            event._screen_x,
            event._screen_y,
            event._style,
        )
        return new_event

    def __rich_repr__(self) -> rich.repr.Result:
        yield self.widget
        yield "x", self.x
        yield "y", self.y
        yield "pointer_x", self.pointer_x
        yield "pointer_y", self.pointer_y
        yield "delta_x", self.delta_x, 0
        yield "delta_y", self.delta_y, 0
        if self.screen_x != self.x:
            yield "screen_x", self._screen_x
        if self.screen_y != self.y:
            yield "screen_y", self._screen_y
        yield "button", self.button, 0
        yield "shift", self.shift, False
        yield "meta", self.meta, False
        yield "ctrl", self.ctrl, False
        if self.style:
            yield "style", self.style

    @property
    def control(self) -> Widget | None:
        return self.widget

    @property
    def offset(self) -> Offset:
        """The mouse coordinate as an offset.

        Returns:
            Mouse coordinate.
        """
        return Offset(self.x, self.y)

    @property
    def screen_offset(self) -> Offset:
        """Mouse coordinate relative to the screen."""
        return Offset(self.screen_x, self.screen_y)

    @property
    def delta(self) -> Offset:
        """Mouse coordinate delta (change since last event)."""
        return Offset(self.delta_x, self.delta_y)

    @property
    def style(self) -> Style:
        """The (Rich) Style under the cursor."""
        return self._style or Style()

    @style.setter
    def style(self, style: Style) -> None:
        self._style = style

    def get_content_offset(self, widget: Widget) -> Offset | None:
        """Get offset within a widget's content area, or None if offset is not in content (i.e. padding or border).

        Args:
            widget: Widget receiving the event.

        Returns:
            An offset where the origin is at the top left of the content area.
        """
        if self.screen_offset not in widget.content_region:
            return None
        return self.get_content_offset_capture(widget)

    def get_content_offset_capture(self, widget: Widget) -> Offset:
        """Get offset from a widget's content area.

        This method works even if the offset is outside the widget content region.

        Args:
            widget: Widget receiving the event.

        Returns:
            An offset where the origin is at the top left of the content area.
        """
        return self.offset - widget.gutter.top_left

    def _apply_offset(self, x: int, y: int) -> MouseEvent:
        return self.__class__(
            self.widget,
            x=self._x + x,
            y=self._y + y,
            delta_x=self._delta_x,
            delta_y=self._delta_y,
            button=self.button,
            shift=self.shift,
            meta=self.meta,
            ctrl=self.ctrl,
            screen_x=self._screen_x,
            screen_y=self._screen_y,
            style=self.style,
        )


@rich.repr.auto
class MouseMove(MouseEvent, bubble=True, verbose=True):
    """Sent when the mouse cursor moves.

    - [X] Bubbles
    - [X] Verbose
    """


@rich.repr.auto
class MouseDown(MouseEvent, bubble=True, verbose=True):
    """Sent when a mouse button is pressed.

    - [X] Bubbles
    - [X] Verbose
    """


@rich.repr.auto
class MouseUp(MouseEvent, bubble=True, verbose=True):
    """Sent when a mouse button is released.

    - [X] Bubbles
    - [X] Verbose
    """


@rich.repr.auto
class MouseScrollDown(MouseEvent, bubble=True, verbose=True):
    """Sent when the mouse wheel is scrolled *down*.

    - [X] Bubbles
    - [X] Verbose
    """


@rich.repr.auto
class MouseScrollUp(MouseEvent, bubble=True, verbose=True):
    """Sent when the mouse wheel is scrolled *up*.

    - [X] Bubbles
    - [X] Verbose
    """


class Click(MouseEvent, bubble=True):
    """Sent when a widget is clicked.

    - [X] Bubbles
    - [ ] Verbose

    Args:
        chain: The number of clicks in the chain. 2 is a double click, 3 is a triple click, etc.
    """

    def __init__(
        self,
        widget: Widget | None,
        x: int,
        y: int,
        delta_x: int,
        delta_y: int,
        button: int,
        shift: bool,
        meta: bool,
        ctrl: bool,
        screen_x: int | None = None,
        screen_y: int | None = None,
        style: Style | None = None,
        chain: int = 1,
    ) -> None:
        super().__init__(
            widget,
            x,
            y,
            delta_x,
            delta_y,
            button,
            shift,
            meta,
            ctrl,
            screen_x,
            screen_y,
            style,
        )
        self.chain = chain

    @classmethod
    def from_event(
        cls: Type[Self],
        widget: Widget,
        event: MouseEvent,
        chain: int = 1,
    ) -> Self:
        new_event = cls(
            widget,
            event.x,
            event.y,
            event.delta_x,
            event.delta_y,
            event.button,
            event.shift,
            event.meta,
            event.ctrl,
            event.screen_x,
            event.screen_y,
            event._style,
            chain=chain,
        )
        return new_event

    def _apply_offset(self, x: int, y: int) -> Self:
        return self.__class__(
            self.widget,
            x=self.x + x,
            y=self.y + y,
            delta_x=self.delta_x,
            delta_y=self.delta_y,
            button=self.button,
            shift=self.shift,
            meta=self.meta,
            ctrl=self.ctrl,
            screen_x=self.screen_x,
            screen_y=self.screen_y,
            style=self.style,
            chain=self.chain,
        )

    def __rich_repr__(self) -> rich.repr.Result:
        yield from super().__rich_repr__()
        yield "chain", self.chain


@rich.repr.auto
class Timer(Event, bubble=False, verbose=True):
    """Sent in response to a timer.

    - [ ] Bubbles
    - [X] Verbose
    """

    __slots__ = ["timer", "time", "count", "callback"]

    def __init__(
        self,
        timer: "TimerClass",
        time: float,
        count: int = 0,
        callback: TimerCallback | None = None,
    ) -> None:
        super().__init__()
        self.timer = timer
        self.time = time
        self.count = count
        self.callback = callback

    def __rich_repr__(self) -> rich.repr.Result:
        yield self.timer.name
        yield "count", self.count


class Enter(Event, bubble=True, verbose=True):
    """Sent when the mouse is moved over a widget.

    Note that this event bubbles, so a widget may receive this event when the mouse
    moves over a child widget. Check the `node` attribute for the widget directly under
    the mouse.

    - [X] Bubbles
    - [X] Verbose
    """

    __slots__ = ["node"]

    def __init__(self, node: DOMNode) -> None:
        self.node = node
        """The node directly under the mouse."""
        super().__init__()

    @property
    def control(self) -> DOMNode:
        """Alias for the `node` under the mouse."""
        return self.node


class Leave(Event, bubble=True, verbose=True):
    """Sent when the mouse is moved away from a widget, or if a widget is
    programmatically disabled while hovered.

    Note that this widget bubbles, so a widget may receive Leave events for any child widgets.
    Check the `node` parameter for the original widget that was previously under the mouse.


    - [X] Bubbles
    - [X] Verbose
    """

    __slots__ = ["node"]

    def __init__(self, node: DOMNode) -> None:
        self.node = node
        """The node that was previously directly under the mouse."""
        super().__init__()

    @property
    def control(self) -> DOMNode:
        """Alias for the `node` that was previously under the mouse."""
        return self.node


class Focus(Event, bubble=False):
    """Sent when a widget is focussed.

    - [ ] Bubbles
    - [ ] Verbose

    Args:
        from_app_focus: True if this focus event has been sent because the app itself has
            regained focus (via an AppFocus event). False if the focus came from within
            the Textual app (e.g. via the user pressing tab or a programmatic setting
            of the focused widget).
    """

    def __init__(self, from_app_focus: bool = False) -> None:
        self.from_app_focus = from_app_focus
        super().__init__()

    def __rich_repr__(self) -> rich.repr.Result:
        yield from super().__rich_repr__()
        yield "from_app_focus", self.from_app_focus


class Blur(Event, bubble=False):
    """Sent when a widget is blurred (un-focussed).

    - [ ] Bubbles
    - [ ] Verbose
    """


class AppFocus(Event, bubble=False):
    """Sent when the app has focus.

    - [ ] Bubbles
    - [ ] Verbose

    Note:
        Only available when running within a terminal that supports
        `FocusIn`, or when running via textual-web.
    """


class AppBlur(Event, bubble=False):
    """Sent when the app loses focus.

    - [ ] Bubbles
    - [ ] Verbose

    Note:
        Only available when running within a terminal that supports
        `FocusOut`, or when running via textual-web.
    """


@dataclass
class DescendantFocus(Event, bubble=True, verbose=True):
    """Sent when a child widget is focussed.

    - [X] Bubbles
    - [X] Verbose
    """

    widget: Widget
    """The widget that was focused."""

    @property
    def control(self) -> Widget:
        """The widget that was focused (alias of `widget`)."""
        return self.widget


@dataclass
class DescendantBlur(Event, bubble=True, verbose=True):
    """Sent when a child widget is blurred.

    - [X] Bubbles
    - [X] Verbose
    """

    widget: Widget
    """The widget that was blurred."""

    @property
    def control(self) -> Widget:
        """The widget that was blurred (alias of `widget`)."""
        return self.widget


@rich.repr.auto
class Paste(Event, bubble=True):
    """Event containing text that was pasted into the Textual application.
    This event will only appear when running in a terminal emulator that supports
    bracketed paste mode. Textual will enable bracketed pastes when an app starts,
    and disable it when the app shuts down.

    - [X] Bubbles
    - [ ] Verbose


    Args:
        text: The text that has been pasted.
    """

    def __init__(self, text: str) -> None:
        super().__init__()
        self.text = text
        """The text that was pasted."""

    def __rich_repr__(self) -> rich.repr.Result:
        yield "text", self.text


class ScreenResume(Event, bubble=False):
    """Sent to screen that has been made active.

    - [ ] Bubbles
    - [ ] Verbose
    """


class ScreenSuspend(Event, bubble=False):
    """Sent to screen when it is no longer active.

    - [ ] Bubbles
    - [ ] Verbose
    """


@rich.repr.auto
class Print(Event, bubble=False):
    """Sent to a widget that is capturing [`print`][print].

    - [ ] Bubbles
    - [ ] Verbose

    Args:
        text: Text that was printed.
        stderr: `True` if the print was to stderr, or `False` for stdout.

    Note:
        Python's [`print`][print] output can be captured with
        [`App.begin_capture_print`][textual.app.App.begin_capture_print].
    """

    def __init__(self, text: str, stderr: bool = False) -> None:
        super().__init__()
        self.text = text
        """The text that was printed."""
        self.stderr = stderr
        """`True` if the print was to stderr, or `False` for stdout."""

    def __rich_repr__(self) -> rich.repr.Result:
        yield self.text
        yield self.stderr


@dataclass
class DeliveryComplete(Event, bubble=False):
    """Sent to App when a file has been delivered."""

    key: str
    """The delivery key associated with the delivery.
    
    This is the same key that was returned by `App.deliver_text`/`App.deliver_binary`.
    """

    path: Path | None = None
    """The path where the file was saved, or `None` if the path is not available, for
    example if the file was delivered via web browser.
    """

    name: str | None = None
    """Optional name returned to the app to identify the download."""


@dataclass
class DeliveryFailed(Event, bubble=False):
    """Sent to App when a file delivery fails."""

    key: str
    """The delivery key associated with the delivery."""

    exception: BaseException
    """The exception that was raised during the delivery."""

    name: str | None = None
    """Optional name returned to the app to identify the download."""
