# textbox.py Extension to nanogui providing the Textbox class

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2020 Peter Hinch

# Usage:
# from gui.widgets.textbox import Textbox

from gui.core.ugui import LinearIO
from hardware_setup import ssd  # Display driver for Writer
from gui.core.writer import Writer

from time import ticks_diff, ticks_ms
import uasyncio as asyncio

# Reason for no tab support in nano-gui/private/reason_for_no_tabs

class Textbox(LinearIO):
    def __init__(self, writer, row, col, width, nlines, *,
                 bdcolor=None, fgcolor=None,
                 bgcolor=None, clip=True, active=False):
        height = nlines * writer.height
        devht = writer.device.height
        devwd = writer.device.width
        if ((row + height + 2) > devht) or ((col + width + 2) > devwd):
            raise ValueError('Textbox extends beyond physical screen.')
        super().__init__(writer, row, col, height, width,
                         fgcolor, bgcolor, bdcolor, 0, active)
        self.nlines = nlines
        self.clip = clip
        self.lines = []
        self.start = 0  # Start line for display

    def _add_lines(self, s):
        width = self.width
        font = self.writer.font
        n = -1  # Index into string
        newline = True
        while True:
            n += 1
            if newline:
                newline = False
                ls = n  # Start of line being processed
                col = 0  # Column relative to text area
            if n >= len(s):  # End of string
                if n > ls:
                    self.lines.append(s[ls :])
                return
            c = s[n]  # Current char
            if c == '\n':
                self.lines.append(s[ls : n])
                newline = True
                continue  # Line fits window
            col += font.get_ch(c)[2]  # width of current char
            if col > width:
                if self.clip:
                    p = s[ls :].find('\n')  # end of 1st line
                    if p == -1:
                        self.lines.append(s[ls : n])  # clip, discard all to right
                        return
                    self.lines.append(s[ls : n])  # clip, discard to 1st newline
                    n = p  # n will move to 1st char after newline
                elif c == ' ':  # Easy word wrap
                    self.lines.append(s[ls : n])
                else:  # Edge splits a word
                    p = s.rfind(' ', ls, n + 1)
                    if p >= 0:  # spacechar in line: wrap at space
                        assert (p > 0), 'space char in position 0'
                        self.lines.append(s[ls : p])
                        n = p
                    else:  # No spacechar: wrap at end
                        self.lines.append(s[ls : n])
                        n -= 1  # Don't skip current char
                newline = True

    def _print_lines(self):
        if len(self.lines) == 0:
            return

        wri = self.writer
        col = self.col
        row = self.row
        left = col
        ht = wri.height
        wri.setcolor(self.fgcolor, self.bgcolor)
        # Print the first (or last?) lines that fit widget's height
        #for line in self.lines[-self.nlines : ]:
        for line in self.lines[self.start : self.start + self.nlines]:
            Writer.set_textpos(ssd, row, col)
            wri.printstring(line)
            row += ht
            col = left
        wri.setcolor()  # Restore defaults

    def show(self):
        if super().show(False):
            self._print_lines()

    def append(self, s, ntrim=None, line=None):
        self._add_lines(s)
        if ntrim is None:  # Default to no. of lines that can fit
            ntrim = self.nlines
        if len(self.lines) > ntrim:
            self.lines = self.lines[-ntrim:]
        self.goto(line)

    def scroll(self, n):  # Relative scrolling
        value = len(self.lines)
        if n == 0 or value <= self.nlines:  # Nothing to do
            return False
        s = self.start
        self.start = max(0, min(self.start + n, value - self.nlines))
        if s != self.start:
            self.draw = True  # Cause a refresh
            return True
        return False

    def value(self):
        return len(self.lines)

    def clear(self):
        self.lines = []
        self.draw = True  # Cause a refresh

    def goto(self, line=None):  # Absolute scrolling
        if line is None:
            self.start = max(0, len(self.lines) - self.nlines)
        else:
            self.start = max(0, min(line, len(self.lines) - self.nlines))
        self.draw = True  # Cause a refresh

    def do_adj(self, button, val):
        if isinstance(button, int):  # Using an encoder
            self.scroll(val)
        else:
            asyncio.create_task(self.btn_handler(button, val))

    async def btn_handler(self, button, up):  # Only runs if not using encoder
        self.scroll(-up)
        t = ticks_ms()
        d = 1
        while not button():
            await asyncio.sleep_ms(0)  # Quit fast on button release
            if ticks_diff(ticks_ms(), t) > 500:  # Button was held down
                d = min(16, d * 2)
                self.scroll(-up * d)
                t = ticks_ms()
