##
# @file    main.py
# @brief   Visual display of VBOX Precision Ranging System over CAN and Serial
# @version 1.2.4
#

import can
import gui
import ft8xx as ft
import gui
import ustruct as us
import vts
import serial
from screenshot import screenshot_cb
from picture_button import Picture_Button
from image import Image_Bank
from utils import Filter
import os

### Constants ###

offset = 0.0
page = 0
swipe_r = 50
range = ["{:06.2f}".format(0.0-offset)]
filter = Filter()
storedResults = []
serial_buffer = [0] * 32
read_pos = 0
filtered = 0
version_info ={ 'app_version': '1.2.4', 'serial_number':123456}
version_gui = []
path = os.getcwd() + '/'


def load_fonts():
    from font import load_font
    start, end = load_font(path + 'BebasNeue Bold-56.rft', 8)
    start, end = load_font(path + 'BebasNeue_0-184.rft', 10, mempos_end=start)

def vsync_cb(l):
    gui.redraw()

def swipe_cb(gui_list,start):
    global page

    if start:
        pass
    else:
        si = gui.swipe_info()
        if si.dx <= -50:
            if page == 0 or page < (len(storedResults)/10):
                page += 1
        elif si.dx >= 50:
            if page > 0:
                page -= 1
        draw()

def next_cb(button):
    global page
    if page == 0 or page < (len(storedResults)/10):
        page += 1
    draw()

def previous_cb(button):
    global page
    if page > 0:
        page -= 1
    draw()

def view_press_cb(data):
    if data[0] == 2:    # 1 = press, 2 = press and hold
        screenshot_cb(data)

def bytes_to_int(bytes):
    result = 0
    for b in bytes:
        result = result * 256 + int(b)
    return result

def handle_can(bus):
    global range
    global filtered
    global newmetres
    while True:
        can_msg = can.get_msg(bus)
        if can_msg is None:
            return
        if can_msg.id == 0x801 or can_msg.id == 0x6F1:
            mm = bytes_to_int(can_msg.data[0:4])
            newmetres = mm / 1000
            filtered = filter(newmetres-offset)
            # print("Raw: {} \t\tFiltered: {}".format(newmetres-offset, filtered))
            pad = '06' if filtered >= 0 else '07'
            range[0] = "{:{p}.2f}".format(filtered, p=pad)

def can0_cb():
    handle_can(can.CAN0)

def can1_cb():
    handle_can(can.CAN1)

def reset_cb(button):
    global page
    global storedResults
    if page == 0:
        global offset, filter
        offset = 0
        filter = Filter()
    else:
        storedResults.clear()
        draw()
    led_flash(63,25,0)

def zero_cb(button):
    global offset, filter
    offset = filtered+offset
    filter = Filter()
    led_flash(0,0,50)

def led_flash(red= 0, green= 50, blue= 0, delay= 250, blink = 0):
    if blink == 0:
        vts.leds(*[red,green,blue]*4)
        vts.delay_ms(delay)
        vts.leds(*[0]*12)
    else:
        for i in range(blink):
            vts.leds(*[red,green,blue]*4)
            vts.delay_ms(delay)
            vts.leds(*[0]*12)
            vts.delay_ms(delay)

def save_cb(button):
    global storedResults, page
    if page == 0:
        storedResults.append(range[0])
        led_flash()
        draw()
    if page !=0:
        try:
            save_ok, msg = save_txt()
        except TypeError:
            save_ok, msg = False, 'SD Card Error - Not Compatible'
        if save_ok:
            led_flash(blink=2)
            print(msg)
        else:
            led_flash(50,0,0, blink=2)
            print(msg)

def get_file_name(directory = '/sd', prefix='VPRS_'):
    try:
        directory_list = os.listdir(directory)
    except OSError:
        return
    directory_list = [
        x for x in directory_list if len(x) == 12 and x[:5] == prefix  #if filename is 12 characters long and first 5 chars == prefix
    ]
    if not directory_list:
        n = 0
    else:
        n = int(max(directory_list)[5:8]) + 1
    return directory + '/{}{:03}.txt'.format(prefix, n)

def save_txt():
    global storedResults
    if vts.sd_present():
        filename = get_file_name()
    else:
        return False, "Save to Txt Fail: No SD Card"
    with open(filename, 'w') as file:
        file.write("VBOX Precision Ranging System - Saved Measurements\n\n")
        for i in range(len(storedResults)):
            file.write("Measurement {}: {}m\n".format(i+1,storedResults[i]))
        file.close()

    return True, "Saved results to {}".format(filename)

def wrap_callback(callback):
    def cb(*args, **kwargs):
        callback(*args, **kwargs)
    return cb

def create_buttons(*args):
    global buttons
    buttons = []
    if 'Reset' in args:
        buttons.append(Picture_Button(15, 410, bank.get('Reset'),'Reset',reset_cb))
    if 'Zero' in args:
        buttons.append(Picture_Button(100, 410, bank.get('Zero'),'Zero',zero_cb))
    if 'Save' in args:
        buttons.append(Picture_Button(350, 410, bank.get('Save'),'Save',save_cb))
    if 'Previous' in args:
        buttons.append(Picture_Button(650, 410, bank.get('Previous'), 'Previous', previous_cb))
    if 'Next' in args:
        buttons.append(Picture_Button(725, 410, bank.get('Next'), 'Next', next_cb))

    button_cbs_l = []
    button_icons_l = [gui.DL_BEGIN(gui.PRIM_BITMAPS)]
    for i, button in enumerate(buttons):
        button_cb_l = [
             gui.PARAM_TAG_REGISTER,
             wrap_callback(button.get_callback()),
        ]
        button_cb_l.append(button.name)
        button_cbs_l.append(button_cb_l)
        button.set_gui_l_index(len(button_icons_l))
        button_icons_l.extend(button.generate_gui_l(i + 1))

    return button_icons_l, button_cbs_l

def drawPage():
    if page == 0:
        return drawPage1()
    else:
        return drawStoredPage(page)

def drawPage1():
    gui_input_list = [
        gui.SUBLIST,
        [gui.DL_COLOR_RGB(0, 0, 0)],    
        [gui.CTRL_TEXT, 30, 100, 8, 0,'RANGE TO BEACON'],
        [gui.CTRL_TEXT, 580, 180, 10, gui.OPT_RIGHTX,range],
        [gui.CTRL_TEXT, 600, 300, 8, 0,'M'],
        [gui.DL_COLOR_RGB(255, 255, 255)]
    ]
    return gui_input_list

def remove_item_cb(b):
    storedResults.pop(int(b[8]))
    led_flash()
    draw()

def drawStoredPage(drawPage):
    global page
    global version_gui
    gui_input_list = [
        gui.SUBLIST,
        [gui.DL_COLOR_RGB(0, 0, 0)],
    ]

    results = storedResults

    if len(results) > 10:
        results = storedResults[10*(drawPage-1):10*(drawPage-1)+10]
    else:
        page = 1

    t_pages = ((len(storedResults)-1)//10 + 1)
    if t_pages < page:
        t_pages = page

    page_no = "{} OF {}".format(page,t_pages)

    gui_input_list.append([gui.DL_COLOR_RGB(255, 255, 255)])
    gui_input_list.append([gui.CTRL_TEXT, 690, 10, 8, 0, str(page_no)])

    for i, storedValue in enumerate(results):
        gui_input_list.append([gui.DL_COLOR_RGB(0, 0, 0)])
        gui_input_list.append([gui.CTRL_TEXT, 20+390* (i // 5), 80 + (i%5) *70, 8,0,str(storedValue)+'M'])
        gui_input_list.append([gui.DL_COLOR_RGB(255, 255, 255)])
        gui_input_list.append([gui.CTRL_FLATBUTTON, 200+390* (i // 5), 75 + (i%5) *70, 150,50,28,'Remove',remove_item_cb,str(10*(page-1)+i)])

    if (len(results) < 1):
        gui_input_list.append([gui.DL_COLOR_RGB(0, 0, 0)])
        gui_input_list.append([gui.CTRL_TEXT, 190, 210, 8, 0, 'NO RESULTS TO DISPLAY'])
        gui_input_list.extend(version_gui)

    return gui_input_list

def init_buttons():
    global button_layouts
    button_layouts = {}
    button_layouts['main'] = create_buttons('Reset', 'Zero', 'Save', 'Next')
    button_layouts['results1'] = create_buttons('Reset', 'Save', 'Previous', 'Next')
    button_layouts['results2'] = create_buttons('Reset', 'Save', 'Previous')
    button_layouts['all'] = create_buttons('Reset', 'Zero', 'Save', 'Previous', 'Next')

def button_options():
    global button_layouts
    global storedResults
    gui_buttons = []

    results = ((len(storedResults)-1)//10 + 1)
    if results < page:
        results = page

    if page == 0: # Main page
        gui_buttons.append(button_layouts['main'][0])
        gui_buttons.extend(button_layouts['main'][1])

    elif (page != 0) and (results > page): # if not on main page and total number of pages != current page
        gui_buttons.append(button_layouts['results1'][0])
        gui_buttons.extend(button_layouts['results1'][1])

    elif (page != 0) and (results == page): # if not on main page and total number of pages == current page
        gui_buttons.append(button_layouts['results2'][0])
        gui_buttons.extend(button_layouts['results2'][1])

    else: # incase of error, offer all options
        gui_buttons.append(button_layouts['all'][0])
        gui_buttons.extend(button_layouts['all'][1])

    return gui_buttons

def draw():
    gui_l = [
        [gui.EVT_VSYNC, vsync_cb],
        [gui.EVT_SWIPE, swipe_r, swipe_cb],
        [gui.EVT_PRESS, view_press_cb],
        [gui.DL_SAVE_CONTEXT()],
        [gui.DL_CLEAR_COLOR_RGB(0, 41, 66)],
        [gui.DL_CLEAR(1, 1, 1)],
        [gui.DL_SCISSOR_SIZE(800, 350)],
        [gui.DL_SCISSOR_XY(0, 65)],
        [gui.DL_CLEAR_COLOR_RGB(255, 255, 255)],
        [gui.DL_CLEAR(1, 1, 1)],
        [gui.DL_RESTORE_CONTEXT()],
        [gui.DL_COLOR_RGB(255, 255, 255)],
        [gui.CTRL_TEXT, 20, 10, 8, 0, 'VBOX PRECISION RANGING SYSTEM'],
    ]

    gui_l.extend(button_options())

    gui_l.append(drawPage())

    gui.show(gui_l)

def init_info():
    global version_gui
    unit = vts.unit_info()
    version_gui.append([gui.DL_COLOR_RGB(169, 169, 169)])
    version_gui.append([gui.CTRL_TEXT, 10, 380, 29, 0, 'SN: {:06}'.format(unit['Serial Number'])])
    version_gui.append([gui.CTRL_TEXT, 590, 380, 29, 0, 'App Version: {}'.format(version_info['app_version'])])
    version_gui.append([gui.CTRL_TEXT, 270, 380, 29, 0, 'Sys Version: {}'.format(unit['Firmware Version'])])

def serial_cb():
    global range, filtered, serial_buffer, read_pos
    bytesWaiting = serial.available()
    dataIn = serial.read(bytesWaiting)
    all_data = us.unpack((bytesWaiting * 's'), dataIn)
    try:
        for i in all_data:
            if i != b'\x00':
                serial_buffer[read_pos] = i
                read_pos +=1
                if b'\n' == i:
                    # print('Current Serial buffer: {}'.format(serial_buffer))
                    message = b"".join(serial_buffer[0:read_pos])
                    message = str(message, 'utf-8').split(',')
                    # print('message:{}'.format(message))
                    try:
                        raw_data = float(message[0])
                        # print(raw_data)

                        filtered = filter(raw_data-offset)

                        pad = '06' if filtered >= 0 else '07'
                        range[0] = "{:{p}.2f}".format(filtered, p=pad)
                    except ValueError:
                        pass # Silently throw away serial data which does not match set target
                        # print("Value Error: Raw: {} \t\tFull Message: {}".format(message[0], serial_buffer))
                    read_pos = 0
    except IndexError: # Buffer full of non-matching data (no '\n' value has come across in two complete message cycles) -Should never get here
        for i in range(len(serial_buffer)):
            serial_buffer[i] = 0 # Empty the buffer as likely user has connected to a different serial stream (not VPRS)
            read_pos = 0
        # print("Emptied Buffer")

def capture_splash_screen_list():
    import ft8xx as ft
    import ustruct as us
    # try:
    # Wait for vsync
    frames_now = ft.rd32(ft.REG_FRAMES)
    while frames_now == ft.rd32(ft.REG_FRAMES):
        pass
    # Swap display list
    ft.wr32(ft.REG_DLSWAP, 1)
    # Read 50 words
    dlist = bytearray(200)
    ft.rdbuf(ft.RAM_DL, dlist)
    # Swap DL back again
    ft.wr32(ft.REG_DLSWAP, 1)
    # Truncate list after DL_END() command (0x21000000)
    dlist = list(us.unpack('L' * 50, dlist))
    gui_l = dlist[:dlist.index(0x21000000) + 1]
    return gui_l

def display_splash_screen():
    splash = vts.Timer(2000, True)
    unit = vts.unit_info()
    try:
        splash_screen_l = capture_splash_screen_list()
        gui_l = [
            splash_screen_l,
            [gui.CTRL_TEXT, 790, 460, 29, gui.OPT_RIGHTX | gui.OPT_CENTERY,
                'Version : {}'.format(version_info['app_version'])],
            [gui.CTRL_TEXT, 10, 460, 29, gui.OPT_CENTERY,
                'Serial : {:06d}'.format(unit['Serial Number'])],
        ]
        gui.show(gui_l)

        while splash.counter() < 1:
            continue
    except Exception as e:
        print ('Failed to display splash screen : {}'.format(e))

    vts.Timer.destroy_all()

# Main #########################################################################
def main():
    display_splash_screen()

    global bank
    bank = Image_Bank((
        (path + 'icon-reset2.png',                         'Reset'),
        (path + 'icon-save.png',                           'Save'),
        (path + 'icon-zero.png',                           'Zero'),
        (path + 'icon-forwards.png',                       'Next'),
        (path + 'icon-backwards.png',                      'Previous')
    ))

    # Initialization ###############################################################
    can.reset(can.CAN1, 500000)
    # Set up CAN callback
    can.set_callback(can.CAN1, can1_cb)
    #Turn on Termination
    can.termination(can.CAN1, True)
    #Listen on all ids
    can.add_rx_id(can.CAN1,0,0)
    # Disable CAN test mode
    can.test_mode(can.CAN1, 0)
    # Initialization ###############################################################
    can.reset(can.CAN0, 500000)
    # Set up CAN callback
    can.set_callback(can.CAN0, can0_cb)
    #Turn on Termination
    can.termination(can.CAN0, True)
    #Listen on all ids
    can.add_rx_id(can.CAN0,0,0)
    # Disable CAN test mode
    can.test_mode(can.CAN0, 0)

    serial.open() #Default 115200, 1byte flag
    serial.set_callback(serial_cb)

    load_fonts()

    init_buttons()

    init_info()



    draw()

if __name__ == '__main__':
    main()
