# Driver for official MicroPython LCD160CR display
# MIT license; Copyright (c) 2017 Damien P. George

from micropython import const
from utime import sleep_ms
from ustruct import calcsize, pack_into
import uerrno, machine

# for set_orient
PORTRAIT = const(0)
LANDSCAPE = const(1)
PORTRAIT_UPSIDEDOWN = const(2)
LANDSCAPE_UPSIDEDOWN = const(3)

# for set_startup_deco; can be or'd
STARTUP_DECO_NONE = const(0)
STARTUP_DECO_MLOGO = const(1)
STARTUP_DECO_INFO = const(2)

_uart_baud_table = {
    2400: 0,
    4800: 1,
    9600: 2,
    19200: 3,
    38400: 4,
    57600: 5,
    115200: 6,
    230400: 7,
    460800: 8,
}

class LCD160CR:
    def __init__(self, connect=None, *, pwr=None, i2c=None, spi=None, i2c_addr=98):
        if connect in ('X', 'Y', 'XY', 'YX'):
            i = connect[-1]
            j = connect[0]
            y = j + '4'
        elif connect == 'C':
            i = 2
            j = 2
            y = 'A7'
        else:
            if pwr is None or i2c is None or spi is None:
                raise ValueError('must specify valid "connect" or all of "pwr", "i2c" and "spi"')

        if pwr is None:
            pwr = machine.Pin(y, machine.Pin.OUT)
        if i2c is None:
            i2c = machine.I2C(i, freq=1000000)
        if spi is None:
            spi = machine.SPI(j, baudrate=13500000, polarity=0, phase=0)

        if not pwr.value():
            pwr(1)
            sleep_ms(10)
        # else:
        # alread have power
        # lets be optimistic...

        # set connections
        self.pwr = pwr
        self.i2c = i2c
        self.spi = spi
        self.i2c_addr = i2c_addr

        # create temp buffers and memoryviews
        self.buf16 = bytearray(16)
        self.buf19 = bytearray(19)
        self.buf = [None] * 10
        for i in range(1, 10):
            self.buf[i] = memoryview(self.buf16)[0:i]
        self.buf1 = self.buf[1]
        self.array4 = [0, 0, 0, 0]

        # set default orientation and window
        self.set_orient(PORTRAIT)
        self._fcmd2b('<BBBBBB', 0x76, 0, 0, self.w, self.h) # viewport 'v'
        self._fcmd2b('<BBBBBB', 0x79, 0, 0, self.w, self.h) # window 'y'

    def _send(self, cmd):
        i = self.i2c.writeto(self.i2c_addr, cmd)
        if i == len(cmd):
            return
        cmd = memoryview(cmd)
        n = len(cmd)
        while True:
            i += self.i2c.writeto(self.i2c_addr, cmd[i:])
            if i == n:
                return
            sleep_ms(10)

    def _fcmd2(self, fmt, a0, a1=0, a2=0):
        buf = self.buf[calcsize(fmt)]
        pack_into(fmt, buf, 0, 2, a0, a1, a2)
        self._send(buf)

    def _fcmd2b(self, fmt, a0, a1, a2, a3, a4=0):
        buf = self.buf[calcsize(fmt)]
        pack_into(fmt, buf, 0, 2, a0, a1, a2, a3, a4)
        self._send(buf)

    def _waitfor(self, n, buf):
        t = 5000
        while t:
            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
            if self.buf1[0] >= n:
                self.i2c.readfrom_into(self.i2c_addr, buf)
                return
            t -= 1
            sleep_ms(1)
        raise OSError(uerrno.ETIMEDOUT)

    def oflush(self, n=255):
        t = 5000
        while t:
            self.i2c.readfrom_into(self.i2c_addr + 1, self.buf1)
            r = self.buf1[0]
            if r >= n:
                return
            t -= 1
            machine.idle()
        raise OSError(uerrno.ETIMEDOUT)

    def iflush(self):
        t = 5000
        while t:
            self.i2c.readfrom_into(self.i2c_addr, self.buf16)
            if self.buf16[0] == 0:
                return
            t -= 1
            sleep_ms(1)
        raise OSError(uerrno.ETIMEDOUT)

    #### MISC METHODS ####

    @staticmethod
    def rgb(r, g, b):
        return ((b & 0xf8) << 8) | ((g & 0xfc) << 3) | (r >> 3)

    @staticmethod
    def clip_line(c, w, h):
        while True:
            ca = ce = 0
            if c[1] < 0:
                ca |= 8
            elif c[1] > h:
                ca |= 4
            if c[0] < 0:
                ca |= 1
            elif c[0] > w:
                ca |= 2
            if c[3] < 0:
                ce |= 8
            elif c[3] > h:
                ce |= 4
            if c[2] < 0:
                ce |= 1
            elif c[2] > w:
                ce |= 2
            if ca & ce:
                return False
            elif ca | ce:
                ca |= ce
                if ca & 1:
                    if c[2] < c[0]:
                        c[0], c[2] = c[2], c[0]
                        c[1], c[3] = c[3], c[1]
                    c[1] += ((-c[0]) * (c[3] - c[1])) // (c[2] - c[0])
                    c[0] = 0
                elif ca & 2:
                    if c[2] < c[0]:
                        c[0], c[2] = c[2], c[0]
                        c[1], c[3] = c[3], c[1]
                    c[3] += ((w - 1 - c[2]) * (c[3] - c[1])) // (c[2] - c[0])
                    c[2] = w - 1
                elif ca & 4:
                    if c[0] == c[2]:
                        if c[1] >= h:
                            c[1] = h - 1
                        if c[3] >= h:
                            c[3] = h - 1
                    else:
                        if c[3] < c[1]:
                            c[0], c[2] = c[2], c[0]
                            c[1], c[3] = c[3], c[1]
                        c[2] += ((h - 1 - c[3]) * (c[2] - c[0])) // (c[3] - c[1])
                        c[3] = h - 1
                else:
                    if c[0] == c[2]:
                        if c[1] < 0:
                            c[1] = 0
                        if c[3] < 0:
                            c[3] = 0
                    else:
                        if c[3] < c[1]:
                            c[0], c[2] = c[2], c[0]
                            c[1], c[3] = c[3], c[1]
                        c[0] += ((-c[1]) * (c[2] - c[0])) // (c[3] - c[1])
                        c[1] = 0
            else:
                return True

    #### SETUP COMMANDS ####

    def set_power(self, on):
        self.pwr(on)
        sleep_ms(15)

    def set_orient(self, orient):
        self._fcmd2('<BBB', 0x14, (orient & 3) + 4)
        # update width and height variables
        self.iflush()
        self._send(b'\x02g0')
        self._waitfor(4, self.buf[5])
        self.w = self.buf[5][1]
        self.h = self.buf[5][2]

    def set_brightness(self, value):
        self._fcmd2('<BBB', 0x16, value)

    def set_i2c_addr(self, addr):
        # 0x0e set i2c addr
        if addr & 3:
            raise ValueError('must specify mod 4 aligned address')
        self._fcmd2('<BBW', 0x0e, 0x433249 | (addr << 24))

    def set_uart_baudrate(self, baudrate):
        try:
            baudrate = _uart_baud_table[baudrate]
        except KeyError:
            raise ValueError('invalid baudrate')
        self._fcmd2('<BBB', 0x18, baudrate)

    def set_startup_deco(self, value):
        self._fcmd2('<BBB', 0x19, value)

    def save_to_flash(self):
        self._send(b'\x02fn')

    #### PIXEL ACCESS ####

    def set_pixel(self, x, y, c):
        self._fcmd2b('<BBBBH', 0x41, x, y, c)

    def get_pixel(self, x, y):
        self._fcmd2('<BBBB', 0x61, x, y)
        t = 1000
        while t:
            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
            if self.buf1[0] >= 2:
                self.i2c.readfrom_into(self.i2c_addr, self.buf[3])
                return self.buf[3][1] | self.buf[3][2] << 8
            t -= 1
            sleep_ms(1)
        raise OSError(uerrno.ETIMEDOUT)

    def get_line(self, x, y, buf):
        l = len(buf) // 2
        self._fcmd2b('<BBBBB', 0x10, l, x, y)
        t = 1000
        while t:
            self.i2c.readfrom_into(self.i2c_addr, self.buf1)
            if self.buf1[0] >= l:
                self.i2c.readfrom_into(self.i2c_addr, buf)
                return
            t -= 1
            sleep_ms(1)
        raise OSError(uerrno.ETIMEDOUT)

    def screen_dump(self, buf):
        line = bytearray(self.w + 1)
        h = len(buf) // (2 * self.w)
        if h > self.h:
            h = self.h
        for i in range(h):
            ix = i * self.w * 2
            self.get_line(0, i, line)
            for j in range(1, len(line)):
                buf[ix] = line[j]
                ix += 1
            self.get_line(self.w // 2, i, line)
            for j in range(1, len(line)):
                buf[ix] = line[j]
                ix += 1

    def screen_load(self, buf):
        l = self.w * self.h * 2+2
        self._fcmd2b('<BBHBBB', 0x70, l, 16, self.w, self.h)
        n = 0
        ar = memoryview(buf)
        while n < len(buf):
            if len(buf) - n >= 0x200:
                self._send(ar[n:n + 0x200])
                n += 0x200
            else:
                self._send(ar[n:])
                while n < self.w * self.h * 2:
                    self._send(b'\x00')
                    n += 1

    #### TEXT COMMANDS ####

    def set_pos(self, x, y):
        self._fcmd2('<BBBB', 0x58, x, y)

    def set_text_color(self, fg, bg):
        self._fcmd2('<BBHH', 0x63, fg, bg)

    def set_font(self, font, scale=0, bold=0, trans=0, scroll=0):
        self._fcmd2('<BBBB', 0x46, (scroll << 7) | (trans << 6) | ((font & 3) << 4) | (bold & 0xf), scale & 0xff)

    def write(self, s):
        # TODO: eventually check for room in LCD input queue
        self._send(s)

    #### PRIMITIVE DRAWING COMMANDS ####

    def set_pen(self, line, fill):
        self._fcmd2('<BBHH', 0x50, line, fill)

    def erase(self):
        self._send(b'\x02\x45')

    def dot(self, x, y):
        if 0 <= x < self.w and 0 <= y < self.h:
            self._fcmd2('<BBBB', 0x4b, x, y)

    def rect(self, x, y, w, h, cmd=0x72):
        if x + w <= 0 or y + h <= 0 or x >= self.w or y >= self.h:
            return
        elif x < 0 or y < 0:
            left = top = True
            if x < 0:
                left = False
                w += x
                x = 0
            if y < 0:
                top = False
                h += y
                y = 0
            if cmd == 0x51 or cmd == 0x72:
                # draw interior
                self._fcmd2b('<BBBBBB', 0x51, x, y, min(w, 255), min(h, 255))
            if cmd == 0x57 or cmd == 0x72:
                # draw outline
                if left:
                    self._fcmd2b('<BBBBBB', 0x57, x, y, 1, min(h, 255))
                if top:
                    self._fcmd2b('<BBBBBB', 0x57, x, y, min(w, 255), 1)
                if x + w < self.w:
                    self._fcmd2b('<BBBBBB', 0x57, x + w, y, 1, min(h, 255))
                if y + h < self.h:
                    self._fcmd2b('<BBBBBB', 0x57, x, y + h, min(w, 255), 1)
        else:
            self._fcmd2b('<BBBBBB', cmd, x, y, min(w, 255), min(h, 255))

    def rect_outline(self, x, y, w, h):
        self.rect(x, y, w, h, 0x57)

    def rect_interior(self, x, y, w, h):
        self.rect(x, y, w, h, 0x51)

    def line(self, x1, y1, x2, y2):
        ar4 = self.array4
        ar4[0] = x1
        ar4[1] = y1
        ar4[2] = x2
        ar4[3] = y2
        if self.clip_line(ar4, self.w, self.h):
            self._fcmd2b('<BBBBBB', 0x4c, ar4[0], ar4[1], ar4[2], ar4[3])

    def dot_no_clip(self, x, y):
        self._fcmd2('<BBBB', 0x4b, x, y)

    def rect_no_clip(self, x, y, w, h):
        self._fcmd2b('<BBBBBB', 0x72, x, y, w, h)

    def rect_outline_no_clip(self, x, y, w, h):
        self._fcmd2b('<BBBBBB', 0x57, x, y, w, h)

    def rect_interior_no_clip(self, x, y, w, h):
        self._fcmd2b('<BBBBBB', 0x51, x, y, w, h)

    def line_no_clip(self, x1, y1, x2, y2):
        self._fcmd2b('<BBBBBB', 0x4c, x1, y1, x2, y2)

    def poly_dot(self, data):
        if len(data) & 1:
            raise ValueError('must specify even number of bytes')
        self._fcmd2('<BBB', 0x71, len(data) // 2)
        self._send(data)

    def poly_line(self, data):
        if len(data) & 1:
            raise ValueError('must specify even number of bytes')
        self._fcmd2('<BBB', 0x78, len(data) // 2)
        self._send(data)

    #### TOUCH COMMANDS ####

    def touch_config(self, calib=False, save=False, irq=None):
        self._fcmd2('<BBBB', 0x7a, (irq is not None) << 2 | save << 1 | calib, bool(irq) << 7)

    def is_touched(self):
        self._send(b'\x02T')
        b = self.buf[4]
        self._waitfor(3, b)
        return b[1] >> 7 != 0

    def get_touch(self):
        self._send(b'\x02T') # implicit LCD output flush
        b = self.buf[4]
        self._waitfor(3, b)
        return b[1] >> 7, b[2], b[3]

    #### ADVANCED COMMANDS ####

    def set_spi_win(self, x, y, w, h):
        pack_into('<BBBHHHHHHHH', self.buf19, 0, 2, 0x55, 10, x, y, x + w - 1, y + h - 1, 0, 0, 0, 0xffff)
        self._send(self.buf19)

    def fast_spi(self, flush=True):
        if flush:
            self.oflush()
        self._send(b'\x02\x12')
        return self.spi

    def show_framebuf(self, buf):
        self.fast_spi().write(buf)

    def set_scroll(self, on):
        self._fcmd2('<BBB', 0x15, on)

    def set_scroll_win(self, win, x=-1, y=0, w=0, h=0, vec=0, pat=0, fill=0x07e0, color=0):
        pack_into('<BBBHHHHHHHH', self.buf19, 0, 2, 0x55, win, x, y, w, h, vec, pat, fill, color)
        self._send(self.buf19)

    def set_scroll_win_param(self, win, param, value):
        self._fcmd2b('<BBBBH', 0x75, win, param, value)

    def set_scroll_buf(self, s):
        l = len(s)
        if l > 32:
            raise ValueError('length must be 32 or less')
        self._fcmd2('<BBB', 0x11, l)
        self._send(s)

    def jpeg_start(self, l):
        self.oflush()
        self._fcmd2('<BBH', 0x6a, l)

    def jpeg_data(self, buf):
        self._send(buf)

    def jpeg(self, buf):
        self.jpeg_start(len(buf))
        self.jpeg_data(buf)

    def feed_wdt(self):
        self._send(b'\x02\x17')

    def reset(self):
        self._send(b'\x02Y\xef\xbe\xad\xde')
        sleep_ms(15)