#####################################################
# @module    gui_utils.py                           #
# @brief     Simple GUI Tools for many tasks        #
# @author    Original Code form multiple authors    #
# @date      Sept 2020                              #
# @copyright Racelogic Ltd., 2020                   #
# @Version   1.3                                    #
#####################################################

import ft8xx as ft
import ustruct as us
import gui
import vts
from micropython import const

# Spin_Types
SPINNER_WHEEL   = const(0)
SPINNER_LINE    = const(1)
SPINNER_CLOCK   = const(2)
SPINNER_SWAP    = const(3)

DEFAULT_BG = const(0x002040)
DEFAULT_FG = const(0x003870)

class GUI_Error(Exception):
    pass

class Colours:
    white = gui.RGB(255, 255, 255)
    lightgrey = gui.RGB(192, 192, 192)
    grey = gui.RGB(128, 128, 128)
    darkgrey = gui.RGB(64, 64, 64)
    black = gui.RGB(0, 0, 0)
    red = gui.RGB(255, 0, 0)
    green = gui.RGB(0, 255, 0)
    blue = gui.RGB(0, 0, 255)
    darkred = gui.RGB(192, 0, 0)
    darkgreen = gui.RGB(0, 192, 0)
    darkblue = gui.RGB(0, 0, 192)
    gold = gui.RGB(240, 240, 32)
    gradblue0 = gui.RGB(0, 0, 96)
    gradblue1 = gui.RGB(0, 0, 16)

def FG_COLOUR(col):
    return [0xffffff0a, gui.DL_COLOR(col)]

def BG_COLOUR(col):
    return [0xffffff09, gui.DL_COLOR(col)]

def GRAD_COLOUR(col):
    return [0xffffff34, gui.DL_COLOR(col)]

def num_base(val):
    if val in [2,8,10,16]:
        return [0xffffff38, int(val)]
    else:
        raise GUI_Error("Numerical Base must be binary, decimal, octal or hex")

def gradient(x0, y0, col_1, x1, y1, col_2):
    return [
        0xffffff0b,
        (y0 << 16) | x0,
        col_1,
        (y1 << 16) | x1,
        col_2,
    ]

def avgcolour(c0,c1):
    return ((((((c0 >> 16) & 0xff) + ((c1 >> 16) & 0xff)) >> 1) << 16) \
    | (((((c0 >> 8) & 0xff) + ((c1 >> 8) & 0xff)) >> 1) << 8)           \
    | (((c0 & 0xff) + (c1 & 0xff)) >> 1))

class ScreenFilter:
    #Technically a copy of the one in maths_utils, convinient to have here if maths utils not needed
    def __init__(self):
        # 2nd order IIR Gauss LP filter, 2.1 Hz BW, 3 Hz wc, fs = 60 Hz
        self.a = (1.0000000000, -1.4950558121,  0.5668457092)
        self.b = (0.0179474742,  0.0358949485,  0.0179474742)
        # Filter state
        self.w = [0, 0]

    def __call__(self, x):
        # Biquad transposed direct form 2
        w = x - self.a[1] * self.w[0] - self.a[2] * self.w[1]
        y = self.b[0] * w + self.b[1] * self.w[0] + self.b[2] * self.w[1]
        self.w[1] = self.w[0]
        self.w[0] = w
        return y

# FT8XX Loading screen that can update while other function is happening
def wait_screen(function=None, min_time=10, text="Loading...", spin_type=SPINNER_CLOCK):
    if not min_time: min_time = 1
    if text is None:
        text = ""
    gui.pause(True)
    ft.cp_start()
    ft.cp_cmd(gui.DL_CLEAR_COLOR_RGB(0, 0, 0))
    ft.cp_cmd(gui.DL_CLEAR(1, 1, 1))
    ft.cpcmd_text(400, 200, 31, gui.OPT_CENTER, text)
    ft.cpcmd_spinner(400, 300, spin_type, 0)
    with vts.Timer(min_time, False) as t:
        if callable(function):
            function()
        while not t.counter():
            vts.delay_us(100) #allow callbacks to execute
    ft.cpcmd_stop()
    ft.cp_finish()
    gui.pause(False)

# Decorator version of the above function (see example use below)
def wait_screen_decorator(min_time=10, text="Loading...", spin_type=SPINNER_CLOCK):
    def wrap(function):
        def wrapped(*args, **kwargs):
            wrap_min_time = int(min_time) if (min_time > 1) else 1
            wrap_text = text if text else " "
            gui.pause(True)
            ft.cp_start()
            ft.cp_cmd(gui.DL_CLEAR_COLOR_RGB(0, 0, 0))
            ft.cp_cmd(gui.DL_CLEAR(1, 1, 1))
            ft.cpcmd_text(400, 200, 31, gui.OPT_CENTER, wrap_text)
            ft.cpcmd_spinner(400, 300, spin_type, 0)
            with vts.Timer(wrap_min_time, False) as t:
                if callable(function):
                    function(*args, **kwargs)
                while not t.counter():
                    vts.delay_ms(1) #allow callbacks to execute
            ft.cpcmd_stop()
            ft.cp_finish()
            gui.pause(False)
        return wrapped
    return wrap

class ScreenTranslationEffect:
    def __init__(self):
        self.translate_l = [
            gui.DL_VERTEX_TRANSLATE_X(0),
            gui.DL_VERTEX_TRANSLATE_Y(0),
        ]
        self.time = 0
        self.dir = None
        self.time_dir = None
        self.dim = None
        self.active = False
        self.sequence = tuple((t**3) * (8/270) for t in range(1, 31))

    def gui_list(self):
        return self.translate_l

    def update(self):
        if not self.active:
            return
        time = self.time if self.time_dir == 1 else 29 - self.time
        val = self.dir * self.sequence[time]
        if self.dim == 0:
            self.translate_l[0] = gui.DL_VERTEX_TRANSLATE_X(val)
        else:
            self.translate_l[1] = gui.DL_VERTEX_TRANSLATE_Y(val)
        self.time += 1
        if self.time >= 30:
            self.active = False
        gui.redraw()

    def decode_dir(self, transition_req):
        transition = transition_req.split()
        if transition[0] == 'from':
            self.time_dir = -1
        elif transition[0] == 'to':
            self.time_dir = 1
        else:
            raise Exception('invalid transition orientation')
        if transition[1] in {'top', 'bottom'}:
            self.dim = 1
        elif transition[1] in {'left', 'right'}:
            self.dim = 0
        else:
            raise Exception('invalid transition direction')
        if transition[1] in {'top', 'left'}:
            self.dir = -1
        elif transition[1] in {'bottom', 'right'}:
            self.dir = 1

    def run(self, transition_req, block=False):
        self.decode_dir(transition_req)
        self.time = 0
        if self.dim == 0:
            self.translate_l[1] = gui.DL_VERTEX_TRANSLATE_Y(0)
        else:
            self.translate_l[0] = gui.DL_VERTEX_TRANSLATE_X(0)
        self.active = True
        if block:
            while self.active:
                pass

class FT_Font_Handler():

    """FT81X_Series_Programmer_Guide - Section 5.5.1"""

    def __init__(self, font):
        self.font_no = font
        self.font_height = type(self).get_font_height(font)
        self.font_metrics = type(self).get_font_metrics(font)

    def __getitem__(self, val):
        if isinstance(val, str):
            return sum([self.font_metrics[ord(char)-32] for char in list(val)])
        elif isinstance(val, int):
            return self.font_metrics[val-32]

    @staticmethod
    def get_font_height(font):
        return ft.rd32(ft.rd32(0x2ffffc) + 148 * (font - 16) + 140)

    @staticmethod
    def get_font_metrics(font):
        font_metric_buffer = bytearray(148)
        ft.rdbuf(ft.rd32(0x2ffffc) + 148 * (font - 16), font_metric_buffer)
        return us.unpack(('B' * 128) + 'LLLLL', font_metric_buffer)[32:128]

    @staticmethod
    def get_text_length(text, font):
        font_info = FT_Font_Handler.get_font_metrics(font)
        return sum([font_info[ord(char)-32] for char in list(text)])

    def text(self, x, y, text, w=0, linesp=0, center=False):
        """
        Create display list for displaying auto-formated text. Not the quickest,
        so don't use for fast updating text (like parameters), but infrequemt updated
        text would suit this well as it handles linespacing, word_wrapping and centering.

        Credit To Henry Daitx for this, original source: unicode.py

            Parameters:
                x           starting horizontal position
                y           starting vertical position
                text        Unicode string to be displayed
            Optional parameters:
                w           width for word-wrapping (default: 0, no wrapping)
                linesp      line spacing additive factor (default: 0)
                center      center-justify text (default: false, left-justify)
            Output:
                List containing a series of gui.CTRL_TEXT items. These items can be
                displayed using gui.get_list().extend(), for example.
        """
        for c in set(text):
            if not (32 < ord(c) < 127):
                raise Exception('{} is not an available ASCII Character'.format(ord(c)))
        if w:
            # Measure individual word width while limiting it to w
            _words = text.split()
            # words: text split by words (after splitting individual words that are
            # too long for the line width)
            words = []
            # wlen: length in pixels of each word in words
            wlen = []
            for _word in _words:
                wl = 0
                word = []
                for c in _word:
                    cw = self.font_metrics[ord(c)-32]
                    if wl + cw > w:
                        wlen.append(wl)
                        words.append(''.join(word))
                        word = []
                        wl = 0
                    word.append(c)
                    wl += cw
                wlen.append(wl)
                words.append(''.join(word))
            text = ' '.join(words)

            # Position words inside line and create as many lines as needed
            # wi: word counter
            wi = 0
            # dx: current char's x position inside box
            dx = 0
            line = 0
            chars_in_line = 0
            vx = [0] * len(text)
            vy = [0] * len(text)
            starting_word = False
            for i, c in enumerate(text):
                ci = ord(c)
                cw = self.font_metrics[ci - 32]
                # font_handle, index, cw, dy = self.xlat_table[ci]
                vx[i] = x + dx
                if starting_word and dx + wlen[wi + 1] > w or ci == 124:
                    # We're about to start a new word but it won't fit in current
                    # line, put it on a new line instead
                    if center:
                        # If text is centered, go back to all chars in the line
                        # we just concluded and adjust their position by half
                        # the space left (minus the width of the space that came
                        # before this word)
                        left_adjust = (w - dx + cw) // 2
                        for center_i in range(1, chars_in_line + 1):
                            vx[i - center_i] += left_adjust
                    vx[i] = x
                    chars_in_line = 0
                    dx = 0
                    line += 1
                dx += cw
                vy[i] = y + line * (self.font_height + linesp)
                chars_in_line += 1
                if starting_word:
                    # This is a new word, increment word counter and reset flag
                    wi += 1
                    starting_word = False
                if ci == 32:
                    # This is a space, next char starts a word
                    starting_word = True
            # Center last line if needed (the main loop can't center it because it
            # only centers a line when the next line starts)
            if center:
                left_adjust = (w - dx) // 2
                for center_i in range(chars_in_line):
                    vx[i - center_i] += left_adjust #what is i here?

        #TODO: Combine back into individual words to cut down on display list commands
        #TODO: Example below, but commented out as un-tested
        # current_y = vy[0]
        # current_x = vx[0]
        # cur_word = ""
        # lines = []
        # for i, char in enumerate(text):
        #     if vy[i] == current_y:
        #         cur_word += char
        #     else:
        #         lines.append([cur_word, current_x, current_y])
        #         current_y = vy[i]
        #         current x = vx[i]
        # lines.append([cur_word, current_x, current_y])

        font_gui_l = []
        for i, c in enumerate(text):
            if ord(c) != 124:
                if w:
                    font_gui_l.append([gui.CTRL_TEXT, vx[i], vy[i], self.font_no, 0, c])
                else:
                    font_gui_l.append([gui.CTRL_TEXT, x, y, self.font_no, 0, c])
                    x += self.font_metrics[ci - 32]
        return font_gui_l



if __name__ == "__main__":
    #test Functions

    @wait_screen_decorator(min_time=3000, text="Testing Decorator Function", spin_type=SPINNER_CLOCK)
    def _Test_dec(name="bob"):
        print("bob")
        vts.delay_ms(1000)
        print(name)

    _Test_dec("This Works")

    font_28 = FT_Font_Handler(28)
    assert font_28[0x68] == 11 #ascii 'h'
    assert font_28['h'] == 11
    assert font_28["hello world"] == 104
    assert font_28.font_height == 25
    print("No Errors with Font Handling")




#end