from typing import Optional

from prompt_toolkit.application.current import get_app
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.enums import SYSTEM_BUFFER
from prompt_toolkit.filters import (
    Condition,
    FilterOrBool,
    emacs_mode,
    has_arg,
    has_completions,
    has_focus,
    has_validation_error,
    to_filter,
    vi_mode,
    vi_navigation_mode,
)
from prompt_toolkit.formatted_text import (
    AnyFormattedText,
    StyleAndTextTuples,
    fragment_list_len,
    to_formatted_text,
)
from prompt_toolkit.key_binding.key_bindings import (
    ConditionalKeyBindings,
    KeyBindings,
    KeyBindingsBase,
    merge_key_bindings,
)
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.key_binding.vi_state import InputMode
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window
from prompt_toolkit.layout.controls import (
    BufferControl,
    FormattedTextControl,
    SearchBufferControl,
    UIContent,
    UIControl,
)
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.layout.processors import BeforeInput
from prompt_toolkit.lexers import SimpleLexer
from prompt_toolkit.search import SearchDirection

__all__ = [
    "ArgToolbar",
    "CompletionsToolbar",
    "FormattedTextToolbar",
    "SearchToolbar",
    "SystemToolbar",
    "ValidationToolbar",
]

E = KeyPressEvent


class FormattedTextToolbar(Window):
    def __init__(self, text: AnyFormattedText, style: str = "", **kw) -> None:
        # Note: The style needs to be applied to the toolbar as a whole, not
        #       just the `FormattedTextControl`.
        super().__init__(
            FormattedTextControl(text, **kw),
            style=style,
            dont_extend_height=True,
            height=Dimension(min=1),
        )


class SystemToolbar:
    """
    Toolbar for a system prompt.

    :param prompt: Prompt to be displayed to the user.
    """

    def __init__(
        self,
        prompt: AnyFormattedText = "Shell command: ",
        enable_global_bindings: FilterOrBool = True,
    ) -> None:

        self.prompt = prompt
        self.enable_global_bindings = to_filter(enable_global_bindings)

        self.system_buffer = Buffer(name=SYSTEM_BUFFER)

        self._bindings = self._build_key_bindings()

        self.buffer_control = BufferControl(
            buffer=self.system_buffer,
            lexer=SimpleLexer(style="class:system-toolbar.text"),
            input_processors=[
                BeforeInput(lambda: self.prompt, style="class:system-toolbar")
            ],
            key_bindings=self._bindings,
        )

        self.window = Window(
            self.buffer_control, height=1, style="class:system-toolbar"
        )

        self.container = ConditionalContainer(
            content=self.window, filter=has_focus(self.system_buffer)
        )

    def _get_display_before_text(self) -> StyleAndTextTuples:
        return [
            ("class:system-toolbar", "Shell command: "),
            ("class:system-toolbar.text", self.system_buffer.text),
            ("", "\n"),
        ]

    def _build_key_bindings(self) -> KeyBindingsBase:
        focused = has_focus(self.system_buffer)

        # Emacs
        emacs_bindings = KeyBindings()
        handle = emacs_bindings.add

        @handle("escape", filter=focused)
        @handle("c-g", filter=focused)
        @handle("c-c", filter=focused)
        def _cancel(event: E) -> None:
            " Hide system prompt. "
            self.system_buffer.reset()
            event.app.layout.focus_last()

        @handle("enter", filter=focused)
        def _accept(event: E) -> None:
            " Run system command. "
            event.app.run_system_command(
                self.system_buffer.text,
                display_before_text=self._get_display_before_text(),
            )
            self.system_buffer.reset(append_to_history=True)
            event.app.layout.focus_last()

        # Vi.
        vi_bindings = KeyBindings()
        handle = vi_bindings.add

        @handle("escape", filter=focused)
        @handle("c-c", filter=focused)
        def _cancel_vi(event: E) -> None:
            " Hide system prompt. "
            event.app.vi_state.input_mode = InputMode.NAVIGATION
            self.system_buffer.reset()
            event.app.layout.focus_last()

        @handle("enter", filter=focused)
        def _accept_vi(event: E) -> None:
            " Run system command. "
            event.app.vi_state.input_mode = InputMode.NAVIGATION
            event.app.run_system_command(
                self.system_buffer.text,
                display_before_text=self._get_display_before_text(),
            )
            self.system_buffer.reset(append_to_history=True)
            event.app.layout.focus_last()

        # Global bindings. (Listen to these bindings, even when this widget is
        # not focussed.)
        global_bindings = KeyBindings()
        handle = global_bindings.add

        @handle(Keys.Escape, "!", filter=~focused & emacs_mode, is_global=True)
        def _focus_me(event: E) -> None:
            " M-'!' will focus this user control. "
            event.app.layout.focus(self.window)

        @handle("!", filter=~focused & vi_mode & vi_navigation_mode, is_global=True)
        def _focus_me_vi(event: E) -> None:
            " Focus. "
            event.app.vi_state.input_mode = InputMode.INSERT
            event.app.layout.focus(self.window)

        return merge_key_bindings(
            [
                ConditionalKeyBindings(emacs_bindings, emacs_mode),
                ConditionalKeyBindings(vi_bindings, vi_mode),
                ConditionalKeyBindings(global_bindings, self.enable_global_bindings),
            ]
        )

    def __pt_container__(self) -> Container:
        return self.container


class ArgToolbar:
    def __init__(self) -> None:
        def get_formatted_text() -> StyleAndTextTuples:
            arg = get_app().key_processor.arg or ""
            if arg == "-":
                arg = "-1"

            return [
                ("class:arg-toolbar", "Repeat: "),
                ("class:arg-toolbar.text", arg),
            ]

        self.window = Window(FormattedTextControl(get_formatted_text), height=1)

        self.container = ConditionalContainer(content=self.window, filter=has_arg)

    def __pt_container__(self) -> Container:
        return self.container


class SearchToolbar:
    """
    :param vi_mode: Display '/' and '?' instead of I-search.
    :param ignore_case: Search case insensitive.
    """

    def __init__(
        self,
        search_buffer: Optional[Buffer] = None,
        vi_mode: bool = False,
        text_if_not_searching: AnyFormattedText = "",
        forward_search_prompt: AnyFormattedText = "I-search: ",
        backward_search_prompt: AnyFormattedText = "I-search backward: ",
        ignore_case: FilterOrBool = False,
    ) -> None:

        if search_buffer is None:
            search_buffer = Buffer()

        @Condition
        def is_searching() -> bool:
            return self.control in get_app().layout.search_links

        def get_before_input() -> AnyFormattedText:
            if not is_searching():
                return text_if_not_searching
            elif (
                self.control.searcher_search_state.direction == SearchDirection.BACKWARD
            ):
                return "?" if vi_mode else backward_search_prompt
            else:
                return "/" if vi_mode else forward_search_prompt

        self.search_buffer = search_buffer

        self.control = SearchBufferControl(
            buffer=search_buffer,
            input_processors=[
                BeforeInput(get_before_input, style="class:search-toolbar.prompt")
            ],
            lexer=SimpleLexer(style="class:search-toolbar.text"),
            ignore_case=ignore_case,
        )

        self.container = ConditionalContainer(
            content=Window(self.control, height=1, style="class:search-toolbar"),
            filter=is_searching,
        )

    def __pt_container__(self) -> Container:
        return self.container


class _CompletionsToolbarControl(UIControl):
    def create_content(self, width: int, height: int) -> UIContent:
        all_fragments: StyleAndTextTuples = []

        complete_state = get_app().current_buffer.complete_state
        if complete_state:
            completions = complete_state.completions
            index = complete_state.complete_index  # Can be None!

            # Width of the completions without the left/right arrows in the margins.
            content_width = width - 6

            # Booleans indicating whether we stripped from the left/right
            cut_left = False
            cut_right = False

            # Create Menu content.
            fragments: StyleAndTextTuples = []

            for i, c in enumerate(completions):
                # When there is no more place for the next completion
                if fragment_list_len(fragments) + len(c.display_text) >= content_width:
                    # If the current one was not yet displayed, page to the next sequence.
                    if i <= (index or 0):
                        fragments = []
                        cut_left = True
                    # If the current one is visible, stop here.
                    else:
                        cut_right = True
                        break

                fragments.extend(
                    to_formatted_text(
                        c.display_text,
                        style=(
                            "class:completion-toolbar.completion.current"
                            if i == index
                            else "class:completion-toolbar.completion"
                        ),
                    )
                )
                fragments.append(("", " "))

            # Extend/strip until the content width.
            fragments.append(("", " " * (content_width - fragment_list_len(fragments))))
            fragments = fragments[:content_width]

            # Return fragments
            all_fragments.append(("", " "))
            all_fragments.append(
                ("class:completion-toolbar.arrow", "<" if cut_left else " ")
            )
            all_fragments.append(("", " "))

            all_fragments.extend(fragments)

            all_fragments.append(("", " "))
            all_fragments.append(
                ("class:completion-toolbar.arrow", ">" if cut_right else " ")
            )
            all_fragments.append(("", " "))

        def get_line(i: int) -> StyleAndTextTuples:
            return all_fragments

        return UIContent(get_line=get_line, line_count=1)


class CompletionsToolbar:
    def __init__(self) -> None:
        self.container = ConditionalContainer(
            content=Window(
                _CompletionsToolbarControl(), height=1, style="class:completion-toolbar"
            ),
            filter=has_completions,
        )

    def __pt_container__(self) -> Container:
        return self.container


class ValidationToolbar:
    def __init__(self, show_position: bool = False) -> None:
        def get_formatted_text() -> StyleAndTextTuples:
            buff = get_app().current_buffer

            if buff.validation_error:
                row, column = buff.document.translate_index_to_position(
                    buff.validation_error.cursor_position
                )

                if show_position:
                    text = "%s (line=%s column=%s)" % (
                        buff.validation_error.message,
                        row + 1,
                        column + 1,
                    )
                else:
                    text = buff.validation_error.message

                return [("class:validation-toolbar", text)]
            else:
                return []

        self.control = FormattedTextControl(get_formatted_text)

        self.container = ConditionalContainer(
            content=Window(self.control, height=1), filter=has_validation_error
        )

    def __pt_container__(self) -> Container:
        return self.container
