import inspect
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union

from prompt_toolkit import PromptSession
from prompt_toolkit.filters import Always
from prompt_toolkit.filters import Condition
from prompt_toolkit.filters import IsDone
from prompt_toolkit.layout import ConditionalContainer
from prompt_toolkit.layout import FormattedTextControl
from prompt_toolkit.layout import HSplit
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout import Window
from prompt_toolkit.styles import Style
from prompt_toolkit.validation import ValidationError
from prompt_toolkit.validation import Validator

from ..constants import DEFAULT_SELECTED_POINTER
from ..constants import DEFAULT_STYLE
from ..constants import INDICATOR_SELECTED
from ..constants import INDICATOR_UNSELECTED
from ..constants import INVALID_INPUT

# This is a cut-down version of `prompt_toolkit.formatted_text.AnyFormattedText`
# which does not exist in v2 of prompt_toolkit
AnyFormattedText = Union[
    str,
    List[Tuple[str, str]],
    List[Tuple[str, str, Callable[[Any], None]]],
    None,
]


class Choice:
    """One choice in a :meth:`select`, :meth:`rawselect` or :meth:`checkbox`.

    Args:
        title: Text shown in the selection list.

        value: Value returned, when the choice is selected. If this argument
               is `None` or unset, then the value of `title` is used.

        disabled: If set, the choice can not be selected by the user. The
                  provided text is used to explain, why the selection is
                  disabled.

        checked: Preselect this choice when displaying the options.

        shortcut_key: Key shortcut used to select this item.
    """

    title: AnyFormattedText
    """Display string for the choice"""

    value: Optional[Any]
    """Value of the choice"""

    disabled: Optional[str]
    """Whether the choice can be selected"""

    checked: Optional[bool]
    """Whether the choice is initially selected"""

    shortcut_key: Optional[str]
    """A shortcut key for the choice"""

    def __init__(
        self,
        title: AnyFormattedText,
        value: Optional[Any] = None,
        disabled: Optional[str] = None,
        checked: Optional[bool] = False,
        shortcut_key: Optional[Union[str, bool]] = True,
    ) -> None:
        self.disabled = disabled
        self.title = title
        self.checked = checked if checked is not None else False

        if value is not None:
            self.value = value
        elif isinstance(title, list):
            self.value = "".join([token[1] for token in title])
        else:
            self.value = title

        if shortcut_key is not None:
            if isinstance(shortcut_key, bool):
                self.auto_shortcut = shortcut_key
                self.shortcut_key = None
            else:
                self.shortcut_key = str(shortcut_key)
                self.auto_shortcut = False
        else:
            self.shortcut_key = None
            self.auto_shortcut = True

    @staticmethod
    def build(c: Union[str, "Choice", Dict[str, Any]]) -> "Choice":
        """Create a choice object from different representations.

        Args:
            c: Either a :obj:`str`, :class:`Choice` or :obj:`dict` with
               ``name``, ``value``, ``disabled``, ``checked`` and
               ``key`` properties.

        Returns:
            An instance of the :class:`Choice` object.
        """

        if isinstance(c, Choice):
            return c
        elif isinstance(c, str):
            return Choice(c, c)
        else:
            return Choice(
                c.get("name"),
                c.get("value"),
                c.get("disabled", None),
                c.get("checked"),
                c.get("key"),
            )

    def get_shortcut_title(self):
        if self.shortcut_key is None:
            return "-) "
        else:
            return "{}) ".format(self.shortcut_key)


class Separator(Choice):
    """Used to space/separate choices group."""

    default_separator: str = "-" * 15
    """The default separator used if none is specified"""

    line: str
    """The string being used as a separator"""

    def __init__(self, line: Optional[str] = None) -> None:
        """Create a separator in a list.

        Args:
            line: Text to be displayed in the list, by default uses ``---``.
        """

        self.line = line or self.default_separator
        super().__init__(self.line, None, "-")


class InquirerControl(FormattedTextControl):
    SHORTCUT_KEYS = [
        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "0",
        "a",
        "b",
        "c",
        "d",
        "e",
        "f",
        "g",
        "h",
        "i",
        "j",
        "k",
        "l",
        "m",
        "n",
        "o",
        "p",
        "q",
        "r",
        "s",
        "t",
        "u",
        "v",
        "w",
        "x",
        "y",
        "z",
    ]

    choices: List[Choice]
    default: Optional[Union[str, Choice, Dict[str, Any]]]
    selected_options: List[Any]
    use_indicator: bool
    use_shortcuts: bool
    use_arrow_keys: bool
    pointer: Optional[str]
    pointed_at: int
    is_answered: bool

    def __init__(
        self,
        choices: Sequence[Union[str, Choice, Dict[str, Any]]],
        default: Optional[Union[str, Choice, Dict[str, Any]]] = None,
        pointer: Optional[str] = DEFAULT_SELECTED_POINTER,
        use_indicator: bool = True,
        use_shortcuts: bool = False,
        show_selected: bool = False,
        use_arrow_keys: bool = True,
        initial_choice: Optional[Union[str, Choice, Dict[str, Any]]] = None,
        **kwargs: Any,
    ):
        self.use_indicator = use_indicator
        self.use_shortcuts = use_shortcuts
        self.show_selected = show_selected
        self.use_arrow_keys = use_arrow_keys
        self.default = default
        self.pointer = pointer

        if isinstance(default, Choice):
            default = default.value

        choices_values = [
            choice.value for choice in choices if isinstance(choice, Choice)
        ]

        if (
            default is not None
            and default not in choices
            and default not in choices_values
        ):
            raise ValueError(
                f"Invalid `default` value passed. The value (`{default}`) "
                f"does not exist in the set of choices. Please make sure the "
                f"default value is one of the available choices."
            )

        if initial_choice is None:
            pointed_at = None
        elif initial_choice in choices:
            pointed_at = choices.index(initial_choice)
        elif initial_choice in choices_values:
            for k, choice in enumerate(choices):
                if isinstance(choice, Choice):
                    if choice.value == initial_choice:
                        pointed_at = k
                        break

        else:
            raise ValueError(
                f"Invalid `initial_choice` value passed. The value "
                f"(`{initial_choice}`) does not exist in "
                f"the set of choices. Please make sure the initial value is "
                f"one of the available choices."
            )

        self.is_answered = False
        self.choices = []
        self.submission_attempted = False
        self.error_message = None
        self.selected_options = []

        self._init_choices(choices, pointed_at)
        self._assign_shortcut_keys()

        super().__init__(self._get_choice_tokens, **kwargs)

        if not self.is_selection_valid():
            raise ValueError(
                f"Invalid 'initial_choice' value ('{initial_choice}'). "
                f"It must be a selectable value."
            )

    def _is_selected(self, choice: Choice):
        if isinstance(self.default, Choice):
            compare_default = self.default == choice
        else:
            compare_default = self.default == choice.value
        return choice.checked or compare_default and self.default is not None

    def _assign_shortcut_keys(self):
        available_shortcuts = self.SHORTCUT_KEYS[:]

        # first, make sure we do not double assign a shortcut
        for c in self.choices:
            if c.shortcut_key is not None:
                if c.shortcut_key in available_shortcuts:
                    available_shortcuts.remove(c.shortcut_key)
                else:
                    raise ValueError(
                        "Invalid shortcut '{}'"
                        "for choice '{}'. Shortcuts "
                        "should be single characters or numbers. "
                        "Make sure that all your shortcuts are "
                        "unique.".format(c.shortcut_key, c.title)
                    )

        shortcut_idx = 0
        for c in self.choices:
            if c.auto_shortcut and not c.disabled:
                c.shortcut_key = available_shortcuts[shortcut_idx]
                shortcut_idx += 1

            if shortcut_idx == len(available_shortcuts):
                break  # fail gracefully if we run out of shortcuts

    def _init_choices(
        self,
        choices: Sequence[Union[str, Choice, Dict[str, Any]]],
        pointed_at: Optional[int],
    ):
        # helper to convert from question format to internal format
        self.choices = []

        if pointed_at is not None:
            self.pointed_at = pointed_at

        for i, c in enumerate(choices):
            choice = Choice.build(c)

            if self._is_selected(choice):
                self.selected_options.append(choice.value)

            if pointed_at is None and not choice.disabled:
                # find the first (available) choice
                self.pointed_at = pointed_at = i

            self.choices.append(choice)

    @property
    def choice_count(self) -> int:
        return len(self.choices)

    def _get_choice_tokens(self):
        tokens = []

        def append(index: int, choice: Choice):
            # use value to check if option has been selected
            selected = choice.value in self.selected_options

            if index == self.pointed_at:
                if self.pointer is not None:
                    tokens.append(("class:pointer", " {} ".format(self.pointer)))
                else:
                    tokens.append(("class:text", " " * 3))

                tokens.append(("[SetCursorPosition]", ""))
            else:
                pointer_length = len(self.pointer) if self.pointer is not None else 1
                tokens.append(("class:text", " " * (2 + pointer_length)))

            if isinstance(choice, Separator):
                tokens.append(("class:separator", "{}".format(choice.title)))
            elif choice.disabled:  # disabled
                if isinstance(choice.title, list):
                    tokens.append(
                        ("class:selected" if selected else "class:disabled", "- ")
                    )
                    tokens.extend(choice.title)
                else:
                    tokens.append(
                        (
                            "class:selected" if selected else "class:disabled",
                            "- {}".format(choice.title),
                        )
                    )

                tokens.append(
                    (
                        "class:selected" if selected else "class:disabled",
                        "{}".format(
                            ""
                            if isinstance(choice.disabled, bool)
                            else " ({})".format(choice.disabled)
                        ),
                    )
                )
            else:
                shortcut = choice.get_shortcut_title() if self.use_shortcuts else ""

                if selected:
                    if self.use_indicator:
                        indicator = INDICATOR_SELECTED + " "
                    else:
                        indicator = ""

                    tokens.append(("class:selected", "{}".format(indicator)))
                else:
                    if self.use_indicator:
                        indicator = INDICATOR_UNSELECTED + " "
                    else:
                        indicator = ""

                    tokens.append(("class:text", "{}".format(indicator)))

                if isinstance(choice.title, list):
                    tokens.extend(choice.title)
                elif selected:
                    tokens.append(
                        ("class:selected", "{}{}".format(shortcut, choice.title))
                    )
                elif index == self.pointed_at:
                    tokens.append(
                        ("class:highlighted", "{}{}".format(shortcut, choice.title))
                    )
                else:
                    tokens.append(("class:text", "{}{}".format(shortcut, choice.title)))

            tokens.append(("", "\n"))

        # prepare the select choices
        for i, c in enumerate(self.choices):
            append(i, c)

        if self.show_selected:
            current = self.get_pointed_at()

            answer = current.get_shortcut_title() if self.use_shortcuts else ""

            answer += (
                current.title if isinstance(current.title, str) else current.title[0][1]
            )

            tokens.append(("class:text", "  Answer: {}".format(answer)))
        else:
            tokens.pop()  # Remove last newline.
        return tokens

    def is_selection_a_separator(self) -> bool:
        selected = self.choices[self.pointed_at]
        return isinstance(selected, Separator)

    def is_selection_disabled(self) -> Optional[str]:
        return self.choices[self.pointed_at].disabled

    def is_selection_valid(self) -> bool:
        return not self.is_selection_disabled() and not self.is_selection_a_separator()

    def select_previous(self) -> None:
        self.pointed_at = (self.pointed_at - 1) % self.choice_count

    def select_next(self) -> None:
        self.pointed_at = (self.pointed_at + 1) % self.choice_count

    def get_pointed_at(self) -> Choice:
        return self.choices[self.pointed_at]

    def get_selected_values(self) -> List[Choice]:
        # get values not labels
        return [
            c
            for c in self.choices
            if (not isinstance(c, Separator) and c.value in self.selected_options)
        ]


def build_validator(validate: Any) -> Optional[Validator]:
    if validate:
        if inspect.isclass(validate) and issubclass(validate, Validator):
            return validate()
        elif isinstance(validate, Validator):
            return validate
        elif callable(validate):

            class _InputValidator(Validator):
                def validate(self, document):
                    verdict = validate(document.text)
                    if verdict is not True:
                        if verdict is False:
                            verdict = INVALID_INPUT
                        raise ValidationError(
                            message=verdict, cursor_position=len(document.text)
                        )

            return _InputValidator()
    return None


def _fix_unecessary_blank_lines(ps: PromptSession) -> None:
    """This is a fix for additional empty lines added by prompt toolkit.

    This assumes the layout of the default session doesn't change, if it
    does, this needs an update."""

    default_container = ps.layout.container

    default_buffer_window = (
        default_container.get_children()[0].content.get_children()[1].content  # type: ignore[attr-defined]
    )

    assert isinstance(default_buffer_window, Window)
    # this forces the main window to stay as small as possible, avoiding
    # empty lines in selections
    default_buffer_window.dont_extend_height = Always()
    default_buffer_window.always_hide_cursor = Always()


def create_inquirer_layout(
    ic: InquirerControl,
    get_prompt_tokens: Callable[[], List[Tuple[str, str]]],
    **kwargs: Any,
) -> Layout:
    """Create a layout combining question and inquirer selection."""

    ps: PromptSession = PromptSession(
        get_prompt_tokens, reserve_space_for_menu=0, **kwargs
    )
    _fix_unecessary_blank_lines(ps)

    validation_prompt: PromptSession = PromptSession(
        bottom_toolbar=lambda: ic.error_message, **kwargs
    )

    return Layout(
        HSplit(
            [
                ps.layout.container,
                ConditionalContainer(Window(ic), filter=~IsDone()),
                ConditionalContainer(
                    validation_prompt.layout.container,
                    filter=Condition(lambda: ic.error_message is not None),
                ),
            ]
        )
    )


def print_formatted_text(text: str, style: Optional[str] = None, **kwargs: Any) -> None:
    """Print formatted text.

    Sometimes you want to spice up your printed messages a bit,
    :meth:`questionary.print` is a helper to do just that.

    Example:

        >>> import questionary
        >>> questionary.print("Hello World 🦄", style="bold italic fg:darkred")
        Hello World 🦄

    .. image:: ../images/print.gif

    Args:
        text: Text to be printed.
        style: Style used for printing. The style argument uses the
            prompt :ref:`toolkit style strings <prompt_toolkit:styling>`.
    """
    from prompt_toolkit import print_formatted_text as pt_print
    from prompt_toolkit.formatted_text import FormattedText as FText

    if style is not None:
        text_style = Style([("text", style)])
    else:
        text_style = DEFAULT_STYLE

    pt_print(FText([("class:text", text)]), style=text_style, **kwargs)
