# Source code for joy.vui.text_viewer

    # -*- coding: utf-8 -*-
    #
    #    Copyright © 2019 Simon Forman
    #
    #    This file is part of Thun
    #
    #    Thun is free software: you can redistribute it and/or modify
    #    it under the terms of the GNU General Public License as published by
    #    the Free Software Foundation, either version 3 of the License, or
    #    (at your option) any later version.
    #
    #    Thun is distributed in the hope that it will be useful,
    #    but WITHOUT ANY WARRANTY; without even the implied warranty of
    #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #    GNU General Public License for more details.
    #
    #    You should have received a copy of the GNU General Public License
    #    along with Thun.  If not see <http://www.gnu.org/licenses/>.
    #
    '''

    Text Viewer
    =================

    '''
    import string
    import pygame
    from joy.utils.stack import expression_to_string
    from joy.vui.core import (
        ARROW_KEYS,
        BACKGROUND as BG,
        FOREGROUND as FG,
        CommandMessage,
        ModifyMessage,
        OpenMessage,
        SUCCESS,
        push,
        )
    from joy.vui import viewer, font_data
    #reload(viewer)

    MenuViewer = viewer.MenuViewer

    SELECTION_COLOR = 235, 255, 0, 32
    SELECTION_KEYS = {
        pygame.K_F1,
        pygame.K_F2,
        pygame.K_F3,
        pygame.K_F4,
        }
    STACK_CHATTER_KEYS = {
        pygame.K_F5,
        pygame.K_F6,
        pygame.K_F7,
        pygame.K_F8,
        }

    def _is_command(display, word):
        return display.lookup(word) or word.isdigit() or all(
            not s or s.isdigit() for s in word.split('.', 1)
            ) and len(word) > 1

    def format_stack_item(content):
        if isinstance(content, tuple):
            return '[%s]' % expression_to_string(content)
        return str(content)

    class Font(object):

        IMAGE = pygame.image.load(font_data.data, 'Iosevka12.BMP')
        LOOKUP = (string.ascii_letters +
                  string.digits +
                  '''@#$&_~|`'"%^=-+*/\\<>[]{}(),.;:!?''')

        def __init__(self, char_w=8, char_h=19, line_h=19):
            self.char_w = char_w
            self.char_h = char_h
            self.line_h = line_h

        def size(self, text):
            return self.char_w * len(text), self.line_h

        def render(self, text):
            surface = pygame.Surface(self.size(text))
            surface.fill(BG)
            x = 0
            for ch in text:
                if not ch.isspace():
                    try:
                        i = self.LOOKUP.index(ch)
                    except ValueError:
                        # render a lil box...
                        r = (x + 1, self.line_h / 2 - 3,
                             self.char_w - 2, self.line_h / 2)
                        pygame.draw.rect(surface, FG, r, 1)
                    else:
                        iy, ix = divmod(i, 26)
                        ix *= self.char_w
                        iy *= self.char_h
                        area = ix, iy, self.char_w, self.char_h
                        surface.blit(self.IMAGE, (x, 0), area)
                x += self.char_w
            return surface

        def __contains__(self, char):
            assert len(char) == 1, repr(char)
            return char in self.LOOKUP

    FONT = Font()

    class TextViewer(MenuViewer):

        MINIMUM_HEIGHT = FONT.line_h + 3
        CLOSE_TEXT = FONT.render('close')
        GROW_TEXT = FONT.render('grow')

        class Cursor(object):

            def __init__(self, viewer):
                self.v = viewer
                self.x = self.y = 0
                self.w, self.h = 2, FONT.line_h
                self.mem = pygame.Surface((self.w, self.h))
                self.can_fade = False

            def set_to(self, x, y):
                self.fade()
                self.x, self.y = x, y
                self.draw()

            def draw(self):
                r = self.x * FONT.char_w, self.screen_y(), self.w, self.h
                self.mem.blit(self.v.body_surface, (0, 0), r)
                self.v.body_surface.fill(FG, r)
                self.can_fade = True

            def fade(self):
                if self.can_fade:
                    dest = self.x * FONT.char_w, self.screen_y()
                    self.v.body_surface.blit(self.mem, dest)
                    self.can_fade = False

            def screen_y(self, row=None):
                if row is None: row = self.y
                return (row - self.v.at_line) * FONT.line_h

            def up(self, _mod):
                if self.y:
                    self.fade()
                    self.y -= 1
                    self.x = min(self.x, len(self.v.lines[self.y]))
                    self.draw()

            def down(self, _mod):
                if self.y < len(self.v.lines) - 1:
                    self.fade()
                    self.y += 1
                    self.x = min(self.x, len(self.v.lines[self.y]))
                    self.draw()
                    self._check_scroll()

            def left(self, _mod):
                if self.x:
                    self.fade()
                    self.x -= 1
                    self.draw()
                elif self.y:
                    self.fade()
                    self.y -= 1
                    self.x = len(self.v.lines[self.y])
                    self.draw()
                    self._check_scroll()

            def right(self, _mod):
                if self.x < len(self.v.lines[self.y]):
                    self.fade()
                    self.x += 1
                    self.draw()
                elif self.y < len(self.v.lines) - 1:
                    self.fade()
                    self.y += 1
                    self.x = 0
                    self.draw()
                    self._check_scroll()

            def _check_scroll(self):
                if self.y < self.v.at_line:
                    self.v.scroll_down()
                elif self.y > self.v.at_line + self.v.h_in_lines:
                    self.v.scroll_up()

        def __init__(self, surface):
            self.cursor = self.Cursor(self)
            MenuViewer.__init__(self, surface)
            self.lines = ['']
            self.content_id = None
            self.at_line = 0
            self.bg = BG
            self.command = self.command_rect = None
            self._sel_start = self._sel_end = None

        def resurface(self, surface):
            self.cursor.fade()
            MenuViewer.resurface(self, surface)

            w, h = self.CLOSE_TEXT.get_size()
            self.close_rect = pygame.rect.Rect(self.w - 2 - w, 1, w, h)
            w, h = self.GROW_TEXT.get_size()
            self.grow_rect = pygame.rect.Rect(1, 1, w, h)

            self.body_surface = surface.subsurface(self.body_rect)
            self.line_w = self.body_rect.w / FONT.char_w + 1
            self.h_in_lines = self.body_rect.h / FONT.line_h - 1
            self.command_rect = self.command = None
            self._sel_start = self._sel_end = None

        def handle(self, message):
            if super(TextViewer, self).handle(message):
                return
            if (isinstance(message, ModifyMessage)
                and message.subject is self.lines
                ):
                # TODO: check self.at_line
                self.draw_body()

        # Drawing

        def draw_menu(self):
            #MenuViewer.draw_menu(self)
            self.surface.blit(self.GROW_TEXT, (1, 1))
            self.surface.blit(self.CLOSE_TEXT,
                              (self.w - 2 - self.close_rect.w, 1))
            if self.content_id:
                self.surface.blit(FONT.render('| ' + self.content_id),
                              (self.grow_rect.w + FONT.char_w + 3, 1))
            self.surface.fill( # light grey background
                (196, 196, 196),
                (0, 0, self.w - 1, self.MINIMUM_HEIGHT),
                pygame.BLEND_MULT
                )

        def draw_body(self):
            MenuViewer.draw_body(self)
            ys = xrange(0, self.body_rect.height, FONT.line_h)
            ls = self.lines[self.at_line:self.at_line + self.h_in_lines + 2]
            for y, line in zip(ys, ls):
                self.draw_line(y, line)

        def draw_line(self, y, line):
            surface = FONT.render(line[:self.line_w])
            self.body_surface.blit(surface, (0, y))

        def _redraw_line(self, row):
            try: line = self.lines[row]
            except IndexError: line = ' ' * self.line_w
            else:
                n = self.line_w - len(line)
                if n > 0: line = line + ' ' * n
            self.draw_line(self.cursor.screen_y(row), line)

        # General Functionality

        def focus(self, display):
            self.cursor.v = self
            self.cursor.draw()

        def unfocus(self):
            self.cursor.fade()

        def scroll_up(self):
            if self.at_line < len(self.lines) - 1:
                self._fade_command()
                self._deselect()
                self._sel_start = self._sel_end = None
                self.at_line += 1
                self.body_surface.scroll(0, -FONT.line_h)
                row = self.h_in_lines + self.at_line
                self._redraw_line(row)
                self._redraw_line(row + 1)
                self.cursor.draw()

        def scroll_down(self):
            if self.at_line:
                self._fade_command()
                self._deselect()
                self._sel_start = self._sel_end = None
                self.at_line -= 1
                self.body_surface.scroll(0, FONT.line_h)
                self._redraw_line(self.at_line)
                self.cursor.draw()

        def command_down(self, display, x, y):
            if self.command_rect and self.command_rect.collidepoint(x, y):
                return
            self._fade_command()
            line, column, _row = self.at(x, y)
            word_start = line.rfind(' ', 0, column) + 1
            word_end = line.find(' ', column)
            if word_end == -1: word_end = len(line)
            word = line[word_start:word_end]
            if not _is_command(display, word):
                return
            r = self.command_rect = pygame.Rect(
                word_start * FONT.char_w, # x
                y / FONT.line_h * FONT.line_h, # y
                len(word) * FONT.char_w, # w
                FONT.line_h # h
                )
            pygame.draw.line(self.body_surface, FG, r.bottomleft, r.bottomright)
            self.command = word

        def command_up(self, display):
            if self.command:
                command = self.command
                self._fade_command()
                display.broadcast(CommandMessage(self, command))

        def _fade_command(self):
            self.command = None
            r, self.command_rect = self.command_rect, None
            if r:
                pygame.draw.line(self.body_surface, BG, r.bottomleft, r.bottomright)

        def at(self, x, y):
            '''
            Given screen coordinates return the line, row, and column of the
            character there.
            '''
            row = self.at_line + y / FONT.line_h
            try:
                line = self.lines[row]
            except IndexError:
                row = len(self.lines) - 1
                line = self.lines[row]
                column = len(line)
            else:
                column = min(x / FONT.char_w, len(line))
            return line, column, row

        # Event Processing

        def body_click(self, display, x, y, button):
            if button == 1:
                _line, column, row = self.at(x, y)
                self.cursor.set_to(column, row)
            elif button == 2:
                if pygame.KMOD_SHIFT & pygame.key.get_mods():
                    self.scroll_up()
                else:
                    self.scroll_down()
            elif button == 3:
                self.command_down(display, x, y)
            elif button == 4: self.scroll_down()
            elif button == 5: self.scroll_up()

        def menu_click(self, display, x, y, button):
            if MenuViewer.menu_click(self, display, x, y, button):
                return True

        def mouse_up(self, display, x, y, button):
            if MenuViewer.mouse_up(self, display, x, y, button):
                return True
            elif button == 3 and self.body_rect.collidepoint(x, y):
                self.command_up(display)

        def mouse_motion(self, display, x, y, rel_x, rel_y, button0, button1, button2):
            if MenuViewer.mouse_motion(self, display, x, y, rel_x, rel_y,
                                       button0, button1, button2):
                return True
            if (button0
                and display.focused_viewer is self
                and self.body_rect.collidepoint(x, y)
                ):
                bx, by = self.body_rect.topleft
                _line, column, row = self.at(x - bx, y - by)
                self.cursor.set_to(column, row)
            elif button2 and self.body_rect.collidepoint(x, y):
                bx, by = self.body_rect.topleft
                self.command_down(display, x - bx, y - by)

        def close(self):
            self._sel_start = self._sel_end = None

        def key_down(self, display, uch, key, mod):

            if key in SELECTION_KEYS:
                self._selection_key(display, key, mod)
                return
            if key in STACK_CHATTER_KEYS:
                self._stack_chatter_key(display, key, mod)
                return
            if key in ARROW_KEYS:
                self._arrow_key(key, mod)
                return

            line, i = self.lines[self.cursor.y], self.cursor.x
            modified = ()
            if key == pygame.K_RETURN:
                self._return_key(mod, line, i)
                modified = True
            elif key == pygame.K_BACKSPACE:
                modified = self._backspace_key(mod, line, i)
            elif key == pygame.K_DELETE:
                modified = self._delete_key(mod, line, i)
            elif key == pygame.K_INSERT:
                modified = self._insert_key(display, mod, line, i)
            elif uch and uch in FONT or uch == ' ':
                self._printable_key(uch, mod, line, i)
                modified = True
            else:
                print '%r %i %s' % (uch, key, bin(mod))

            if modified:
                # The selection is fragile.
                self._deselect()
                self._sel_start = self._sel_end = None
                message = ModifyMessage(
                    self, self.lines, content_id=self.content_id)
                display.broadcast(message)

        def _stack_chatter_key(self, display, key, mod):
            if key == pygame.K_F5:
                if mod & pygame.KMOD_SHIFT:
                    command = 'roll<'
                else:
                    command = 'swap'
            elif key == pygame.K_F6:
                if mod & pygame.KMOD_SHIFT:
                    command = 'roll>'
                else:
                    command = 'dup'
            elif key == pygame.K_F7:
                if mod & pygame.KMOD_SHIFT:
                    command = 'tuck'
                else:
                    command = 'over'
    ##        elif key == pygame.K_F8:
    ##            if mod & pygame.KMOD_SHIFT:
    ##                command = ''
    ##            else:
    ##                command = ''
            else:
                return
            display.broadcast(CommandMessage(self, command))

        # Selection Handling

        def _selection_key(self, display, key, mod):
            self.cursor.fade()
            self._deselect()
            if key == pygame.K_F1: # set sel start
                self._sel_start = self.cursor.y, self.cursor.x
                self._update_selection()
            elif key == pygame.K_F2: # set sel end
                self._sel_end = self.cursor.y, self.cursor.x
                self._update_selection()
            elif key == pygame.K_F3: # copy
                if mod & pygame.KMOD_SHIFT:
                    self._parse_selection(display)
                else:
                    self._copy_selection(display)
                self._update_selection()
            elif key == pygame.K_F4: # cut or delete
                if mod & pygame.KMOD_SHIFT:
                    self._delete_selection(display)
                else:
                    self._cut_selection(display)
            self.cursor.draw()

        def _deselect(self):
            if self._has_selection():
                srow, erow = self._sel_start[0], self._sel_end[0]
                # Just erase the whole selection.
                for r in range(min(srow, erow), max(srow, erow) + 1):
                    self._redraw_line(r)

        def _copy_selection(self, display):
            if push(self, self._get_selection(), display.broadcast) == SUCCESS:
                return True
    ##        om = OpenMessage(self, 'stack.pickle')
    ##        display.broadcast(om)
    ##        if om.status == SUCCESS:
    ##            selection = self._get_selection()
    ##            om.thing[0] = selection, om.thing[0]
    ##            display.broadcast(ModifyMessage(
    ##                self, om.thing, content_id=om.content_id))

        def _parse_selection(self, display):
            if self._has_selection():
                if self._copy_selection(display):
                    display.broadcast(CommandMessage(self, 'parse'))

        def _cut_selection(self, display):
            if self._has_selection():
                if self._copy_selection(display):
                    self._delete_selection(display)

        def _delete_selection(self, display):
            if not self._has_selection():
                return
            self.cursor.fade()
            srow, scolumn, erow, ecolumn = self._selection_coords()
            if srow == erow:
                line = self.lines[srow]
                self.lines[srow] = line[:scolumn] + line[ecolumn:]
            else:
                left = self.lines[srow][:scolumn]
                right = self.lines[erow][ecolumn:]
                self.lines[srow:erow + 1] = [left + right]
            self.draw_body()
            self.cursor.set_to(srow, scolumn)
            display.broadcast(ModifyMessage(
                self, self.lines, content_id=self.content_id))

        def _has_selection(self):
            return (self._sel_start
                    and self._sel_end
                    and self._sel_start != self._sel_end)

        def _get_selection(self):
            '''Return the current selection if any as a single string.'''
            if not self._has_selection():
                return ''
            srow, scolumn, erow, ecolumn = self._selection_coords()
            if srow == erow:
                return str(self.lines[srow][scolumn:ecolumn])
            lines = []
            assert srow < erow
            while srow <= erow:
                line = self.lines[srow]
                e = ecolumn if srow == erow else len(line)
                lines.append(line[scolumn:e])
                scolumn = 0
                srow += 1
            return str('\n'.join(lines))

        def _selection_coords(self):
            (srow, scolumn), (erow, ecolumn) = (
                min(self._sel_start, self._sel_end),
                max(self._sel_start, self._sel_end)
                )
            return srow, scolumn, erow, ecolumn

        def _update_selection(self):
            if self._sel_start is None and self._sel_end:
                self._sel_start = self._sel_end
            elif self._sel_end is None and self._sel_start:
                self._sel_end = self._sel_start
            assert self._sel_start and self._sel_end
            if self._sel_start != self._sel_end:
                for rect in self._iter_selection_rectangles():
                    self.body_surface.fill(
                        SELECTION_COLOR,
                        rect,
                        pygame.BLEND_RGBA_MULT
                        )

        def _iter_selection_rectangles(self, ):
            srow, scolumn, erow, ecolumn = self._selection_coords()
            if srow == erow:
                yield (
                    scolumn * FONT.char_w,
                    self.cursor.screen_y(srow),
                    (ecolumn - scolumn) * FONT.char_w,
                    FONT.line_h
                    )
                return
            lines = self.lines[srow:erow + 1]
            assert len(lines) >= 2
            first_line = lines[0]
            yield (
                scolumn * FONT.char_w,
                self.cursor.screen_y(srow),
                (len(first_line) - scolumn) * FONT.char_w,
                FONT.line_h
                )
            yield (
                0,
                self.cursor.screen_y(erow),
                ecolumn * FONT.char_w,
                FONT.line_h
                )
            if len(lines) > 2:
                for line in lines[1:-1]:
                    srow += 1
                    yield (
                        0,
                        self.cursor.screen_y(srow),
                        len(line) * FONT.char_w,
                        FONT.line_h
                        )

        # Key Handlers

        def _printable_key(self, uch, _mod, line, i):
            line = line[:i] + uch + line[i:]
            self.lines[self.cursor.y] = line
            self.cursor.fade()
            self.cursor.x += 1
            self.draw_line(self.cursor.screen_y(), line)
            self.cursor.draw()

        def _backspace_key(self, _mod, line, i):
            res = False
            if i:
                line = line[:i - 1] + line[i:]
                self.lines[self.cursor.y] = line
                self.cursor.fade()
                self.cursor.x -= 1
                self.draw_line(self.cursor.screen_y(), line + ' ')
                self.cursor.draw()
                res = True
            elif self.cursor.y:
                y = self.cursor.y
                left, right = self.lines[y - 1:y + 1]
                self.lines[y - 1:y + 1] = [left + right]
                self.cursor.x = len(left)
                self.cursor.y -= 1
                self.draw_body()
                self.cursor.draw()
                res = True
            return res

        def _delete_key(self, _mod, line, i):
            res = False
            if i < len(line):
                line = line[:i] + line[i + 1:]
                self.lines[self.cursor.y] = line
                self.cursor.fade()
                self.draw_line(self.cursor.screen_y(), line + ' ')
                self.cursor.draw()
                res = True
            elif self.cursor.y < len(self.lines) - 1:
                y = self.cursor.y
                left, right = self.lines[y:y + 2]
                self.lines[y:y + 2] = [left + right]
                self.draw_body()
                self.cursor.draw()
                res = True
            return res

        def _arrow_key(self, key, mod):
            if key == pygame.K_UP: self.cursor.up(mod)
            elif key == pygame.K_DOWN: self.cursor.down(mod)
            elif key == pygame.K_LEFT: self.cursor.left(mod)
            elif key == pygame.K_RIGHT: self.cursor.right(mod)

        def _return_key(self, _mod, line, i):
            self.cursor.fade()
            # Ignore the mods for now.
            n = self.cursor.y
            self.lines[n:n + 1] = [line[:i], line[i:]]
            self.cursor.y += 1
            self.cursor.x = 0
            if self.cursor.y > self.at_line + self.h_in_lines:
                self.scroll_up()
            else:
                self.draw_body()
                self.cursor.draw()

        def _insert_key(self, display, mod, _line, _i):
            om = OpenMessage(self, 'stack.pickle')
            display.broadcast(om)
            if om.status != SUCCESS:
                return
            stack = om.thing[0]
            if stack:
                content = format_stack_item(stack[0])
                if self.insert(content):
                    if mod & pygame.KMOD_SHIFT:
                        display.broadcast(CommandMessage(self, 'pop'))
                    return True

        def insert(self, content):
            assert isinstance(content, basestring), repr(content)
            if content:
                self.cursor.fade()
                row, column = self.cursor.y, self.cursor.x
                line = self.lines[row]
                lines = (line[:column] + content + line[column:]).splitlines()
                self.lines[row:row + 1] = lines
                self.draw_body()
                self.cursor.y = row + len(lines) - 1
                self.cursor.x = len(lines[-1]) - len(line) + column
                self.cursor.draw()
                return True

        def append(self, content):
            self.cursor.fade()
            self.cursor.y = len(self.lines) - 1
            self.cursor.x = len(self.lines[self.cursor.y])
            self.insert(content)