"""
Creation of the `Layout` instance for the Python input/REPL.
"""
from __future__ import unicode_literals

from prompt_toolkit.application import get_app
from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
from prompt_toolkit.filters import is_done, has_completions, renderer_height_is_known, has_focus, Condition
from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text
from prompt_toolkit.key_binding.vi_state import InputMode
from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.layout.margins import PromptMargin
from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu
from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightIncrementalSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation
from prompt_toolkit.lexers import SimpleLexer
from prompt_toolkit.selection import SelectionType
from prompt_toolkit.widgets.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar

from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring
from .utils import if_mousedown

from pygments.lexers import PythonLexer

import platform
import sys

__all__ = (
    'PtPythonLayout',
    'CompletionVisualisation',
)


# DisplayMultipleCursors: Only for prompt_toolkit>=1.0.8
try:
    from prompt_toolkit.layout.processors import DisplayMultipleCursors
except ImportError:
    class DisplayMultipleCursors(Processor):
        " Dummy. "
        def __init__(self, *a):
            pass

        def apply_transformation(self, document, lineno,
                                 source_to_display, tokens):
            return Transformation(tokens)


class CompletionVisualisation:
    " Visualisation method for the completions. "
    NONE = 'none'
    POP_UP = 'pop-up'
    MULTI_COLUMN = 'multi-column'
    TOOLBAR = 'toolbar'


def show_completions_toolbar(python_input):
    return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR)


def show_completions_menu(python_input):
    return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP)


def show_multi_column_completions_menu(python_input):
    return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN)


def python_sidebar(python_input):
    """
    Create the `Layout` for the sidebar with the configurable options.
    """
    def get_text_fragments():
        tokens = []

        def append_category(category):
            tokens.extend([
                ('class:sidebar', '  '),
                ('class:sidebar.title', '   %-36s' % category.title),
                ('class:sidebar', '\n'),
            ])

        def append(index, label, status):
            selected = index == python_input.selected_option_index

            @if_mousedown
            def select_item(mouse_event):
                python_input.selected_option_index = index

            @if_mousedown
            def goto_next(mouse_event):
                " Select item and go to next value. "
                python_input.selected_option_index = index
                option = python_input.selected_option
                option.activate_next()

            sel = ',selected' if selected else ''

            tokens.append(('class:sidebar' + sel, ' >' if selected else '  '))
            tokens.append(('class:sidebar.label' + sel, '%-24s' % label, select_item))
            tokens.append(('class:sidebar.status' + sel, ' ', select_item))
            tokens.append(('class:sidebar.status' + sel, '%s' % status, goto_next))

            if selected:
                tokens.append(('[SetCursorPosition]', ''))

            tokens.append(('class:sidebar.status' + sel, ' ' * (13 - len(status)), goto_next))
            tokens.append(('class:sidebar', '<' if selected else ''))
            tokens.append(('class:sidebar', '\n'))

        i = 0
        for category in python_input.options:
            append_category(category)

            for option in category.options:
                append(i, option.title, '%s' % option.get_current_value())
                i += 1

        tokens.pop()  # Remove last newline.

        return tokens

    class Control(FormattedTextControl):
        def move_cursor_down(self):
            python_input.selected_option_index += 1

        def move_cursor_up(self):
            python_input.selected_option_index -= 1

    return Window(
        Control(get_text_fragments),
        style='class:sidebar',
        width=Dimension.exact(43),
        height=Dimension(min=3),
        scroll_offsets=ScrollOffsets(top=1, bottom=1))


def python_sidebar_navigation(python_input):
    """
    Create the `Layout` showing the navigation information for the sidebar.
    """
    def get_text_fragments():
        tokens = []

        # Show navigation info.
        tokens.extend([
            ('class:sidebar', '    '),
            ('class:sidebar.key', '[Arrows]'),
            ('class:sidebar', ' '),
            ('class:sidebar.description', 'Navigate'),
            ('class:sidebar', ' '),
            ('class:sidebar.key', '[Enter]'),
            ('class:sidebar', ' '),
            ('class:sidebar.description', 'Hide menu'),
        ])

        return tokens

    return Window(
        FormattedTextControl(get_text_fragments),
        style='class:sidebar',
        width=Dimension.exact(43),
        height=Dimension.exact(1))


def python_sidebar_help(python_input):
    """
    Create the `Layout` for the help text for the current item in the sidebar.
    """
    token = 'class:sidebar.helptext'

    def get_current_description():
        """
        Return the description of the selected option.
        """
        i = 0
        for category in python_input.options:
            for option in category.options:
                if i == python_input.selected_option_index:
                    return option.description
                i += 1
        return ''

    def get_help_text():
        return [(token, get_current_description())]

    return ConditionalContainer(
        content=Window(
            FormattedTextControl(get_help_text),
            style=token,
            height=Dimension(min=3)),
        filter=ShowSidebar(python_input) &
               Condition(lambda: python_input.show_sidebar_help) & ~is_done)


def signature_toolbar(python_input):
    """
    Return the `Layout` for the signature.
    """
    def get_text_fragments():
        result = []
        append = result.append
        Signature = 'class:signature-toolbar'

        if python_input.signatures:
            sig = python_input.signatures[0]  # Always take the first one.

            append((Signature, ' '))
            try:
                append((Signature, sig.full_name))
            except IndexError:
                # Workaround for #37: https://github.com/jonathanslenders/python-prompt-toolkit/issues/37
                # See also: https://github.com/davidhalter/jedi/issues/490
                return []

            append((Signature + ',operator', '('))

            try:
                enumerated_params = enumerate(sig.params)
            except AttributeError:
                # Workaround for #136: https://github.com/jonathanslenders/ptpython/issues/136
                # AttributeError: 'Lambda' object has no attribute 'get_subscope_by_name'
                return []

            for i, p in enumerated_params:
                # Workaround for #47: 'p' is None when we hit the '*' in the signature.
                #                     and sig has no 'index' attribute.
                # See: https://github.com/jonathanslenders/ptpython/issues/47
                #      https://github.com/davidhalter/jedi/issues/598
                description = (p.description if p else '*') #or '*'
                sig_index = getattr(sig, 'index', 0)

                if i == sig_index:
                    # Note: we use `_Param.description` instead of
                    #       `_Param.name`, that way we also get the '*' before args.
                    append((Signature + ',current-name', str(description)))
                else:
                    append((Signature, str(description)))
                append((Signature + ',operator', ', '))

            if sig.params:
                # Pop last comma
                result.pop()

            append((Signature + ',operator', ')'))
            append((Signature, ' '))
        return result

    return ConditionalContainer(
        content=Window(
            FormattedTextControl(get_text_fragments),
            height=Dimension.exact(1)),
        filter=
            # Show only when there is a signature
            HasSignature(python_input) &
            # And there are no completions to be shown. (would cover signature pop-up.)
            ~(has_completions & (show_completions_menu(python_input) |
                                   show_multi_column_completions_menu(python_input)))
            # Signature needs to be shown.
            & ShowSignature(python_input) &
            # Not done yet.
            ~is_done)


class PythonPromptMargin(PromptMargin):
    """
    Create margin that displays the prompt.
    It shows something like "In [1]:".
    """
    def __init__(self, python_input):
        self.python_input = python_input

        def get_prompt_style():
            return python_input.all_prompt_styles[python_input.prompt_style]

        def get_prompt():
            return to_formatted_text(get_prompt_style().in_prompt())

        def get_continuation(width, line_number, is_soft_wrap):
            if python_input.show_line_numbers and not is_soft_wrap:
                text = ('%i ' % (line_number + 1)).rjust(width)
                return [('class:line-number', text)]
            else:
                return get_prompt_style().in2_prompt(width)

        super(PythonPromptMargin, self).__init__(get_prompt, get_continuation)


def status_bar(python_input):
    """
    Create the `Layout` for the status bar.
    """
    TB = 'class:status-toolbar'

    @if_mousedown
    def toggle_paste_mode(mouse_event):
        python_input.paste_mode = not python_input.paste_mode

    @if_mousedown
    def enter_history(mouse_event):
        python_input.enter_history()

    def get_text_fragments():
        python_buffer = python_input.default_buffer

        result = []
        append = result.append

        append((TB, ' '))
        result.extend(get_inputmode_fragments(python_input))
        append((TB, ' '))

        # Position in history.
        append((TB, '%i/%i ' % (python_buffer.working_index + 1,
                                len(python_buffer._working_lines))))

        # Shortcuts.
        app = get_app()
        if not python_input.vi_mode and app.current_buffer == python_input.search_buffer:
            append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.'))
        elif bool(app.current_buffer.selection_state) and not python_input.vi_mode:
            # Emacs cut/copy keys.
            append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel'))
        else:
            result.extend([
                (TB + ' class:key', '[F3]', enter_history),
                (TB, ' History ', enter_history),
                (TB + ' class:key', '[F6]', toggle_paste_mode),
                (TB, ' ', toggle_paste_mode),
            ])

            if python_input.paste_mode:
                append((TB + ' class:paste-mode-on', 'Paste mode (on)', toggle_paste_mode))
            else:
                append((TB, 'Paste mode', toggle_paste_mode))

        return result

    return ConditionalContainer(
            content=Window(content=FormattedTextControl(get_text_fragments), style=TB),
            filter=~is_done & renderer_height_is_known &
                 Condition(lambda: python_input.show_status_bar and
                                      not python_input.show_exit_confirmation))


def get_inputmode_fragments(python_input):
    """
    Return current input mode as a list of (token, text) tuples for use in a
    toolbar.
    """
    app = get_app()
    @if_mousedown
    def toggle_vi_mode(mouse_event):
        python_input.vi_mode = not python_input.vi_mode

    token = 'class:status-toolbar'
    input_mode_t = 'class:status-toolbar.input-mode'

    mode = app.vi_state.input_mode
    result = []
    append = result.append

    append((input_mode_t, '[F4] ', toggle_vi_mode))

    # InputMode
    if python_input.vi_mode:
        recording_register = app.vi_state.recording_register
        if recording_register:
            append((token, ' '))
            append((token + ' class:record', 'RECORD({})'.format(recording_register)))
            append((token, ' - '))

        if bool(app.current_buffer.selection_state):
            if app.current_buffer.selection_state.type == SelectionType.LINES:
                append((input_mode_t, 'Vi (VISUAL LINE)', toggle_vi_mode))
            elif app.current_buffer.selection_state.type == SelectionType.CHARACTERS:
                append((input_mode_t, 'Vi (VISUAL)', toggle_vi_mode))
                append((token, ' '))
            elif app.current_buffer.selection_state.type == 'BLOCK':
                append((input_mode_t, 'Vi (VISUAL BLOCK)', toggle_vi_mode))
                append((token, ' '))
        elif mode in (InputMode.INSERT, 'vi-insert-multiple'):
            append((input_mode_t, 'Vi (INSERT)', toggle_vi_mode))
            append((token, '  '))
        elif mode == InputMode.NAVIGATION:
            append((input_mode_t, 'Vi (NAV)', toggle_vi_mode))
            append((token, '     '))
        elif mode == InputMode.REPLACE:
            append((input_mode_t, 'Vi (REPLACE)', toggle_vi_mode))
            append((token, ' '))
    else:
        if app.emacs_state.is_recording:
            append((token, ' '))
            append((token + ' class:record', 'RECORD'))
            append((token, ' - '))

        append((input_mode_t, 'Emacs', toggle_vi_mode))
        append((token, ' '))

    return result


def show_sidebar_button_info(python_input):
    """
    Create `Layout` for the information in the right-bottom corner.
    (The right part of the status bar.)
    """
    @if_mousedown
    def toggle_sidebar(mouse_event):
        " Click handler for the menu. "
        python_input.show_sidebar = not python_input.show_sidebar

    version = sys.version_info
    tokens = [
        ('class:status-toolbar.key', '[F2]', toggle_sidebar),
        ('class:status-toolbar', ' Menu', toggle_sidebar),
        ('class:status-toolbar', ' - '),
        ('class:status-toolbar.python-version', '%s %i.%i.%i' % (platform.python_implementation(),
                                               version[0], version[1], version[2])),
        ('class:status-toolbar', ' '),
    ]
    width = fragment_list_width(tokens)

    def get_text_fragments():
        # Python version
        return tokens

    return ConditionalContainer(
        content=Window(
            FormattedTextControl(get_text_fragments),
            style='class:status-toolbar',
            height=Dimension.exact(1),
            width=Dimension.exact(width)),
        filter=~is_done & renderer_height_is_known &
            Condition(lambda: python_input.show_status_bar and
                                  not python_input.show_exit_confirmation))


def exit_confirmation(python_input, style='class:exit-confirmation'):
    """
    Create `Layout` for the exit message.
    """
    def get_text_fragments():
        # Show "Do you really want to exit?"
        return [
            (style, '\n %s ([y]/n)' % python_input.exit_message),
            ('[SetCursorPosition]', ''),
            (style, '  \n'),
        ]

    visible = ~is_done & Condition(lambda: python_input.show_exit_confirmation)

    return ConditionalContainer(
        content=Window(FormattedTextControl(get_text_fragments), style=style),   # , has_focus=visible)),
        filter=visible)


def meta_enter_message(python_input):
    """
    Create the `Layout` for the 'Meta+Enter` message.
    """
    def get_text_fragments():
        return [('class:accept-message', ' [Meta+Enter] Execute ')]

    def extra_condition():
        " Only show when... "
        b = python_input.default_buffer

        return (
            python_input.show_meta_enter_message and
            (not b.document.is_cursor_at_the_end or
                python_input.accept_input_on_enter is None) and
            '\n' in b.text)

    visible = ~is_done & has_focus(DEFAULT_BUFFER) & Condition(extra_condition)

    return ConditionalContainer(
        content=Window(FormattedTextControl(get_text_fragments)),
        filter=visible)


class PtPythonLayout(object):
    def __init__(self, python_input, lexer=PythonLexer, extra_body=None,
                 extra_toolbars=None, extra_buffer_processors=None,
                 input_buffer_height=None):
        D = Dimension
        extra_body = [extra_body] if extra_body else []
        extra_toolbars = extra_toolbars or []
        extra_buffer_processors = extra_buffer_processors or []
        input_buffer_height = input_buffer_height or D(min=6)

        search_toolbar = SearchToolbar(python_input.search_buffer)

        def create_python_input_window():
            def menu_position():
                """
                When there is no autocompletion menu to be shown, and we have a
                signature, set the pop-up position at `bracket_start`.
                """
                b = python_input.default_buffer

                if b.complete_state is None and python_input.signatures:
                    row, col = python_input.signatures[0].bracket_start
                    index = b.document.translate_row_col_to_index(row - 1, col)
                    return index

            return Window(
                BufferControl(
                    buffer=python_input.default_buffer,
                    search_buffer_control=search_toolbar.control,
                    lexer=lexer,
                    include_default_input_processors=False,
                    input_processors=[
                        ConditionalProcessor(
                            processor=HighlightIncrementalSearchProcessor(),
                            filter=has_focus(SEARCH_BUFFER) | has_focus(search_toolbar.control),
                        ),
                        HighlightSelectionProcessor(),
                        DisplayMultipleCursors(),
                        # Show matching parentheses, but only while editing.
                        ConditionalProcessor(
                            processor=HighlightMatchingBracketProcessor(chars='[](){}'),
                            filter=has_focus(DEFAULT_BUFFER) & ~is_done &
                                Condition(lambda: python_input.highlight_matching_parenthesis)),
                        ConditionalProcessor(
                            processor=AppendAutoSuggestion(),
                            filter=~is_done)
                    ] + extra_buffer_processors,
                    menu_position=menu_position,

                    # Make sure that we always see the result of an reverse-i-search:
                    preview_search=True,
                ),
                left_margins=[PythonPromptMargin(python_input)],
                # Scroll offsets. The 1 at the bottom is important to make sure
                # the cursor is never below the "Press [Meta+Enter]" message
                # which is a float.
                scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4),
                # As long as we're editing, prefer a minimal height of 6.
                height=(lambda: (
                    None if get_app().is_done or python_input.show_exit_confirmation
                            else input_buffer_height)),
                wrap_lines=Condition(lambda: python_input.wrap_lines),
            )

        sidebar = python_sidebar(python_input)

        root_container = HSplit([
            VSplit([
                HSplit([
                    FloatContainer(
                        content=HSplit(
                            [create_python_input_window()] + extra_body
                        ),
                        floats=[
                            Float(xcursor=True,
                                  ycursor=True,
                                  content=ConditionalContainer(
                                      content=CompletionsMenu(
                                          scroll_offset=(
                                              lambda: python_input.completion_menu_scroll_offset),
                                          max_height=12),
                                      filter=show_completions_menu(python_input))),
                            Float(xcursor=True,
                                  ycursor=True,
                                  content=ConditionalContainer(
                                      content=MultiColumnCompletionsMenu(),
                                      filter=show_multi_column_completions_menu(python_input))),
                            Float(xcursor=True,
                                  ycursor=True,
                                  content=signature_toolbar(python_input)),
                            Float(left=2,
                                  bottom=1,
                                  content=exit_confirmation(python_input)),
                            Float(bottom=0, right=0, height=1,
                                  content=meta_enter_message(python_input),
                                  hide_when_covering_content=True),
                            Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)),
                        ]),
                    ArgToolbar(),
                    search_toolbar,
                    SystemToolbar(),
                    ValidationToolbar(),
                    ConditionalContainer(
                        content=CompletionsToolbar(),
                        filter=show_completions_toolbar(python_input)),

                    # Docstring region.
                    ConditionalContainer(
                        content=Window(
                            height=D.exact(1),
                            char='\u2500',
                            style='class:separator'),
                        filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done),
                    ConditionalContainer(
                        content=Window(
                            BufferControl(
                                buffer=python_input.docstring_buffer,
                                lexer=SimpleLexer(style='class:docstring'),
                                #lexer=PythonLexer,
                            ),
                            height=D(max=12)),
                        filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done),
                ]),
                ConditionalContainer(
                    content=HSplit([
                        sidebar,
                        Window(style='class:sidebar,separator', height=1),
                        python_sidebar_navigation(python_input),
                    ]),
                    filter=ShowSidebar(python_input) & ~is_done)
            ]),
        ] + extra_toolbars + [
            VSplit([
                status_bar(python_input),
                show_sidebar_button_info(python_input),
            ])
        ])

        self.layout = Layout(root_container)
        self.sidebar = sidebar