# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import urwid

from gertty import keymap
from gertty.view import mouse_scroll_decorator

GLOBAL_HELP = (
    (keymap.HELP,
     "Display help"),
    (keymap.PREV_SCREEN,
     "Back to previous screen"),
    (keymap.TOP_SCREEN,
     "Back to project list"),
    (keymap.QUIT,
     "Quit Gertty"),
    (keymap.CHANGE_SEARCH,
     "Search for changes"),
    (keymap.LIST_HELD,
     "List held changes"),
    (keymap.KILL,
     "Kill to end of line (editing)"),
    (keymap.YANK,
     "Yank from kill ring (editing)"),
    (keymap.YANK_POP,
     "Replace previous yank from kill ring (editing)"),
    )

class TextButton(urwid.Button):
    def selectable(self):
        return True

    def __init__(self, text, on_press=None, user_data=None):
        super(TextButton, self).__init__('', on_press=on_press, user_data=user_data)
        self.text = urwid.SelectableIcon(text)
        self._w = urwid.AttrMap(self.text, None, focus_map='focused')

class FixedButton(urwid.Button):
    def sizing(self):
        return frozenset([urwid.FIXED])

    def pack(self, size, focus=False):
        return (len(self.get_label())+4, 1)

class FixedRadioButton(urwid.RadioButton):
    def sizing(self):
        return frozenset([urwid.FIXED])

    def pack(self, size, focus=False):
        return (len(self.get_label())+4, 1)

class TableColumn(urwid.Pile):
    def sizing(self):
        """Explicit declare flow sizing due to the custom pack method."""
        return frozenset((urwid.FLOW,))

    def pack(self, size, focus=False):
        maxcol = size[0]
        mx = max([i[0].pack((maxcol,), focus)[0] for i in self.contents])
        return (min(mx+2, maxcol), len(self.contents))

class Table(urwid.WidgetWrap):
    def __init__(self, headers=[], columns=None):
        if columns is None:
            cols = [('pack', TableColumn([('pack', w)])) for w in headers]
        else:
            cols = [('pack', TableColumn([])) for x in range(columns)]
        super(Table, self).__init__(
            urwid.Columns(cols))

    def addRow(self, cells=[]):
        for i, widget in enumerate(cells):
            self._w.contents[i][0].contents.append((widget, ('pack', None)))

class KillRing(object):
    def __init__(self):
        self.ring = []

    def kill(self, text):
        self.ring.append(text)

    def yank(self, repeat=False):
        if not self.ring:
            return None
        if repeat:
            t = self.ring.pop()
            self.ring.insert(0, t)
        return self.ring[-1]

class MyEdit(urwid.Edit):
    def __init__(self, *args, **kw):
        self.ring = kw.pop('ring', None)
        if not self.ring:
            self.ring = KillRing()
        self.last_yank = None
        super(MyEdit, self).__init__(*args, **kw)

    def keypress(self, size, key):
        if self._command_map[key] == keymap.YANK:
            text = self.ring.yank()
            if text:
                self.last_yank = (self.edit_pos, self.edit_pos+len(text))
                self.insert_text(text)
            return
        if self._command_map[key] == keymap.YANK_POP:
            if not self.last_yank:
                return
            text = self.ring.yank(True)
            if text:
                self.edit_text = (self.edit_text[:self.last_yank[0]] +
                                  self.edit_text[self.last_yank[1]:])
                self.last_yank = (self.edit_pos, self.edit_pos+len(text))
                self.insert_text(text)
            return
        self.last_yank = None
        if self._command_map[key] == keymap.KILL:
            text = self.edit_text[self.edit_pos:]
            self.edit_text = self.edit_text[:self.edit_pos]
            self.ring.kill(text)
        return super(MyEdit, self).keypress(size, key)

class LineBoxTitlePropertyMixin(object):

    @property
    def title(self):
        return self._w.title_widget.text.strip()

    @title.setter
    def title(self, text):
        return self._w.set_title(text)

class SystemMessage(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
    def __init__(self, message):
        w = urwid.Filler(urwid.Text(message, align='center'))
        super(SystemMessage, self).__init__(urwid.LineBox(w, u'System Message'))

@mouse_scroll_decorator.ScrollByWheel
class ButtonDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
    def __init__(self, title, message, entry_prompt=None,
                 entry_text='', buttons=[], ring=None):
        button_widgets = []
        for button in buttons:
            button_widgets.append(('pack', button))
        button_columns = urwid.Columns(button_widgets, dividechars=2)
        rows = []
        rows.append(urwid.Text(message))
        if entry_prompt:
            self.entry = MyEdit(entry_prompt, edit_text=entry_text, ring=ring)
            rows.append(self.entry)
        else:
            self.entry = None
        rows.append(urwid.Divider())
        rows.append(button_columns)
        listbox = urwid.ListBox(rows)
        super(ButtonDialog, self).__init__(urwid.LineBox(listbox, title))

class LineEditDialog(ButtonDialog):
    signals = ['save', 'cancel']
    def __init__(self, app, title, message, entry_prompt=None,
                 entry_text='', ring=None):
        self.app = app
        save_button = FixedButton('Save')
        cancel_button = FixedButton('Cancel')
        urwid.connect_signal(save_button, 'click',
                             lambda button:self._emit('save'))
        urwid.connect_signal(cancel_button, 'click',
                             lambda button:self._emit('cancel'))
        super(LineEditDialog, self).__init__(title, message, entry_prompt,
                                             entry_text,
                                             buttons=[save_button,
                                                      cancel_button],
                                             ring=ring)

    def keypress(self, size, key):
        if not self.app.input_buffer:
            key = super(LineEditDialog, self).keypress(size, key)
        keys = self.app.input_buffer + [key]
        commands = self.app.config.keymap.getCommands(keys)
        if keymap.ACTIVATE in commands:
            self._emit('save')
            return None
        return key

class TextEditDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
    signals = ['save', 'cancel']
    def __init__(self, title, prompt, button, text, ring=None):
        save_button = FixedButton(button)
        cancel_button = FixedButton('Cancel')
        urwid.connect_signal(save_button, 'click',
                             lambda button:self._emit('save'))
        urwid.connect_signal(cancel_button, 'click',
                             lambda button:self._emit('cancel'))
        button_widgets = [('pack', save_button),
                          ('pack', cancel_button)]
        button_columns = urwid.Columns(button_widgets, dividechars=2)
        rows = []
        self.entry = MyEdit(edit_text=text, multiline=True, ring=ring)
        rows.append(urwid.Text(prompt))
        rows.append(self.entry)
        rows.append(urwid.Divider())
        rows.append(button_columns)
        pile = urwid.Pile(rows)
        fill = urwid.Filler(pile, valign='top')
        super(TextEditDialog, self).__init__(urwid.LineBox(fill, title))

class MessageDialog(ButtonDialog):
    signals = ['close']
    def __init__(self, title, message):
        ok_button = FixedButton('OK')
        urwid.connect_signal(ok_button, 'click',
                             lambda button:self._emit('close'))
        super(MessageDialog, self).__init__(title, message, buttons=[ok_button])

class YesNoDialog(ButtonDialog):
    signals = ['yes', 'no']
    def __init__(self, title, message):
        yes_button = FixedButton('Yes')
        no_button = FixedButton('No')
        urwid.connect_signal(yes_button, 'click',
                             lambda button:self._emit('yes'))
        urwid.connect_signal(no_button, 'click',
                             lambda button:self._emit('no'))
        super(YesNoDialog, self).__init__(title, message, buttons=[yes_button,
                                                                   no_button])
    def keypress(self, size, key):
        r = super(YesNoDialog, self).keypress(size, key)
        if r in ('Y', 'y'):
            self._emit('yes')
            return None
        if r in ('N', 'n'):
            self._emit('no')
            return None
        return r

class SearchableText(urwid.Text):
    def set_text(self, markup):
        self._markup = markup
        super(SearchableText, self).set_text(markup)

    def search(self, search, attribute):
        if not search:
            self.set_text(self._markup)
            return
        (text, attrs) = urwid.util.decompose_tagmarkup(self._markup)
        last = 0
        found = False
        while True:
            start = text.find(search, last)
            if start < 0:
                break
            found = True
            end = start + len(search)
            i = 0
            newattrs = []
            for attr, al in attrs:
                if i + al <= start:
                    i += al
                    newattrs.append((attr, al))
                    continue
                if i >= end:
                    i += al
                    newattrs.append((attr, al))
                    continue
                before = max(start - i, 0)
                after = max(i + al - end, 0)
                if before:
                    newattrs.append((attr, before))
                newattrs.append((attribute, len(search)))
                if after:
                    newattrs.append((attr, after))
                i += al
            if i < start:
                newattrs.append((None, start-i))
                i += start-i
            if i < end:
                newattrs.append((attribute, len(search)))
            last = start + 1
            attrs = newattrs
        self._text = text
        self._attrib = attrs
        self._invalidate()
        return found

class Searchable(object):
    def searchInit(self):
        self.search = None
        self.results = []
        self.current_result = 0

    def searchValidChar(self, ch):
        return urwid.util.is_wide_char(ch, 0) or (len(ch) == 1 and ord(ch) >= 32)

    def searchKeypress(self, size, key):
        if self.search is not None:
            if self.searchValidChar(key) or key == 'backspace':
                if key == 'backspace':
                    self.search = self.search[:-1]
                else:
                    self.search += key
                self.interactiveSearch(self.search)
                return True
            else:
                commands = self.app.config.keymap.getCommands([key])
                if keymap.INTERACTIVE_SEARCH in commands:
                    self.nextSearchResult()
                    return True
                else:
                    self.app.status.update(title=self.title)
                    if not self.search:
                        self.interactiveSearch(None)
                    self.search = None
                    if key in ['enter', 'esc']:
                        return True
        return False

    def searchStart(self):
        self.search = ''
        self.app.status.update(title=("Search: "))

    def interactiveSearch(self, search):
        if search is not None:
            self.app.status.update(title=("Search: " + search))
        self.results = []
        self.current_result = 0
        for i, line in enumerate(self.listbox.body):
            if hasattr(line, 'search'):
                if line.search(search, 'search-result'):
                    self.results.append(i)

    def nextSearchResult(self):
        if not self.results:
            return
        dest = self.results[self.current_result]
        self.listbox.set_focus(dest)
        self.listbox._invalidate()
        self.current_result += 1
        if self.current_result >= len(self.results):
            self.current_result = 0

class HyperText(urwid.Text):
    _selectable = True

    def sizing(self):
        """Explicit declare flow sizing due to the custom pack method.

        Normal Text can be rendered as FIXED.
        """
        return frozenset((urwid.FLOW,))

    def __init__(self, markup, align=urwid.LEFT, wrap=urwid.SPACE, layout=None):
        self._mouse_press_item = None
        self.selectable_items = []
        self.focused_index = None
        self.last_focused_index = 0
        super(HyperText, self).__init__(markup, align, wrap, layout)

    def focusFirstItem(self):
        if len(self.selectable_items) == 0:
            return False
        self.focusItem(0)
        return True

    def focusLastItem(self):
        if len(self.selectable_items) == 0:
            return False
        self.focusItem(len(self.selectable_items)-1)
        return True

    def focusPreviousItem(self):
        if len(self.selectable_items) == 0:
            return False
        if self.focused_index is None:
            self.focusItem(self.last_focused_index)
        item = max(0, self.focused_index-1)
        if item != self.focused_index:
            self.focusItem(item)
            return True
        return False

    def focusNextItem(self):
        if len(self.selectable_items) == 0:
            return False
        if self.focused_index is None:
            self.focusItem(self.last_focused_index)
        item = min(len(self.selectable_items)-1, self.focused_index+1)
        if item != self.focused_index:
            self.focusItem(item)
            return True
        return False

    def focusItem(self, item):
        self.last_focused_index = self.focused_index
        self.focused_index = item
        self.set_text(self._markup)
        self._invalidate()

    def select(self):
        if self.focused_index is not None:
            self.selectable_items[self.focused_index][0].select()

    def keypress(self, size, key):
        if self._command_map[key] == urwid.CURSOR_UP:
            if self.focusPreviousItem():
                return False
            return key
        elif self._command_map[key] == urwid.CURSOR_DOWN:
            if self.focusNextItem():
                return False
            return key
        elif self._command_map[key] == urwid.ACTIVATE:
            self.select()
            return False
        return key

    def getPosAtCoords(self, maxcol, col, row):
        trans = self.get_line_translation(maxcol)
        colpos = 0
        line = None
        try:
            line = trans[row]
        except IndexError:
            return None
        for t in line:
            if len(t) == 2:
                width, pos = t
                if colpos <= col < colpos + width:
                    return pos
            else:
                width, start, end = t
                if colpos <= col < colpos + width:
                    return start + (col - colpos)
            colpos += width
        return None

    def getItemAtCoords(self, maxcol, col, row):
        pos = self.getPosAtCoords(maxcol, col, row)
        index = 0
        for item, start, end in self.selectable_items:
            if pos is not None and start <= pos <= end:
                return index
            index += 1
        return None

    def mouse_event(self, size, event, button, col, row, focus):
        if ((button not in [0, 1]) or
            (event not in ['mouse press', 'mouse release'])):
            return False
        item = self.getItemAtCoords(size[0], col, row)
        if item is None:
            if self.focused_index is None:
                self.focusFirstItem()
            return False
        if event == 'mouse press':
            self.focusItem(item)
            self._mouse_press_item = item
        if event == 'mouse release':
            if self._mouse_press_item == item:
                self.select()
            self._mouse_press_item = None
        return True

    def processLinks(self, markup, data=None):
        if data is None:
            data = dict(pos=0)
        if isinstance(markup, list):
            return [self.processLinks(i, data) for i in markup]
        if isinstance(markup, tuple):
            return (markup[0], self.processLinks(markup[1], data))
        if isinstance(markup, Link):
            self.selectable_items.append((markup, data['pos'], data['pos']+len(markup.text)))
            data['pos'] += len(markup.text)
            focused = len(self.selectable_items)-1 == self.focused_index
            link_attr = markup.getAttr(focused)
            if link_attr:
                return (link_attr, markup.text)
            else:
                return markup.text
        data['pos'] += len(markup)
        return markup

    def get_cursor_coords(self, size):
        if self.focused_index is None:
            cursor_pos = 0
            return None
        else:
            item, start, end = self.selectable_items[self.focused_index]
            cursor_pos = start

        (maxcol,) = size
        trans = self.get_line_translation(maxcol)
        x, y = urwid.text_layout.calc_coords(self.text, trans, cursor_pos)
        if maxcol <= x:
            return None

        return x, y

    def set_text(self, markup):
        self._markup = markup
        self.selectable_items = []
        super(HyperText, self).set_text(self.processLinks(markup))

    def move_cursor_to_coords(self, size, col, row):
        if self.focused_index is None:
            if row:
                self.focusLastItem()
            else:
                self.focusFirstItem()
        return True

    def render(self, size, focus=False):
        if (not focus) and (self.focused_index is not None):
            self.focusItem(None)
        ret = super(HyperText, self).render(size, focus)
        if focus:
            ret = urwid.canvas.CompositeCanvas(ret)
            ret.cursor = self.get_cursor_coords(size)
        return ret

class Link(urwid.Widget):
    signals = ['selected']

    def __init__(self, text, attr=None, focused_attr=None):
        self.text = text
        self.attr = attr
        self.focused_attr = focused_attr

    def select(self):
        self._emit('selected')

    def getAttr(self, focus):
        if focus:
            return self.focused_attr
        return self.attr
