"""
Menu Script
===========
This is a modified menu script by Pixtxa.
You can customize this script however you want :)  If you want to go back to
card10s default version, just delete this file; the firmware will recreate it on
next run.
"""
import buttons
import collections
import color
import display
import os
import simple_menu
import sys
import ujson
import utime
import leds

App = collections.namedtuple("App", ["name", "path"])

# Favorite apps which are shown at the very top of the app list
FAVORITE_APPS = ["personal_state", "ecg"]

# Scroll delay for advanced info texts
SCROLLDELAY = 4

leds.set_flashlight(0)

def enumerate_entries():
    if 'main.py' in os.listdir("/"):
        yield App("Home", "main.py")

    yield App("USB Storage", "USB_STORAGE_FLAG")

    yield from enumerate_apps(FAVORITE_APPS)

    yield App("*Refresh this List", "REFRESH_LIST_FLAG")

    yield from sorted(enumerate_apps(), key=lambda b: b.name.lower())


def enumerate_apps(apps=None):
    """List all installed apps."""
    for app in apps or os.listdir("/apps"):
        if app.startswith("."):
            continue

        # Skip special apps when enumerating from filesystem
        if apps is None and app in FAVORITE_APPS:
            continue

        if app.endswith(".py") or app.endswith(".elf"):
            yield App(app, "/apps/" + app)
            continue

        try:
            with open("/apps/" + app + "/metadata.json") as f:
                info = ujson.load(f)

            yield App(
                info["name"], "/apps/{}/{}".format(app, info.get("bin", "__init__.py"))
            )
        except Exception as e:
            print("'{}': metadata.json is invalid ... hoping for the best.".format(app))
            sys.print_exception(e)
            pyfile = "/apps/{}/__init__.py".format(app)
            try:
                open(pyfile).close()
                yield App(app, pyfile)
            except OSError:
                print(pyfile, "does not even exist :(")
                pass


def usb_mode(disp):
    os.usbconfig(os.USB_FLASH)

    disp.clear(color.CHAOSBLUE)
    disp.print("USB Storage", posx=3, posy=20, fg=color.CHAOSBLUE_DARK)
    disp.print("open", posx=52, posy=40, fg=color.CHAOSBLUE_DARK)
    disp.update()

    utime.sleep_ms(200)

    # Wait for select button to be released
    while buttons.read() == buttons.TOP_RIGHT:
        pass

    # Wait for any button to be pressed and disable USB storage again
    while buttons.read() == 0:
        pass

    os.usbconfig(os.USB_SERIAL)


def draw_info(disp, offset, lines):
    disp.clear()
    
    for line in range(4):
        if lines[line]:
            if line%2:
                bgcol = color.COMMYELLOW
            else:
                bgcol = color.COMMYELLOW_DARK
            linelen = len(lines[line])
            useoffset = offset%linelen
            linerep = ""
            if (useoffset+11) > linelen:
                linerep = lines[line][:(useoffset+11 - linelen)]
            disp.rect(0, line*20, 159, (line+1)*20, col=bgcol)
            disp.print(lines[line][useoffset:(useoffset+11)] + linerep, posx=3, posy=line*20, fg=color.BLACK, bg=bgcol)
    disp.update()
    
def advanced_info(disp, app):
    folder = '/'.join(app.path.split('/')[:-1]) # delete last entry of path
    info = {}
    if folder:
        try:
            with open(folder+'/metadata.json') as data:
                info = ujson.loads(data.read())
                {"category":"hardware","author":"card10 contributors","revision":-1,"source":"preload"}
                {"name":"Essensangebote","description":"Essensangebot der Kantine","category":"system","author":"card10 contributors","revision":1}
        except:
            pass
        info['path'] = folder
    else:
        folder = "/"
        if app.path == 'main.py':
            info = {"description":"This is the default app. It's loaded at system start and when you don't press any button in the menu","path":"/main.py","category":"System"}
        elif app.path == 'USB_STORAGE_FLAG':
            info = {"description":"Puts card10 into USB Storage mode.","path":"/menu.py","category":"System","author":"card10 contributors"}
        elif app.path == 'REFRESH_LIST_FLAG':
            info = {"description":"Refreshes the app list of the menu and saves it. Instead of the original menu.py script, this version doesn't create the list on every start, which is faster, but needs manual refreshing.","path":"/menu.py","category":"System","author":"Pixtxa"}
    
    if folder[0] != '/':
        folder = '/' + folder
    
    if 'name' not in info:
        info['name'] = app.name

    lines = ['','','','']
    
    line = 0
    lines[line] = str(info.pop('name')) + ' -'
    if 'revision' in info:
        lines[line] += ' Rev: ' + str(info.pop('revision')) + ' -'
    if 'path' in info:
        lines[line] += ' Path: ' + str(info.pop('path')) + ' -'
    if 'path' in info:
        lines[line] += ' Path: ' + str(info.pop('path')) + ' -'
    lines[line] += "-- Name: "
    
    line +=1
    
    if 'description' in info:
        lines[line] = str(info.pop('description')) + ' --- Description: '
        line += 1
    else:
        try:
            with open(folder+'/README.md') as data:
                lines[line] = data.read() + ' --- Readme: '
                line += 1
        except:
            lines[line] = 'No description avialable. ---'
            
    if 'author' in info:
        lines[line] += 'Author: ' + str(info.pop('author')) + ' - '
    if 'category' in info:
        lines[line] += 'Category: ' + str(info.pop('category')) + ' - '
    if lines[line]:
        lines[line] = lines[line][:-1] + '-- '
        line += 1
        
    for entry in info:
        field = str(entry).replace('_',' ')
        if field.islower() or field.isupper():
            field = ''.join([s[0].upper() + s.split(s[0],1)[1].lower() for s in field.split(' ')])
        lines[line] += str(entry) + ': ' + str(info[entry]) + ' - '
    if lines[line]:
        lines[line] = lines[line][:-1] + '-- '
        line += 1
            
    scroll = 0
    offset = 0
    draw_info(disp, offset, lines)
    # Wait for select button to be released
    while buttons.read() == buttons.TOP_RIGHT:
        pass

    #Debounce
    utime.sleep_ms(20)
    buttons.read()
    
    # Wait for select button to be pressed again
    btn = 0
    while not (btn & buttons.TOP_RIGHT):
        btn = buttons.read()

        #Reset view when bottom left button is pressed
        if btn & buttons.BOTTOM_LEFT:
            scroll = 0
            offset = 0
            draw_info(disp, offset, lines)

        #Pause scrolling when bottom right button is pressed
        if not (btn & buttons.BOTTOM_RIGHT):
            scroll += 1
            if scroll >= SCROLLDELAY:
                offset += 1
                scroll = 0
                draw_info(disp, offset, lines)

        utime.sleep_ms(10)


class MainMenu(simple_menu.Menu):
    timeout = 30.0
    color_1 = color.CAMPGREEN
    color_2 = color.CAMPGREEN_DARK

    def entry2name(self, app):
        return app.name

    def on_select(self, app, index):
        self.disp.clear().update()
        try:
            if app.path == "USB_STORAGE_FLAG":
                usb_mode(self.disp)
#                self.exit() #Refresh menu list
                return

            if app.path == "REFRESH_LIST_FLAG":
                self.exit()

            print("Trying to load " + app.path)
            os.exec(app.path)
        except OSError as e:
            print("Loading failed: ")
            sys.print_exception(e)
            self.error("Loading", "failed")
            utime.sleep(1.0)
            os.exit(1)
            
    def on_long_select(self, app, index):
        advanced_info(self.disp, app)

    def on_timeout(self):
        try:
            f = open("main.py")
            f.close()
            os.exec("main.py")
        except OSError:
            pass


def loading_message():
    with display.open() as disp:
        disp.clear(color.COMMYELLOW)
        disp.print("Loading", posx=31, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
        disp.print("menu ...", posx=24, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
        disp.update()


def no_apps_message():
    """Display a warning if no apps are installed."""
    with display.open() as disp:
        disp.clear(color.COMMYELLOW)
        disp.print(
            " No apps ", posx=17, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
        )
        disp.print(
            "available", posx=17, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW
        )
        disp.update()

    while True:
        utime.sleep(0.5)

def refresh_app_list():
    global apps
    with display.open() as disp:
        disp.clear(color.COMMYELLOW)
        disp.print("Refreshing", posx=10, posy=20, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
        disp.print("list ...", posx=24, posy=40, fg=color.COMMYELLOW_DARK, bg=color.COMMYELLOW)
        disp.update()
    try:
        apps = list(enumerate_entries())
    except OSError:
        apps = []

    if not apps:
        no_apps_message()

    # Write apps list to filesystem, so it can be reused on next run
    # Format for each entry: app.name[tab]app.path[newline]
    with open('menu.apps', 'w') as applist:
        for app in apps:
            applist.write('\t'.join(app)+'\n')

if __name__ == "__main__":
    global apps
    loading_message()
    try:
        apps = []
        # Try to read the apps from filesystem
        # If the file is corrupted or missing, this fails and the list will be refreshed
        with open('menu.apps') as applist:
            for app in applist:
                apps.append(App(*app[:-1].split('\t')))
    except:
        apps = []

    while True:
        if "REFRESH_LIST_FLAG" not in [app.path for app in apps]: # App list is empty or corrupted => refresh it
            refresh_app_list()
        MainMenu(apps).run()
        apps = [] #Reset list so it will be refreshed