/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2022 Aidan MacDonald
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#include "plugin.h"

/* should be more than enough */
#define MAX_ROOTS 128

static enum plugin_status plugin_status = PLUGIN_OK;
static char tmpbuf[MAX_PATH+1];
static char tmpbuf2[MAX_PATH+1];
static char cur_root[MAX_PATH];
static char roots[MAX_ROOTS][MAX_PATH];
static int nroots;

/* Read a redirect file and return the path */
static char* read_redirect_file(const char* filename)
{
    int fd = rb->open(filename, O_RDONLY);
    if(fd < 0)
        return NULL;

    ssize_t ret = rb->read(fd, tmpbuf, sizeof(tmpbuf));
    rb->close(fd);
    if(ret < 0 || (size_t)ret >= sizeof(tmpbuf))
        return NULL;

    /* relative paths are ignored */
    if(tmpbuf[0] != '/')
        ret = 0;

    /* remove trailing control chars (newlines etc.) */
    for(ssize_t i = ret - 1; i >= 0 && tmpbuf[i] < 0x20; --i)
        tmpbuf[i] = '\0';

    return tmpbuf;
}

/* Search for a redirect file, like get_redirect_dir() */
static const char* read_redirect(void)
{
    for(int vol = NUM_VOLUMES-1; vol >= MULTIBOOT_MIN_VOLUME; --vol) {
        rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR);
        const char* path = read_redirect_file(tmpbuf);
        if(path && path[0] == '/') {
            /* prepend the volume because that's what we expect */
            rb->snprintf(tmpbuf2, sizeof(tmpbuf2), "/<%d>%s", vol, path);
            return tmpbuf2;
        }
    }

    tmpbuf[0] = '\0';
    return tmpbuf;
}

/* Remove all redirect files except for the one on volume 'exclude_vol' */
static int clear_redirect(int exclude_vol)
{
    int ret = 0;
    for(int vol = MULTIBOOT_MIN_VOLUME; vol < NUM_VOLUMES; ++vol) {
        if(vol == exclude_vol)
            continue;

        rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR);
        if(rb->file_exists(tmpbuf) && rb->remove(tmpbuf) < 0)
            ret = -1;
    }

    return ret;
}

/* Save a path to the redirect file */
static int write_redirect(const char* abspath)
{
    /* get the volume (required) */
    const char* path = abspath;
    int vol = rb->path_strip_volume(abspath, &path, false);
    if(path == abspath)
        return -1;

    /* remove all other redirect files */
    if(clear_redirect(vol))
        return -1;

    /* open the redirect file on the same volume */
    rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s", vol, BOOT_REDIR);
    int fd = rb->open(tmpbuf, O_WRONLY|O_CREAT|O_TRUNC, 0644);
    if(fd < 0)
        return fd;

    size_t pathlen = rb->strlen(path);
    ssize_t ret = rb->write(fd, path, pathlen);
    if(ret < 0 || (size_t)ret != pathlen) {
        rb->close(fd);
        return -1;
    }

    ret = rb->write(fd, "\n", 1);
    rb->close(fd);
    if(ret != 1)
        return -1;

    return 0;
}

/* Check if the firmware file is valid
 * TODO: this should at least check model number or something */
static bool check_firmware(const char* path)
{
    return rb->file_exists(path);
}

static int root_compare(const void* a, const void* b)
{
    const char* as = a;
    const char* bs = b;
    return rb->strcmp(as, bs);
}

/* Scan the filesystem for possible redirect targets. To prevent this from
 * taking too long we only check the directories in the root of each volume
 * look check for a rockbox firmware binary underneath /dir/.rockbox. If it
 * exists then we report /<vol#>/dir as a root. */
static int find_roots(void)
{
    const char* bootdir = *BOOTDIR == '/' ? &BOOTDIR[1] : BOOTDIR;
    nroots = 0;

    for(int vol = MULTIBOOT_MIN_VOLUME; vol < NUM_VOLUMES; ++vol) {
        rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>", vol);

        /* try to open the volume root; ignore failures since they'll
         * occur if the volume is unmounted */
        DIR* dir = rb->opendir(tmpbuf);
        if(!dir)
            continue;

        struct dirent* ent;
        while((ent = rb->readdir(dir))) {
            /* skip non-directories */
            if ((rb->dir_get_info(dir, ent).attribute & ATTR_DIRECTORY) == 0) {
                continue;
            }

            const char *dname = ent->d_name;
            /* check for bootdir in the root of the volume */
            if (rb->strcmp(bootdir, dname) == 0) {
                dname = "";
            }

            int r = rb->snprintf(tmpbuf, sizeof(tmpbuf), "/<%d>/%s/%s/%s",
                                 vol, dname, bootdir, BOOTFILE);

            if(r < 0 || (size_t)r >= sizeof(tmpbuf))
                continue;

            if(check_firmware(tmpbuf)) {
                rb->snprintf(roots[nroots], MAX_PATH, "/<%d>/%s",
                             vol, dname);
                nroots += 1;

                /* quit if we hit the maximum */
                if(nroots == MAX_ROOTS) {
                    vol = NUM_VOLUMES;
                    break;
                }
            }
        }

        rb->closedir(dir);
    }

    rb->qsort(roots, nroots, MAX_PATH, root_compare);
    return nroots;
}

static const char* picker_get_name_cb(int selected, void* data,
                                      char* buffer, size_t buffer_len)
{
    (void)data;
    (void)buffer;
    (void)buffer_len;
    return roots[selected];
}

static int picker_action_cb(int action, struct gui_synclist* lists)
{
    (void)lists;
    if(action == ACTION_STD_OK)
        action = ACTION_STD_CANCEL;
    return action;
}

static bool show_picker_menu(int* selection)
{
    struct simplelist_info info;
    rb->simplelist_info_init(&info, "Select new root", nroots, NULL);
    info.selection = *selection;
    info.get_name = picker_get_name_cb;
    info.action_callback = picker_action_cb;
    if(rb->simplelist_show_list(&info))
        return true;

    if(0 <= info.selection && info.selection < nroots)
        *selection = info.selection;

    return false;
}

enum {
    MB_SELECT_ROOT,
    MB_CLEAR_REDIRECT,
    MB_CURRENT_ROOT,
    MB_SAVE_AND_EXIT,
    MB_SAVE_AND_REBOOT,
    MB_EXIT,
    MB_NUM_ITEMS,
};

static const char* menu_get_name_cb(int selected, void* data,
                                    char* buffer, size_t buffer_len)
{
    (void)data;

    switch(selected) {
    case MB_SELECT_ROOT:
        return "Select root";

    case MB_CLEAR_REDIRECT:
        return "Clear redirect";

    case MB_CURRENT_ROOT:
        if(cur_root[0]) {
            rb->snprintf(buffer, buffer_len, "Using root: %s", cur_root);
            return buffer;
        }

        return "Using default root";

    case MB_SAVE_AND_EXIT:
        return "Save and Exit";

    case MB_SAVE_AND_REBOOT:
        return "Save and Reboot";

    case MB_EXIT:
    default:
        return "Exit";
    }
}

static int menu_action_cb(int action, struct gui_synclist* lists)
{
    int selected = rb->gui_synclist_get_sel_pos(lists);

    if(action != ACTION_STD_OK)
        return action;

    switch(selected) {
    case MB_SELECT_ROOT: {
        if(find_roots() <= 0) {
            rb->splashf(3*HZ, "No roots found");
            break;
        }

        int root_sel = nroots-1;
        for(; root_sel > 0; --root_sel)
            if(!rb->strcmp(roots[root_sel], cur_root))
                break;

        if(show_picker_menu(&root_sel))
            return SYS_USB_CONNECTED;

        rb->strcpy(cur_root, roots[root_sel]);
        action = ACTION_REDRAW;
    } break;

    case MB_CLEAR_REDIRECT:
        if(cur_root[0]) {
            memset(cur_root, 0, sizeof(cur_root));
            rb->splashf(HZ, "Cleared redirect");
        }

        action = ACTION_REDRAW;
        break;

    case MB_SAVE_AND_REBOOT:
    case MB_SAVE_AND_EXIT: {
        int ret;
        if(cur_root[0])
            ret = write_redirect(cur_root);
        else
            ret = clear_redirect(-1);

        if(ret < 0)
            rb->splashf(3*HZ, "Couldn't save settings");
        else
            rb->splashf(HZ, "Settings saved");

        action = ACTION_STD_CANCEL;
        plugin_status = (ret < 0) ? PLUGIN_ERROR : PLUGIN_OK;

        if(ret >= 0 && selected == MB_SAVE_AND_REBOOT)
            rb->sys_reboot();
    } break;

    case MB_EXIT:
        return ACTION_STD_CANCEL;

    default:
        action = ACTION_REDRAW;
        break;
    }

    return action;
}

static bool show_menu(void)
{
    struct simplelist_info info;
    rb->simplelist_info_init(&info, "Multiboot", MB_NUM_ITEMS, NULL);
    info.get_name = menu_get_name_cb;
    info.action_callback = menu_action_cb;
    return rb->simplelist_show_list(&info);
}

enum plugin_status plugin_start(const void* param)
{
    (void)param;

#ifdef HAVE_TOUCHSCREEN
    rb->touchscreen_set_mode(rb->global_settings->touch_mode);
#endif

    /* load the current root */
    const char* myroot = read_redirect();
    rb->strcpy(cur_root, myroot);

    /* display the menu */
    if(show_menu())
        return PLUGIN_USB_CONNECTED;
    else
        return plugin_status;
}
