# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# display.py - OLED rendering
#
import machine, uzlib, ckcc, utime, version
from ssd1306 import SSD1306_SPI
import framebuf
from graphics_mk4 import Graphics
from charcodes import OUT_CTRL_TITLE, OUT_CTRL_ADDRESS

# we support 4 fonts
from zevvpeep import FontSmall, FontLarge, FontTiny
FontFixed = object()    # ugly 8x8 PET font
display2_buf = bytearray(1024)


class Display:

    WIDTH = 128
    HEIGHT = 64

    # use these negative X values for auto layout features
    CENTER = -2
    RJUST = -1

    # use this to know if on Q1 or earlier 
    has_lcd = False

    def __init__(self):
        from machine import Pin

        spi = machine.SPI(1)
        reset_pin = Pin('PA6', Pin.OUT)
        dc_pin = Pin('PA8', Pin.OUT)
        cs_pin = Pin('PA4', Pin.OUT)

        if version.mk_num == 5:
            # Early revs (A-D) needed this pin asserted to enable +12v to OLED
            # - removed in rev E and later boards, but keep here for dev boards
            # - remove this in 2027
            vcc_en = Pin('V12EN', Pin.OUT)      # aka PC1
            vcc_en(1)

        self.dis = SSD1306_SPI(128, 64, spi, dc_pin, reset_pin, cs_pin, is_mk5=(version.mk_num==5))

        self.last_bar_update = 0
        self.clear()
        self.show()

    def width(self, msg, font):
        if font == FontFixed:
            return len(msg) * 8
        else:
            return sum(font.lookup(ord(ch)).w for ch in msg)

    def icon(self, x, y, name, invert=0):
        if isinstance(name, tuple):
            w,h, bw, wbits, data = name
        else:
            # see graphics.py (auto generated file) for names
            w,h, bw, wbits, data = getattr(Graphics, name)

        if wbits:
            data = uzlib.decompress(data, wbits)

        if invert:
            data = bytearray(i^0xff for i in data)

        gly = framebuf.FrameBuffer(bytearray(data), w, h, framebuf.MONO_HLSB)
        self.dis.blit(gly, x, y, invert)

        return (w, h)

    def text(self, x,y, msg, font=FontSmall, invert=0):
        # Draw at x,y (top left corner of first letter)
        # using font. Use invert=1 to get reverse video

        if x is None or x < 0:
            # center/rjust
            w = self.width(msg, font)
            if x is None:
                x = max(0, (self.WIDTH - w) // 2)
            else:
                # measure from right edge (right justify)
                x = max(0, self.WIDTH - w + 1 + x)

        if y < 0:
            # measure up from bottom edge
            y = self.HEIGHT - font.height + 1 + y

        if font == FontFixed:
            # use font provided by Micropython: 8x8
            self.dis.text(msg, x, y)

            return x + (len(msg) * 8)

        for ch in msg:
            fn = font.lookup(ord(ch))
            if fn is None:
                # use last char in font as error char for junk we don't
                # know how to render
                fn = font.lookup(font.code_range.stop)
            bits = bytearray(fn.w * fn.h)
            bits[0:len(fn.bits)] = fn.bits
            if invert:
                bits = bytearray(i^0xff for i in bits)
            gly = framebuf.FrameBuffer(bits, fn.w, fn.h, framebuf.MONO_HLSB)
            self.dis.blit(gly, x, y, invert)
            x += fn.w

        return x

    def clear(self):
        self.dis.fill(0x0)

    def clear_rect(self, x,y, w,h):
        self.dis.fill_rect(x,y, w,h, 0)

    def show(self):
        self.dis.show()

    # rather than clearing and redrawing, use this buffer w/ fixed parts of screen
    def save(self):
        display2_buf[:] = self.dis.buffer
    def restore(self):
        self.dis.buffer[:] = display2_buf

    def hline(self, y):
        self.dis.line(0, y, 128, y, 1)
    def vline(self, x):
        self.dis.line(x, 0, x, 64, 1)

    def scroll_bar(self, offset, count, per_page):
        # along right edge, height is proportional to page size
        num_pages = max(count / per_page, 2)
        bh = max(int(64 / num_pages), 4)
        pos = int((64 - bh) * (offset / count))

        if offset and (offset + per_page >= count):
            # force last page to be at end
            pos = 64 - bh

        self.dis.fill_rect(128-5, 0, 5, 64, 0)
        self.icon(128-3, 1, 'scroll')
        self.dis.fill_rect(128-2, pos, 1, bh, 1)

        if version.is_devmode and not ckcc.is_simulator():
            self.dis.fill_rect(128-6, 20, 5, 21, 1)
            self.text(-2, 21, 'D', font=FontTiny, invert=1)
            self.text(-2, 28, 'E', font=FontTiny, invert=1)
            self.text(-2, 35, 'V', font=FontTiny, invert=1)
        elif version.is_edge:
            self.dis.fill_rect(128 - 6, 19, 5, 26, 1)
            self.text(-2, 20, 'E', font=FontTiny, invert=1)
            self.text(-2, 27, 'D', font=FontTiny, invert=1)
            self.text(-2, 33, 'G', font=FontTiny, invert=1)
            self.text(-2, 39, 'E', font=FontTiny, invert=1)

    def fullscreen(self, msg, percent=None, line2=None):
        # show a simple message "fullscreen". 
        self.clear()
        y = 14
        self.text(None, y, msg, font=FontLarge)

        if line2:
            # 21 + 6 ie. FontLarge.height of above text + FontTiny.height as space between
            self.text(None, y + 27, line2, font=FontSmall)

        if percent is not None:
            self.progress_bar(percent)
        self.show()

    def splash(self):
        # display a splash screen with some version numbers
        self.clear()
        y = 4
        self.text(None,    y, 'COLDCARD', font=FontLarge)
        self.text(None, y+20, 'Wallet', font=FontLarge)

        from version import get_mpy_version
        timestamp, label, *_ = get_mpy_version()

        y = self.HEIGHT-10
        self.text(0,  y, 'Version '+label, font=FontTiny)
        self.text(-1, y, timestamp, font=FontTiny)
        
        self.show()

    def progress_bar(self, percent):
        # Horizontal progress bar
        # takes 0.0 .. 1.0 as fraction of doneness
        percent = max(0, min(1.0, percent))
        self.dis.hline(0, self.HEIGHT-1, int(self.WIDTH * percent), 1)

    def progress_sofar(self, done, total):
        # Update progress bar, but only if it's been a while since last update
        if utime.ticks_diff(utime.ticks_ms(), self.last_bar_update) < 100:
            return
        self.last_bar_update = utime.ticks_ms()
        self.progress_bar_show(done / total)

    def progress_bar_show(self, percent):
        # useful as a callback
        self.progress_bar(percent)
        self.show()

    def mark_sensitive(self, from_y, to_y):
        wx = self.WIDTH-4       # avoid scroll bar
        for y in range(from_y, to_y):
            ln = max(2, ckcc.rng() % 32)
            self.dis.line(wx-ln, y, wx, y, 1)

    def busy_bar(self, enable):
        # Render a continuous activity (not progress) bar in lower 8 lines of display
        #
        if not enable:
            self.dis.busy_bar(False, None)
            self.show()
        else:
            # Need a pattern that repeats nicely mod 128
            # - each byte here is a vertical column, 8 pixels tall, MSB at bottom
            pat = bytes(0x80 if (x%4)<2 else 0x0 for x in range(128))

            self.dis.busy_bar(True, pat)

    def set_brightness(self, val):
        # normal = 0x7f, brightness=0xff, dim=0x00 (but they are all very similar)
        return self.dis.contrast(val)

    def menu_draw(self, ry, msg, is_sel, is_checked, space_indicators):
        # draw a menu item, perhaps selected, checked.
        x, y = (10, 2)
        h = 14
        y += ry * h

        if is_sel:
            self.dis.fill_rect(0, y, Display.WIDTH, h-1, 1)
            self.icon(2, y, 'wedge', invert=1)
            nx = self.text(x, y, msg, invert=1)
        else:
            nx = self.text(x, y, msg)

        # LATER: removed because caused confusion w/ underscore
        #if msg[0] == ' ' and space_indicators:
            # see also graphics/mono/space.txt
            #self.icon(x-2, y+9, 'space', invert=is_sel)

        if is_checked and nx <= 113:
            # omit checkmark if it doesn't fit
            self.icon(113, y, 'selected', invert=is_sel)

    def menu_show(self, *a):
        self.show()

    def show_yikes(self, lines):
        self.clear()
        self.text(None, 1, '>>>> Yikes!! <<<<')

        y = 13+2
        for num, ln in enumerate(lines):
            ln = ln.strip()

            if ln[0:6] == 'File "':
                # convert: File "main.py", line 63, in interact
                #    into: main.py:63  interact
                ln = ln[6:].replace('", line ', ':').replace(', in ', '  ')

            self.text(0, y + (num*8), ln, FontTiny)

        self.show()

    def draw_story(self, lines, top, num_lines, is_sensitive, **ignored):
        self.clear()

        y=0
        for ln in lines:
            if ln == 'EOT':
                self.hline(y+3)
            elif ln and ln[0] == OUT_CTRL_TITLE:
                self.text(0, y, ln[1:], FontLarge)
                y += 21
            elif ln and ln[0] == OUT_CTRL_ADDRESS:
                from utils import chunk_address
                fmt = '\u2009'.join(chunk_address(ln[1:]))
                self.text(14, y, fmt)       # fixed indent, to be centered
                y += 15                     # a bit extra vertical line height
            else:
                self.text(0, y, ln)

                if is_sensitive and len(ln) > 3 and ln[2] == ':':
                    self.mark_sensitive(y, y+13)

                y += 13

        self.scroll_bar(top, num_lines, 4)
        self.show()

    def draw_status(self, **k):
        # no status bar on Mk4
        return

    def draw_qr_error(self, idx_hint, msg):
        self.clear()
        lm = 4
        bw = 54
        y = (self.HEIGHT - bw) // 2
        # empty rectangle
        self.dis.fill_rect(lm, y, bw, bw, 1)
        self.dis.fill_rect(lm+1, y+1, bw-2, bw-2, 0)
        # error in rectangle - handpicked position
        self.text(lm+5,y+10, "QR too")
        self.text(lm+16,y+24, "big")
        self._draw_qr_display(bw, lm, msg, False, None, idx_hint, False)

    def draw_qr_display(self, qr_data, msg, is_alnum, sidebar, idx_hint, invert,
                        is_addr=False, force_msg=False, side_msg=None):
        # 'sidebar' is a pre-formated obj to show to right of QR -- oled life
        # - 'msg' will appear to right if very short, else under in tiny
        # - ignores "is_addr" because exactly zero space to do anything special
        self.clear()
        w = qr_data.width()
        if w <= 29:
            # version 1,2,3 => we can double-up the pixels
            dbl = True
            lm = 5 if idx_hint else 2  # do not overlap with idx
            h = w * 2
            bw = h + 4  # 2 white pixels from each side
            tm = (self.HEIGHT - bw) // 2
            XO, YO = lm + 2, tm + 2  # two white pixel around QR
        else:
            # v4+ => just one pixel per module, might not be easy to read
            # - vert center, left justify; text on space to right
            dbl = False
            YO = max(0, (64 - w) // 2)
            XO,lm = 6, 4
            bw = w + lm
            tm = (64 - bw) // 2

        if dbl:
            if not invert:
                self.dis.fill_rect(lm, tm, bw, bw, 1)
            else:
                self.dis.fill_rect(lm, tm, bw, bw, 0)

            for x in range(w):
                for y in range(w):
                    if not qr_data.get(x, y):
                        continue
                    X = (x*2) + XO
                    Y = (y*2) + YO
                    self.dis.fill_rect(X,Y, 2,2, invert)
        else:
            # direct "bilt" .. faster. Does not support inversion.
            self.dis.fill_rect(lm, tm, bw, bw, 1)
            _, _, packed = qr_data.packed()
            packed = bytes(i^0xff for i in packed)
            gly = framebuf.FrameBuffer(bytearray(packed), w, w, framebuf.MONO_HLSB)
            self.dis.blit(gly, XO, YO, 1)

        self._draw_qr_display(bw, lm, msg, is_alnum, sidebar, idx_hint, invert, is_addr, side_msg)

    def _draw_qr_display(self, bw, lm, msg, is_alnum, sidebar, idx_hint, invert,
                        is_addr=False, side_msg=None):
        # does not draw actual QR, but all other things in the screen
        from utils import word_wrap

        if not sidebar and not msg:
            pass
        elif not sidebar and ((len(msg) > (5*7)) or side_msg):
            # use FontTiny and word wrap (will just split if no spaces)
            # native segwit addresses and taproot
            # if 'side_msg' also p2pkh and p2sh fall into this category as space is needed for "CHANGE BACK" text
            x = bw + lm + 4
            ww = ((128 - x)//4) - 1        # char width avail
            y = 1

            parts = list(word_wrap(msg, ww))
            if len(parts) > 8:
                parts = parts[:8]
                parts[-1] = parts[-1][0:-3] + '...'
            elif len(parts) <= 5:
                parts.insert(0, '')
    
            for line in parts:
                self.text(x, y, line, FontTiny)
                y += 8

            if side_msg and (len(side_msg) < 15):
                y_pos = y + 8
                # only render if there is space
                if (self.HEIGHT - y_pos) >= FontTiny.height:
                    self.text(x+4, y+8, side_msg, FontTiny)
        else:
            # hand-positioned for known cases
            # - sidebar = (text, #of char per line)
            # p2pkh and p2sh addresses (if is_change=False)
            x, y = 73, (0 if is_alnum else 2)
            dy = 10 if is_alnum else 12
            sidebar, ll = sidebar if sidebar else (msg, 7)

            for i in range(0, len(sidebar), ll):
                self.text(x, y, sidebar[i:i+ll], FontSmall)
                y += dy

        if not invert and idx_hint:
            # show path number, very tiny: vertical left edge
            assert len(idx_hint) <= 10
            y = 2
            for c in idx_hint:
                self.text(0, y, c, FontTiny)
                y += 6  # number is 5px + 1px space

        self.busy_bar(False)     # includes show

    def bootrom_takeover(self):
        # we are going to go into the bootrom and have it do stuff on the
        # screen... nothing needed on here, since we redraw completely
        pass

# EOF
