import gui
import vts
import ft8xx as ft
import ustruct as us
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)
    rl_header = gui.RGB(0, 41, 66)

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))

# 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 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


class ScreenFilter:
    #Technically a copy of the gauss filter in maths_utils, convenient 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


class Reduc_Filter:
    # Again technically a copy of the one in maths_utils, convinient to have here if maths utils not needed
    """
    Generic digital filter object. Obtained response is defined by the reducing
    function used.

        Parameters:
            length      filter length [samples]
            fun_red     reducing function (takes a container as input and emits
                        a result)
        Optional parameters:
            init        initial value for "empty" samples
    """
    def __init__(self, length, fun_red= lambda vec: sum(vec) / len(vec), init=0):
        self.length = length
        self.fun_red = fun_red
        self.init = init
        self.start()

    def start(self):
        """Restart the filter (throw away all samples)."""
        self.container = [self.init] * self.length

    def push(self, val):
        """
        Push a new sample into filter.
        """
        self.container.pop()
        self.container.insert(0, val)

    def get(self):
        """
        Execute reducing function and get value.
        """
        return self.fun_red(self.container)

class LoopingButton:

    def __init__(self, x, y, width, height, options, font = 7, cb = None):
        self.idx = 0
        self.options = options
        self.current = self.options[self.idx]
        self.len = len(options) - 1
        self.text = [str(self.options[self.idx])]
        self.gui_l = [gui.CTRL_FLATBUTTON, x, y, width, height, font, self.text, self.btn_cb]
        self.cb = cb
        self.set_value = self.set_btn

    def __call__(self):
        return self.gui_l

    def btn_cb(self, btn):
        if self.idx == self.len:
            self.idx = 0
        else:
            self.idx += 1
        self.text[0] = str(self.options[self.idx])
        self.current = self.options[self.idx]
        if self.cb:
            self.cb(self)

    def set_btn(self, var = None, idx = None):
        if var != None:
            self.idx = self.options.index(var)
            self.text[0] = str(self.options[self.idx])
        elif idx != None:
            self.idx = idx
            self.text[0] = str(self.options[self.idx])

    def get_value(self):
        return self.current

    def get_id(self):
        return self.idx

class Animations:
    @staticmethod
    def example_generator():
        while True:
            yield True

    def __init__(self) -> None:
        self.active_animations = [None] #initialise with one empty item
        self.handles = {}
        self.gen_type = self.example_generator()

    def run(self):
        while True:
            if any(self.active_animations):
                to_remove = []
                for index, animation in enumerate(self.active_animations):
                    # if animation: animation()
                    if animation:
                        try:
                            next(animation)
                        except StopIteration:
                            to_remove.append(index) #remove after loop or index tracking is difficult
                for anim in to_remove:
                    self._end_animation(anim)
            yield


    def add_animation(self, cb, key=None):
        if callable(cb):
            if key in self.handles.keys() and self.active_animations[self.handles[key]]:
                return
            for handle, animation in enumerate(self.active_animations):
                if animation is None:
                    self.active_animations[handle] = cb() #fill empty spaces first before allocating any more
                    # print("Added Animation to slot ", handle)
                    break
            else:
                self.active_animations.append(cb) # no free spaces, add a new slot
                print("Added animation to new slot ", len(self.active_animations)-1)
            handle = len(self.active_animations)-1
            if key:
                self.handles[key] = handle
            return handle
        else:
            raise Exception("Animation must be a generator that can be called")

    def remove(self, key):
        handle = self.handles.get(key)
        if handle:
            self._end_animation(handle)
            self.handles.pop(key)

    def _end_animation(self, index):
        try:
            self.active_animations[index] = None
            for k, v in self.handles.items():
                if v == index:
                    self.handles.pop(k)
            # print("Removed Animation from slot ", index)
        except IndexError:
            raise Exception("Invalid Animation handle, cannot remove")

    def clear_animations(self):
        self.active_animations = [None] #initialise with one empty item
        self.handles.clear()

    def animations_pending(self):
        return any(self.active_animations)