from __future__ import annotations

from collections.abc import Sequence
from typing import Generic, TypeVar

from prompt_toolkit.application import Application
from prompt_toolkit.filters import (
    Condition,
    FilterOrBool,
    is_done,
    renderer_height_is_known,
    to_filter,
)
from prompt_toolkit.formatted_text import AnyFormattedText
from prompt_toolkit.key_binding.key_bindings import (
    DynamicKeyBindings,
    KeyBindings,
    KeyBindingsBase,
    merge_key_bindings,
)
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.layout import (
    AnyContainer,
    ConditionalContainer,
    HSplit,
    Layout,
    Window,
)
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.styles import BaseStyle, Style
from prompt_toolkit.utils import suspend_to_background_supported
from prompt_toolkit.widgets import Box, Frame, Label, RadioList

__all__ = [
    "ChoiceInput",
    "choice",
]

_T = TypeVar("_T")
E = KeyPressEvent


def create_default_choice_input_style() -> BaseStyle:
    return Style.from_dict(
        {
            "frame.border": "#884444",
            "selected-option": "bold",
        }
    )


class ChoiceInput(Generic[_T]):
    """
    Input selection prompt. Ask the user to choose among a set of options.

    Example usage::

        input_selection = ChoiceInput(
            message="Please select a dish:",
            options=[
                ("pizza", "Pizza with mushrooms"),
                ("salad", "Salad with tomatoes"),
                ("sushi", "Sushi"),
            ],
            default="pizza",
        )
        result = input_selection.prompt()

    :param message: Plain text or formatted text to be shown before the options.
    :param options: Sequence of ``(value, label)`` tuples. The labels can be
        formatted text.
    :param default: Default value. If none is given, the first option is
        considered the default.
    :param mouse_support: Enable mouse support.
    :param style: :class:`.Style` instance for the color scheme.
    :param symbol: Symbol to be displayed in front of the selected choice.
    :param bottom_toolbar: Formatted text or callable that returns formatted
        text to be displayed at the bottom of the screen.
    :param show_frame: `bool` or
        :class:`~prompt_toolkit.filters.Filter`. When True, surround the input
        with a frame.
    :param show_numbers: Whether to show the option numbers in front of each choice.
    :param enable_interrupt: `bool` or
        :class:`~prompt_toolkit.filters.Filter`. When True, raise
        the ``interrupt_exception`` (``KeyboardInterrupt`` by default) when
        control-c has been pressed.
    :param interrupt_exception: The exception type that will be raised when
        there is a keyboard interrupt (control-c keypress).
    """

    def __init__(
        self,
        *,
        message: AnyFormattedText,
        options: Sequence[tuple[_T, AnyFormattedText]],
        default: _T | None = None,
        mouse_support: bool = False,
        style: BaseStyle | None = None,
        symbol: str = ">",
        bottom_toolbar: AnyFormattedText = None,
        show_frame: FilterOrBool = False,
        show_numbers: bool = True,
        enable_suspend: FilterOrBool = False,
        enable_interrupt: FilterOrBool = True,
        interrupt_exception: type[BaseException] = KeyboardInterrupt,
        key_bindings: KeyBindingsBase | None = None,
    ) -> None:
        if style is None:
            style = create_default_choice_input_style()

        self.message = message
        self.default = default
        self.options = options
        self.mouse_support = mouse_support
        self.style = style
        self.symbol = symbol
        self.show_frame = show_frame
        self.show_numbers = show_numbers
        self.enable_suspend = enable_suspend
        self.interrupt_exception = interrupt_exception
        self.enable_interrupt = enable_interrupt
        self.bottom_toolbar = bottom_toolbar
        self.key_bindings = key_bindings

    def _create_application(self) -> Application[_T]:
        radio_list = RadioList(
            values=self.options,
            default=self.default,
            select_on_focus=True,
            open_character="",
            select_character=self.symbol,
            close_character="",
            show_cursor=False,
            show_numbers=self.show_numbers,
            container_style="class:input-selection",
            default_style="class:option",
            selected_style="",
            checked_style="class:selected-option",
            number_style="class:number",
            show_scrollbar=False,
        )
        container: AnyContainer = HSplit(
            [
                Box(
                    Label(text=self.message, dont_extend_height=True),
                    padding_top=0,
                    padding_left=1,
                    padding_right=1,
                    padding_bottom=0,
                ),
                Box(
                    radio_list,
                    padding_top=0,
                    padding_left=3,
                    padding_right=1,
                    padding_bottom=0,
                ),
            ]
        )

        @Condition
        def show_frame_filter() -> bool:
            return to_filter(self.show_frame)()

        show_bottom_toolbar = (
            Condition(lambda: self.bottom_toolbar is not None)
            & ~is_done
            & renderer_height_is_known
        )

        container = ConditionalContainer(
            Frame(container),
            alternative_content=container,
            filter=show_frame_filter,
        )

        bottom_toolbar = ConditionalContainer(
            Window(
                FormattedTextControl(
                    lambda: self.bottom_toolbar, style="class:bottom-toolbar.text"
                ),
                style="class:bottom-toolbar",
                dont_extend_height=True,
                height=Dimension(min=1),
            ),
            filter=show_bottom_toolbar,
        )

        layout = Layout(
            HSplit(
                [
                    container,
                    # Add an empty window between the selection input and the
                    # bottom toolbar, if the bottom toolbar is visible, in
                    # order to allow the bottom toolbar to be displayed at the
                    # bottom of the screen.
                    ConditionalContainer(Window(), filter=show_bottom_toolbar),
                    bottom_toolbar,
                ]
            ),
            focused_element=radio_list,
        )

        kb = KeyBindings()

        @kb.add("enter", eager=True)
        def _accept_input(event: E) -> None:
            "Accept input when enter has been pressed."
            event.app.exit(result=radio_list.current_value, style="class:accepted")

        @Condition
        def enable_interrupt() -> bool:
            return to_filter(self.enable_interrupt)()

        @kb.add("c-c", filter=enable_interrupt)
        @kb.add("<sigint>", filter=enable_interrupt)
        def _keyboard_interrupt(event: E) -> None:
            "Abort when Control-C has been pressed."
            event.app.exit(exception=self.interrupt_exception(), style="class:aborting")

        suspend_supported = Condition(suspend_to_background_supported)

        @Condition
        def enable_suspend() -> bool:
            return to_filter(self.enable_suspend)()

        @kb.add("c-z", filter=suspend_supported & enable_suspend)
        def _suspend(event: E) -> None:
            """
            Suspend process to background.
            """
            event.app.suspend_to_background()

        return Application(
            layout=layout,
            full_screen=False,
            mouse_support=self.mouse_support,
            key_bindings=merge_key_bindings(
                [kb, DynamicKeyBindings(lambda: self.key_bindings)]
            ),
            style=self.style,
        )

    def prompt(self) -> _T:
        return self._create_application().run()

    async def prompt_async(self) -> _T:
        return await self._create_application().run_async()


def choice(
    message: AnyFormattedText,
    *,
    options: Sequence[tuple[_T, AnyFormattedText]],
    default: _T | None = None,
    mouse_support: bool = False,
    style: BaseStyle | None = None,
    symbol: str = ">",
    bottom_toolbar: AnyFormattedText = None,
    show_frame: bool = False,
    enable_suspend: FilterOrBool = False,
    enable_interrupt: FilterOrBool = True,
    interrupt_exception: type[BaseException] = KeyboardInterrupt,
    key_bindings: KeyBindingsBase | None = None,
) -> _T:
    """
    Choice selection prompt. Ask the user to choose among a set of options.

    Example usage::

        result = choice(
            message="Please select a dish:",
            options=[
                ("pizza", "Pizza with mushrooms"),
                ("salad", "Salad with tomatoes"),
                ("sushi", "Sushi"),
            ],
            default="pizza",
        )

    :param message: Plain text or formatted text to be shown before the options.
    :param options: Sequence of ``(value, label)`` tuples. The labels can be
        formatted text.
    :param default: Default value. If none is given, the first option is
        considered the default.
    :param mouse_support: Enable mouse support.
    :param style: :class:`.Style` instance for the color scheme.
    :param symbol: Symbol to be displayed in front of the selected choice.
    :param bottom_toolbar: Formatted text or callable that returns formatted
        text to be displayed at the bottom of the screen.
    :param show_frame: `bool` or
        :class:`~prompt_toolkit.filters.Filter`. When True, surround the input
        with a frame.
    :param enable_interrupt: `bool` or
        :class:`~prompt_toolkit.filters.Filter`. When True, raise
        the ``interrupt_exception`` (``KeyboardInterrupt`` by default) when
        control-c has been pressed.
    :param interrupt_exception: The exception type that will be raised when
        there is a keyboard interrupt (control-c keypress).
    """
    return ChoiceInput[_T](
        message=message,
        options=options,
        default=default,
        mouse_support=mouse_support,
        style=style,
        symbol=symbol,
        bottom_toolbar=bottom_toolbar,
        show_frame=show_frame,
        enable_suspend=enable_suspend,
        enable_interrupt=enable_interrupt,
        interrupt_exception=interrupt_exception,
        key_bindings=key_bindings,
    ).prompt()
