"""
Prompt representation.
"""
from __future__ import unicode_literals

from pygments.token import Token
from .enums import IncrementalSearchDirection, InputMode
from .renderer import Screen, Size, Point, Char

__all__ = (
    'HorizontalCompletionMenu',
    'PopupCompletionMenu',
    'Prompt',
    'TokenList',
    'PasswordProcessor',
    'BracketsMismatchProcessor',
)


class TokenList(object):
    """
    Wrapper around (Token, text) tuples.
    Implements logical slice and len operations.
    """
    def __init__(self, iterator=None):
        if iterator is not None:
            self._list = list(iterator)
        else:
            self._list = []

    def __len__(self):
        return sum(len(v) for k, v in self._list)

    def __getitem__(self, val):
        result = []

        for token, string in self._list:
            for c in string:
                result.append((token, c))

        if isinstance(val, slice):
            return TokenList(result[val])
        else:
            return result[val]

    def __iter__(self):
        return iter(self._list)

    def append(self, val):
        self._list.append(val)

    def __add__(self, other):
        return TokenList(self._list + list(other))

    @property
    def text(self):
        return ''.join(p[1] for p in self._list)

    def __repr__(self):
        return 'TokenList(%r)' % self._list


class HorizontalCompletionMenu(object):
    """
    Helper for drawing the completion menu 'wildmenu'-style.
    (Similar to Vim's wildmenu.)
    """
    def write(self, screen, complete_cursor_position, complete_state):
        """
        Write the menu to the screen object.
        """
        completions = complete_state.current_completions
        index = complete_state.complete_index  # Can be None!

        # Don't draw the menu if there is just one completion.
        if len(completions) <= 1:
            return

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

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

        # Create Menu content.
        tokens = TokenList()

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

            tokens.append((Token.HorizontalMenu.Completion.Current if i == index else Token.HorizontalMenu.Completion, c.display))
            tokens.append((Token.HorizontalMenu, ' '))

        # Extend/strip until the content width.
        tokens.append((Token.HorizontalMenu, ' ' * (content_width - len(tokens))))
        tokens = tokens[:content_width]

        # Draw to screen.
        screen.write_highlighted([
            (Token.HorizontalMenu, ' '),
            (Token.HorizontalMenu.Arrow, '<' if cut_left else ' '),
            (Token.HorizontalMenu, ' '),
        ])
        screen.write_highlighted(tokens)
        screen.write_highlighted([
            (Token.HorizontalMenu, ' '),
            (Token.HorizontalMenu.Arrow, '>' if cut_right else ' '),
            (Token.HorizontalMenu, ' '),
        ])


class PopupCompletionMenu(object):
    """
    Helper for drawing the complete menu to the screen.
    """
    current_completion_token = Token.CompletionMenu.Completion.Current
    completion_token = Token.CompletionMenu.Completion

    current_meta_token = Token.CompletionMenu.Meta.Current
    meta_token = Token.CompletionMenu.Meta

    progress_button_token = Token.CompletionMenu.ProgressButton
    progress_bar_token = Token.CompletionMenu.ProgressBar

    def __init__(self, max_height=5):
        self.max_height = max_height

    def get_height(self, complete_state):
        """
        Return the height of the menu. (Number of rows it will use.)
        """
        return min(self.max_height, len(complete_state.current_completions))

    def write(self, screen, complete_cursor_position, complete_state):
        """
        Write the menu to the screen object.
        """
        completions = complete_state.current_completions
        index = complete_state.complete_index  # Can be None!

        # Get position of the menu.
        y, x = complete_cursor_position
        y += 1
        x = max(0, x - 1)  # XXX: Don't draw it in the right margin!!!...

        # Calculate width of completions menu.
        menu_width = self.get_menu_width(screen, complete_state)
        menu_meta_width = self.get_menu_meta_width(screen, complete_state)
        show_meta = self.show_meta(complete_state)

        # Decide which slice of completions to show.
        if len(completions) > self.max_height and (index or 0) > self.max_height / 2:
            slice_from = min(
                (index or 0) - self.max_height // 2,  # In the middle.
                len(completions) - self.max_height  # At the bottom.
            )
        else:
            slice_from = 0

        slice_to = min(slice_from + self.max_height, len(completions))

        # Create a function which decides at which positions the scroll button should be shown.
        def is_scroll_button(row):
            items_per_row = float(len(completions)) / min(len(completions), self.max_height)
            items_on_this_row_from = row * items_per_row
            items_on_this_row_to = (row + 1) * items_per_row
            return items_on_this_row_from <= (index or 0) < items_on_this_row_to

        # Write completions to screen.
        for i, c in enumerate(completions[slice_from:slice_to]):
            is_current_completion = (i + slice_from == index)

            if is_scroll_button(i):
                button_token = self.progress_button_token
            else:
                button_token = self.progress_bar_token

            tokens = ([(Token, ' ')] +
                      self.get_menu_item_tokens(c, is_current_completion, menu_width) +
                      (self.get_menu_item_meta_tokens(c, is_current_completion, menu_meta_width)
                          if show_meta else []) +
                      [(button_token, ' '), (Token, ' ')])

            screen.write_highlighted_at_pos(y+i, x, tokens, z_index=10)

    def show_meta(self, complete_state):
        """
        Return ``True`` if we need to show a column with meta information.
        """
        return any(c.display_meta for c in complete_state.current_completions)

    def get_menu_width(self, screen, complete_state):
        """
        Return the width of the main column.
        """
        max_display = int(screen.size.columns / 2)
        return min(max_display, max(len(c.display) for c in complete_state.current_completions)),

    def get_menu_meta_width(self, screen, complete_state):
        """
        Return the width of the meta column.
        """
        max_display_meta = int(screen.size.columns / 4)
        return min(max_display_meta, max(len(c.display_meta) for c in complete_state.current_completions))

    def get_menu_item_tokens(self, completion, is_current_completion, width):
        if is_current_completion:
            token = self.current_completion_token
        else:
            token = self.completion_token

        return [(token, ' %%-%is ' % width % completion.display)]

    def get_menu_item_meta_tokens(self, completion, is_current_completion, width):
        if is_current_completion:
            token = self.current_meta_token
        else:
            token = self.meta_token

        return [(token, ' %%-%is ' % width % completion.display_meta or 'none')]


class PasswordProcessor(object):
    """
    Processor that turns masks the input. (For passwords.)
    """
    def __init__(self, char='*'):
        self.char = char

    def process_tokens(self, tokens):
        return [(token, self.char * len(text)) for token, text in tokens]


class BracketsMismatchProcessor(object):
    """
    Processor that replaces the token type of bracket mismatches by an Error.
    """
    error_token = Token.Error

    def process_tokens(self, tokens):
        tokens = list(TokenList(tokens))

        stack = []  # Pointers to the result array

        for index, (token, text) in enumerate(tokens):
            top = tokens[stack[-1]][1] if stack else ''

            if text in '({[]})':
                if text in '({[':
                    # Put open bracket on the stack
                    stack.append(index)

                elif (text == ')' and top == '(' or
                      text == '}' and top == '{' or
                      text == ']' and top == '['):
                    # Match found
                    stack.pop()
                else:
                    # No match for closing bracket.
                    tokens[index] = (self.error_token, text)

        # Highlight unclosed tags that are still on the stack.
        for index in stack:
            tokens[index] = (Token.Error, tokens[index][1])

        return tokens


class ISearchComposer(object):
    def __init__(self, isearch_state):
        self.isearch_state = isearch_state

    @property
    def before(self):
        if self.isearch_state.isearch_direction == IncrementalSearchDirection.BACKWARD:
            text = 'reverse-i-search'
        else:
            text = 'i-search'

        return [(Token.Prompt.ISearch, '(%s)`' % text)]

    @property
    def text(self):
        index = self.isearch_state.no_match_from_index
        text = self.isearch_state.isearch_text

        if index is None:
            return [(Token.Prompt.ISearch.Text, text)]
        else:
            return [
                (Token.Prompt.ISearch.Text, text[:index]),
                (Token.Prompt.ISearch.Text.NoMatch, text[index:])
            ]

    @property
    def after(self):
        return [(Token.Prompt.ISearch, '`: ')]

    def get_tokens(self):
        return self.before + self.text + self.after


class Prompt(object):
    """
    Default prompt class.
    """
    #: Menu class for autocompletions. This can be `None`
    completion_menu = PopupCompletionMenu()

    #: Text to show before the input
    prompt_text = '> '

    #: Text for in the left margin in case of a multiline prompt.
    prompt_text2 = '> '

    #: Processors for transforming the tokens received from the `Code` object.
    #: (This can be used for displaying password input as '*' or for
    #: highlighting mismatches of brackets in case of Python input.)
    input_processors = []  # XXX: rename to something else !!!!!

    #: Class responsible for the composition of the i-search tokens.
    isearch_composer = ISearchComposer

    def __init__(self,cli_ref):
        self._cli_ref = cli_ref
        self.reset()

    def reset(self):
        #: Vertical scrolling position of the main content.
        self.vertical_scroll = 0

    @property
    def cli(self):
        """
        The :class:`CommandLineInterface` instance.
        """
        return self._cli_ref()

    @property
    def line(self):
        """
        The main :class:`Line` instance.
        """
        return self.cli.line

    def get_tokens_before_input(self):
        """
        Text shown before the actual input.
        List of (Token, text) tuples.
        """
        if self.cli.input_processor.input_mode == InputMode.INCREMENTAL_SEARCH and self.line.isearch_state:
            return self.isearch_prompt
        elif self.cli.input_processor.input_mode == InputMode.VI_SEARCH:
            return self.vi_search_prompt
        elif self.cli.input_processor.arg is not None:
            return self.arg_prompt
        else:
            return self.default_prompt

    @property
    def default_prompt(self):
        """
        Tokens for the default prompt.
        """
        return [(Token.Prompt, self.prompt_text)]

    @property
    def arg_prompt(self):
        """
        Tokens for the arg-prompt.
        """
        return [
            (Token.Prompt.Arg, '(arg: '),
            (Token.Prompt.Arg.Text, str(self.cli.input_processor.arg)),
            (Token.Prompt.Arg, ') '),
        ]

    @property
    def isearch_prompt(self):
        """
        Tokens for the prompt when we go in reverse-i-search mode.
        """
        if self.line.isearch_state:
            return self.isearch_composer(self.line.isearch_state).get_tokens()
        else:
            return []

    @property
    def vi_search_prompt(self):
        # TODO
        return []

    def get_vi_search_prefix_tokens(self):
        """
        Tokens for the vi-search prompt.
        """
        if self.line.isearch_state.isearch_direction == IncrementalSearchDirection.BACKWARD:
            prefix = '?'
        else:
            prefix = '/'

        return [(Token.Prompt.ViSearch, prefix)]

    def get_tokens_after_input(self):
        """
        List of (Token, text) tuples for after the inut.
        (This can be used to create a help text or a status line.)
        """
        return []

    def get_input_tokens(self):
        tokens = list(self.line.create_code().get_tokens())

        for p in self.input_processors:
            tokens = p.process_tokens(tokens)

        return tokens

    def get_left_margin_tokens(self, line_number, margin_width):
        return [
            (Token.Prompt, self.prompt_text2)
        ]

    @property
    def left_margin_width(self):
        return len(self.prompt_text2)

    def get_highlighted_characters(self):
        """
        Return a dictionary that maps the index of input string characters to
        their Token in case of highlighting.
        """
        highlighted_characters = {}

        # In case of incremental search, highlight all matches.
        if self.line.isearch_state:
            for index in self.line.document.find_all(self.line.isearch_state.isearch_text):
                if index == self.line.cursor_position:
                    token = Token.IncrementalSearchMatch.Current
                else:
                    token = Token.IncrementalSearchMatch

                highlighted_characters.update({
                    x: token for x in range(index, index + len(self.line.isearch_state.isearch_text))
                })

        # In case of selection, highlight all matches.
        selection_range = self.line.document.selection_range()
        if selection_range:
            from_, to = selection_range

            for i in range(from_, to):
                highlighted_characters[i] = Token.SelectedText

        return highlighted_characters

    def write_vi_search(self, screen):
        screen.write_highlighted(self.get_vi_search_prefix_tokens())

        line = self.cli.lines['search']

        for index, c in enumerate(line.text + ' '):
            screen.write_char(c, Token.Prompt.ViSearch.Text,
                              set_cursor_position=(index == line.cursor_position))

    def write_before_input(self, screen):
        screen.write_highlighted(self.get_tokens_before_input())

    def write_input(self, screen, highlight=True):
        # Get tokens
        # Note: we add the space character at the end, because that's where
        #       the cursor can also be.
        input_tokens = self.get_input_tokens() + [(Token, ' ')]

        # 'Explode' tokens in characters.
        input_tokens = [(token, c) for token, text in input_tokens for c in text]

        # Apply highlighting.
        if highlight:
            highlighted_characters = self.get_highlighted_characters()

            for index, token in highlighted_characters.items():
                input_tokens[index] = (token, input_tokens[index][1])

        for index, (token, c) in enumerate(input_tokens):
            # Insert char.
            screen.write_char(c, token,
                              string_index=index,
                              set_cursor_position=(index == self.line.cursor_position))

    def write_input_scrolled(self, screen, accept_or_abort=False, min_available_height=1, left_margin_width=0,
            bottom_margin_height=0):
        """
        Write visible part of the input to the screen. (Scroll if the input is
        too large.)

        :return: Cursor row position after the scroll region.
        """
        # Write to a temp screen first. (Later, we will copy the visible region
        # of this screen to the real screen.)
        temp_screen = Screen(Size(columns=screen.size.columns - left_margin_width,
                                  rows=screen.size.rows))
        self.write_input(temp_screen, highlight=not accept_or_abort)

        # Determine the maximum height.
        max_height = screen.size.rows - 2

        # Scroll.
        if True:
            # Scroll back if we scrolled to much and there's still space at the top.
            if self.vertical_scroll > temp_screen.current_height - max_height:
                self.vertical_scroll = max(0, temp_screen.current_height - max_height)

            # Scroll up if cursor is before visible part.
            if self.vertical_scroll > temp_screen.cursor_position.y:
                self.vertical_scroll = temp_screen.cursor_position.y

            # Scroll down if cursor is after visible part.
            if self.vertical_scroll <= temp_screen.cursor_position.y - max_height:
                self.vertical_scroll = (temp_screen.cursor_position.y + 1) - max_height

            # Scroll down if we need space for the menu.
            if self.need_to_show_completion_menu():
                menu_size = self.completion_menu.get_height(self.line.complete_state) - 1
                if temp_screen.cursor_position.y - self.vertical_scroll >= max_height - menu_size:
                    self.vertical_scroll = (temp_screen.cursor_position.y + 1) - (max_height - menu_size)

        # Now copy the region we need to the real screen.
        y = 0
        for y in range(0, min(max_height, temp_screen.current_height - self.vertical_scroll)):
            # Write line numbers.
            screen.write_highlighted_at_pos(y, 0, self.get_left_margin_tokens(
                y + self.vertical_scroll, left_margin_width))

            # Write line content.
            for x in range(0, temp_screen.size.columns):
                screen._buffer[y][x + left_margin_width] = temp_screen._buffer[y + self.vertical_scroll][x]

        screen.cursor_position = Point(y=temp_screen.cursor_position.y - self.vertical_scroll,
                                       x=temp_screen.cursor_position.x + left_margin_width)

        y_after_input = y

        # Show completion menu.
        if not accept_or_abort and self.need_to_show_completion_menu():
            y, x = temp_screen._cursor_mappings[self.line.complete_state.original_document.cursor_position]
            self.completion_menu.write(screen, (y - self.vertical_scroll, x + left_margin_width), self.line.complete_state)

        return_value = max([min_available_height, screen.current_height]) - bottom_margin_height

        # Fill up with tildes.
        if not accept_or_abort:
            y = y_after_input + 1
            while y < max([min_available_height, screen.current_height]) - bottom_margin_height:
                screen.write_at_pos(y, 1, Char('~', Token.Leftmargin.Tilde))
                y += 1

        return return_value

    def write_after_input(self, screen):
        """
        Write tokens after input.
        """
        screen.write_highlighted(self.get_tokens_after_input())

    def need_to_show_completion_menu(self):
        return self.completion_menu and self.line.complete_state

    def write_menus(self, screen):
        """
        Write completion menu.
        """
        if self.need_to_show_completion_menu():
            # Calculate the position where the cursor was, the moment that we pressed the complete button (tab).
            complete_cursor_position = screen._cursor_mappings[self.line.complete_state.original_document.cursor_position]

            self.completion_menu.write(screen, complete_cursor_position, self.line.complete_state)

    def write_to_screen(self, screen, min_available_height, accept=False, abort=False):
        """
        Render the prompt to a `Screen` instance.

        :param screen: The :class:`Screen` class into which we write the output.
        :param min_available_height: The space (amount of rows) available from
                                     the top of the prompt, until the bottom of
                                     the terminal. We don't have to use them,
                                     but we can.
        """
        if self.cli.input_processor.input_mode == InputMode.VI_SEARCH:
            self.write_vi_search(screen)
        else:
            self.write_before_input(screen)

            # Write actual input.
            if self.line.is_multiline:
                self.write_input_scrolled(screen,
                                      accept_or_abort=(accept or abort),
                                      min_available_height=min_available_height,
                                      left_margin_width=self.left_margin_width)
            else:
                self.write_input(screen, highlight=not (accept or abort))



        if not (accept or abort):
            self.write_after_input(screen)
            self.write_menus(screen)
