#include <rg_system.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>

#include "applications.h"
#include "bookmarks.h"
#include "gui.h"

#define CRC_CACHE_MAGIC 0x21112223
#define CRC_CACHE_MAX_ENTRIES 8192
static struct __attribute__((__packed__))
{
    uint32_t magic;
    uint32_t count;
    struct {
        uint32_t key;
        uint32_t crc;
    } entries[CRC_CACHE_MAX_ENTRIES];
} *crc_cache;
static bool crc_cache_dirty = true;

static retro_app_t *apps[24];
static int apps_count = 0;

static int scan_folder_cb(const rg_scandir_t *entry, void *arg)
{
    retro_app_t *app = (retro_app_t *)arg;
    uint8_t type = RETRO_TYPE_INVALID;

    // Skip hidden files
    if (entry->basename[0] == '.')
        return RG_SCANDIR_SKIP;

    if (entry->is_file)
    {
        if (rg_extension_match(entry->basename, app->extensions))
            type = RETRO_TYPE_FILE;
    }
    else if (entry->is_dir)
    {
        RG_LOGI("Found subdirectory '%s'", entry->path);
        type = RETRO_TYPE_FOLDER;
    }

    if (type == RETRO_TYPE_INVALID)
        return RG_SCANDIR_CONTINUE;

    if (app->files_count + 1 > app->files_capacity)
    {
        size_t new_capacity = (app->files_capacity * 1.5) + 1;
        retro_file_t *new_buf = realloc(app->files, new_capacity * sizeof(retro_file_t));
        if (!new_buf)
        {
            RG_LOGW("Ran out of memory, file scanning stopped at %d entries ...", app->files_count);
            return RG_SCANDIR_STOP;
        }
        app->files = new_buf;
        app->files_capacity = new_capacity;
    }

    char *name = rg_bucket_insert(app->filenames, entry->basename, strlen(entry->basename) + 1);
    if (!name)
    {
        RG_LOGW("Ran out of memory for names, file scanning stopped at %d entries ...", app->files_count);
        return RG_SCANDIR_STOP;
    }

    app->files[app->files_count++] = (retro_file_t) {
        .name = name,
        .folder = rg_unique_string(entry->dirname),
        .checksum = 0,
        .missing_cover = 0,
        .saves = 0,
        .type = type,
        .app = (void*)app,
    };

    return RG_SCANDIR_CONTINUE;
}

static int scan_saves_cb(const rg_scandir_t *entry, void *arg)
{
    if (entry->is_file && rg_extension_match(entry->basename, "sav"))
    {
        retro_app_t *app = (retro_app_t *)arg;
        for (size_t i = 0; i < app->files_count; i++)
        {
            retro_file_t *file = &app->files[i];
            // Saves are the rom name with possibly `.sav` or `-0.sav` appended.
            if (strncmp(entry->basename, file->name, strlen(file->name)) == 0)
            {
                // FIXME: Check if folder matches first
                file->saves++;
                break;
            }
        }
    }
    return RG_SCANDIR_CONTINUE;
}

static void application_init(retro_app_t *app)
{
    RG_LOGI("Initializing application '%s' (%s)", app->description, app->partition);

    if (app->initialized)
        return;

    rg_storage_mkdir(app->paths.covers);
    rg_storage_mkdir(app->paths.saves);
    rg_storage_mkdir(app->paths.roms);

    rg_storage_scandir(app->paths.roms, scan_folder_cb, app, RG_SCANDIR_RECURSIVE);
    rg_storage_scandir(app->paths.saves, scan_saves_cb, app, RG_SCANDIR_RECURSIVE);
    // rg_storage_scandir(app->paths.covers, scan_folder_cb3, app, RG_SCANDIR_RECURSIVE);

    app->use_crc_covers = rg_storage_exists(strcat(app->paths.covers, "/0"));
    app->paths.covers[strlen(app->paths.covers) - 2] = 0;

    app->initialized = true;
}

static const char *get_file_path(retro_file_t *file)
{
    static char buffer[RG_PATH_MAX + 1];
    RG_ASSERT_ARG(file);
    snprintf(buffer, RG_PATH_MAX, "%s/%s", file->folder, file->name);
    return buffer;
}

static void application_start(retro_file_t *file, int load_state)
{
    RG_ASSERT_ARG(file);
    char *part = strdup(file->app->partition);
    char *name = strdup(file->app->short_name);
    char *path = strdup(get_file_path(file));
    int flags = (gui.startup_mode ? RG_BOOT_ONCE : 0) | (load_state != -1 ? RG_BOOT_RESUME : 0);
    bookmark_add(BOOK_TYPE_RECENT, file); // This could relocate *file, but we no longer need it
    rg_system_switch_app(part, name, path, load_state, flags);
}

static uint32_t crc_read_file(retro_file_t *file, bool interactive)
{
    uint8_t buffer[0x800];
    uint32_t crc_tmp = 0;
    bool done = false;
    int count = -1;
    FILE *fp;

    if (file == NULL)
        return 0;

    if ((fp = fopen(get_file_path(file), "rb")))
    {
        fseek(fp, file->app->crc_offset, SEEK_SET);

        while (count != 0)
        {
            // Give up on any button press to improve responsiveness
            if (interactive && (gui.joystick = rg_input_read_gamepad()))
                break;

            count = fread(buffer, 1, sizeof(buffer), fp);
            crc_tmp = rg_crc32(crc_tmp, buffer, count);
        }

        done = feof(fp);

        fclose(fp);
    }

    return done ? crc_tmp : 0;
}

static void crc_cache_init(void)
{
    crc_cache = calloc(1, sizeof(*crc_cache));
    if (!crc_cache)
    {
        RG_LOGE("Failed to allocate crc_cache!");
        return;
    }

    void *data_ptr = crc_cache;
    size_t data_len = sizeof(*crc_cache);
    rg_storage_read_file(RG_BASE_PATH_CACHE "/crc32.bin", &data_ptr, &data_len, RG_FILE_USER_BUFFER);
    if (crc_cache->magic == CRC_CACHE_MAGIC && crc_cache->count <= CRC_CACHE_MAX_ENTRIES)
    {
        RG_LOGI("Loaded CRC cache (entries: %d)", (int)crc_cache->count);
        crc_cache_dirty = false;
    }
    else
    {
        crc_cache->count = 0;
    }
}

static void crc_cache_save(void)
{
    if (!crc_cache || !crc_cache_dirty)
        return;

    RG_LOGI("Saving CRC cache...");
    size_t data_len = RG_MIN(8 + (crc_cache->count * sizeof(crc_cache->entries[0])), sizeof(*crc_cache));
    crc_cache_dirty = !rg_storage_write_file(RG_BASE_PATH_CACHE"/crc32.bin", crc_cache, data_len, 0);
}

static uint32_t crc_cache_calc_key(retro_file_t *file)
{
    // return ((uint64_t)rg_crc32(0, (void *)file->name, strlen(file->name)) << 33 | file->size);
    // This should be reasonably unique
    const char *path = get_file_path(file);
    return rg_crc32(0, (const uint8_t *)path, strlen(path));
}

static int crc_cache_find(retro_file_t *file)
{
    if (!crc_cache)
        return -1;

    uint32_t key = crc_cache_calc_key(file);

    for (int i = 0; i < crc_cache->count; i++)
    {
        if (crc_cache->entries[i].key == key)
            return i;
    }

    return -1;
}

static uint32_t crc_cache_lookup(retro_file_t *file)
{
    int index = crc_cache_find(file);
    if (index != -1)
        return crc_cache->entries[index].crc;
    return 0;
}

static void crc_cache_update(retro_file_t *file)
{
    if (!crc_cache)
        return;

    uint32_t key = crc_cache_calc_key(file);
    int index = crc_cache_find(file);
    if (index == -1)
    {
        if (crc_cache->count < CRC_CACHE_MAX_ENTRIES)
            index = crc_cache->count++;
        else
            index = rand() % CRC_CACHE_MAX_ENTRIES;

        RG_LOGI("Adding %08X => %08X to cache (new total: %d)",
            (int)key, (int)file->checksum, (int)crc_cache->count);
    }
    else
    {
        RG_LOGI("Updating %08X => %08X to cache (total: %d)",
            (int)key, (int)file->checksum, (int)crc_cache->count);
    }

    crc_cache->magic = CRC_CACHE_MAGIC;
    crc_cache->entries[index].key = key;
    crc_cache->entries[index].crc = file->checksum;
    crc_cache_dirty = true;

    // crc_cache_save();
}

void crc_cache_prebuild(void)
{
    if (!crc_cache)
        return;

    for (int i = 0; i < apps_count; i++)
    {
        retro_app_t *app = apps[i];

        if (!app->available)
            continue;

        if (!app->initialized)
            application_init(app);

        for (int j = 0; j < app->files_count; j++)
        {
            retro_file_t *file = &app->files[j];

            rg_gui_draw_message(_("Scanning %s %d/%d"), app->short_name, j, app->files_count);

            // Give up on any button press to improve responsiveness
            if (rg_input_read_gamepad())
                break;

            if (file->checksum)
                continue;

            if ((file->checksum = crc_cache_lookup(file)))
                continue;

            if ((file->checksum = crc_read_file(file, true)))
                crc_cache_update(file);
        }

        if (rg_input_read_gamepad())
            break;

        crc_cache_save();
        gui_redraw();
    }

    crc_cache_save();
}

static void tab_refresh(tab_t *tab, const char *selected)
{
    retro_app_t *app = (retro_app_t *)tab->arg;

    memset(&tab->status, 0, sizeof(tab->status));

    const char *basepath = rg_unique_string(app->paths.roms);
    const char *folder = rg_unique_string(tab->navpath ?: basepath);
    size_t items_count = 0;
    char *ext = NULL;

    if (folder == basepath)
        tab->navpath = NULL;

    if (app->files_count > 0)
    {
        gui_resize_list(tab, app->files_count);

        for (size_t i = 0; i < app->files_count; i++)
        {
            retro_file_t *file = &app->files[i];

            if (file->type == RETRO_TYPE_INVALID || !file->name)
                continue;

            if (file->folder != folder && strcmp(file->folder, folder) != 0)
                continue;

            if (file->type == RETRO_TYPE_FOLDER)
            {
                listbox_item_t *item = &tab->listbox.items[items_count++];
                snprintf(item->text, sizeof(item->text), "[%.40s]", file->name);
                item->group = 1;
                item->arg = file;
            }
            else if (file->type == RETRO_TYPE_FILE)
            {
                listbox_item_t *item = &tab->listbox.items[items_count++];
                snprintf(item->text, sizeof(item->text), "%s", file->name);
                if ((ext = strrchr(item->text, '.')))
                    *ext = 0;
                item->group = 2;
                item->arg = file;
            }
        }
    }

    gui_resize_list(tab, items_count);
    gui_sort_list(tab);

    if (items_count == 0)
    {
        gui_resize_list(tab, 6);
        sprintf(tab->listbox.items[0].text, _("Welcome to Retro-Go!"));
        sprintf(tab->listbox.items[1].text, " ");
        sprintf(tab->listbox.items[2].text, _("Place roms in folder: %s"), rg_relpath(app->paths.roms));
        sprintf(tab->listbox.items[3].text, _("With file extension: %s"), app->extensions);
        sprintf(tab->listbox.items[4].text, " ");
        sprintf(tab->listbox.items[5].text, _("You can hide this tab in the menu"));
        tab->listbox.cursor = 4;
    }
    else if (selected)
    {
        for (int i = 0; i < tab->listbox.length; i++)
        {
            retro_file_t *file = tab->listbox.items[i].arg;
            if (file && strcmp(file->name, selected) == 0) // file->folder == selected->folder
            {
                tab->listbox.cursor = i;
                break;
            }
        }
    }

    gui_scroll_list(tab, SCROLL_SET, tab->listbox.cursor);
}

static void event_handler(gui_event_t event, tab_t *tab)
{
    listbox_item_t *item = gui_get_selected_item(tab);
    retro_app_t *app = (retro_app_t *)tab->arg;
    retro_file_t *file = (retro_file_t *)(item ? item->arg : NULL);

    if (event == TAB_INIT)
    {
        application_init(app);
        tab->navpath = NULL;

        retro_file_t *selected = bookmark_find_by_app(BOOK_TYPE_RECENT, app);
        if (selected) // && !rg_storage_exists(get_file_path(selected)))
        {
            // rg_storage_exists can take a long time on large folders (200ms), this is much faster
            for (size_t i = 0; i < app->files_count; ++i)
            {
                retro_file_t *file = &app->files[i];
                if (selected->folder == file->folder && strcmp(selected->name, file->name) == 0)
                {
                    tab->navpath = file->folder;
                    break;
                }
            }
        }

        tab_refresh(tab, selected ? selected->name : NULL);
    }
    else if (event == TAB_DEINIT)
    {
        if (app && app->initialized)
        {
            rg_bucket_free(app->filenames);
            app->filenames = rg_bucket_create(4096);
            app->files_count = 0;
            app->initialized = false;
        }
    }
    else if (event == TAB_REFRESH)
    {
        tab_refresh(tab, NULL);
    }
    else if (event == TAB_ENTER || event == TAB_LEAVE || event == TAB_SCROLL)
    {
        gui_set_status(tab, NULL, "");
        gui_set_preview(tab, NULL);
    }
    else if (event == TAB_IDLE)
    {
        if (file && !tab->preview && gui.idle_counter == 1)
            gui_load_preview(tab);
    }
    else if (event == TAB_ACTION)
    {
        if (file)
        {
            if (file->type == RETRO_TYPE_FOLDER)
            {
                tab->navpath = rg_unique_string(get_file_path(file));
                tab->listbox.cursor = 0;
                tab_refresh(tab, NULL);
            }
            else if (file->type == RETRO_TYPE_FILE)
            {
                application_show_file_menu(file, false);
            }
        }
    }
    else if (event == TAB_BACK)
    {
        if (tab->navpath)
        {
            const char *from = rg_basename(rg_unique_string(tab->navpath));

            tab->navpath = rg_unique_string(rg_dirname(tab->navpath));
            tab->listbox.cursor = 0;

            tab_refresh(tab, from);
        }
    }
}

bool application_path_to_file(const char *path, retro_file_t *file)
{
    RG_ASSERT_ARG(path && file);

    for (int i = 0; i < apps_count; ++i)
    {
        size_t baselen = strlen(apps[i]->paths.roms);
        if (strncmp(path, apps[i]->paths.roms, baselen) == 0 && path[baselen] == '/')
        {
            *file = (retro_file_t) {
                .name = strdup(rg_basename(path)),
                .folder = rg_unique_string(rg_dirname(path)),
                .saves = 0xFF, // We don't know, but we want gui_load_preview to check if needed
                .type = RETRO_TYPE_FILE,
                .app = apps[i],
            };
            return true;
        }
    }

    return false;
}

bool application_get_file_crc32(retro_file_t *file)
{
    uint32_t crc_tmp = 0;

    if (file == NULL)
        return false;

    if (file->checksum > 0)
        return true;

    if ((crc_tmp = crc_cache_lookup(file)))
    {
        file->checksum = crc_tmp;
    }
    else
    {
        tab_t *tab = gui_get_current_tab();
        gui_set_status(tab, NULL, "CRC32...");
        gui_redraw(); // gui_draw_status(tab);

        if ((crc_tmp = crc_read_file(file, true)))
        {
            file->checksum = crc_tmp;
            crc_cache_update(file);
        }

        gui_set_status(tab, NULL, "");
        gui_redraw(); // gui_draw_status(tab);
    }

    return file->checksum > 0;
}

static void show_file_info(retro_file_t *file)
{
    char filesize[16];
    char filecrc[16] = "Compute";
    rg_stat_t info = rg_storage_stat(get_file_path(file));

    if (!info.exists)
    {
        rg_gui_alert(_("File not found"), file->name);
        return;
    }

    rg_gui_option_t options[] = {
        {0, _("Name"), (char *)file->name, 1, NULL},
        {0, _("Folder"), (char *)file->folder, 1, NULL},
        {0, _("Size"), filesize, 1, NULL},
        {3, _("CRC32"), filecrc, 1, NULL},
        RG_DIALOG_SEPARATOR,
        {5, _("Delete file"), NULL, 1, NULL},
        {1, _("Close"), NULL, 1, NULL},
        RG_DIALOG_END,
    };

    sprintf(filesize, "%d KB", (int)info.size / 1024);

    while (true) // We loop in case we need to update the CRC
    {
        if (file->checksum)
            sprintf(filecrc, "%08X (%d)", (int)file->checksum, file->app->crc_offset);

        switch (rg_gui_dialog(_("File properties"), options, -1))
        {
        case 3:
            application_get_file_crc32(file);
            continue;
        case 5:
            if (rg_gui_confirm(_("Delete selected file?"), 0, 0))
            {
                if (remove(get_file_path(file)) == 0)
                {
                    bookmark_remove(BOOK_TYPE_FAVORITE, file);
                    bookmark_remove(BOOK_TYPE_RECENT, file);
                    file->type = RETRO_TYPE_INVALID;
                    gui_event(TAB_REFRESH, gui_get_current_tab());
                    return;
                }
            }
            continue;
        default:
            return;
        }
    }
}

void application_show_file_menu(retro_file_t *file, bool advanced)
{
    char *rom_path = strdup(get_file_path(file));
    char *sram_path = rg_emu_get_path(RG_PATH_SAVE_SRAM, rom_path);
    rg_emu_states_t *savestates = rg_emu_get_states(rom_path, 4);
    bool has_save = savestates->used > 0; // Don't rely on file->saves just yet
    bool has_sram = rg_storage_exists(sram_path);
    bool is_fav = bookmark_exists(BOOK_TYPE_FAVORITE, file);
    int slot = -1;

    rg_gui_option_t choices[] = {
        {0, _("Resume game"), NULL, has_save, NULL},
        {1, _("New game"), NULL, 1, NULL},
        RG_DIALOG_SEPARATOR,
        {3, is_fav ? _("Del favorite") : _("Add favorite"), NULL, 1, NULL},
        {2, _("Delete save"), NULL, has_save || has_sram, NULL},
        RG_DIALOG_SEPARATOR,
        {4, _("Properties"), NULL, 1, NULL},
        RG_DIALOG_END,
    };

    int sel = rg_gui_dialog(NULL, choices, has_save ? 0 : 1);
    switch (sel)
    {
    case 0:
        if ((slot = rg_gui_savestate_menu(_("Resume"), rom_path)) == -1)
            break;
        /* fallthrough */
    case 1:
        crc_cache_save();
        gui_save_config();
        application_start(file, slot);
        break;

    case 2:
        while ((slot = rg_gui_savestate_menu(_("Delete save?"), rom_path)) != -1)
        {
            remove(savestates->slots[slot].preview);
            remove(savestates->slots[slot].file);
            // FIXME: We should update the last slot used here
        }
        if (has_sram && rg_gui_confirm(_("Delete sram file?"), 0, 0))
        {
            remove(sram_path);
        }
        break;

    case 3:
        if (is_fav)
            bookmark_remove(BOOK_TYPE_FAVORITE, file);
        else
            bookmark_add(BOOK_TYPE_FAVORITE, file); // This could relocate *file
        break;

    case 4:
        show_file_info(file);
        break;

    default:
        break;
    }

    free(rom_path);
    free(sram_path);
    free(savestates);

    // gui_redraw();
}

static void application(const char *desc, const char *name, const char *exts, const char *part, uint16_t crc_offset)
{
    RG_ASSERT_ARG(desc && name && exts && part);

    if (!rg_system_have_app(part))
    {
        RG_LOGI("Application '%s' (%s) not present, skipping", desc, part);
        return;
    }

    retro_app_t *app = calloc(1, sizeof(retro_app_t));
    apps[apps_count++] = app;

    snprintf(app->description, sizeof(app->description), "%s", desc);
    snprintf(app->short_name, sizeof(app->short_name), "%s", name);
    snprintf(app->partition, sizeof(app->partition), "%s", part);
    snprintf(app->extensions, sizeof(app->extensions), " %s ", exts);
    snprintf(app->paths.covers, RG_PATH_MAX, RG_BASE_PATH_COVERS "/%s", app->short_name);
    snprintf(app->paths.saves, RG_PATH_MAX, RG_BASE_PATH_SAVES "/%s", app->short_name);
    snprintf(app->paths.roms, RG_PATH_MAX, RG_BASE_PATH_ROMS "/%s", app->short_name);
    app->available = rg_system_have_app(app->partition);
    app->files = calloc(100, sizeof(retro_file_t));
    app->files_capacity = 100;
    app->filenames = rg_bucket_create(4096);
    app->crc_offset = crc_offset;

    gui_add_tab(app->short_name, app->description, app, event_handler);
}

void applications_init(void)
{
    application("Nintendo Entertainment System", "nes", "nes fc fds nsf zip", "retro-core", 16);
    application("Super Nintendo", "snes", "smc sfc zip", "snes9x", 0);
    application("Nintendo Gameboy", "gb", "gb gbc zip", "retro-core", 0);
    application("Nintendo Gameboy Color", "gbc", "gbc gb zip", "retro-core", 0);
    application("Nintendo Gameboy Advance", "gba", "gba zip", "gbsp", 0);
    application("Nintendo Game & Watch", "gw", "gw", "retro-core", 0);
    // application("Sega SG-1000", "sg1", "sms sg sg1", "retro-core", 0);
    application("Sega Master System", "sms", "sms sg zip", "retro-core", 0);
    application("Sega Game Gear", "gg", "gg zip", "retro-core", 0);
    application("Sega Mega Drive", "md", "md gen bin zip", "gwenesis", 0);
    application("Coleco ColecoVision", "col", "col rom zip", "retro-core", 0);
    application("NEC PC Engine", "pce", "pce zip", "retro-core", 0);
    application("Atari Lynx", "lnx", "lnx zip", "retro-core", 64);
    // application("Atari 2600", "a26", "a26 zip", "stella-go", 0);
    // application("Neo Geo Pocket Color", "ngp", "ngp ngc zip", "ngpocket-go", 0);
    application("DOOM", "doom", "wad zip", "prboom-go", 0);
    application("MSX", "msx", "rom mx1 mx2 dsk", "fmsx", 0);

    // Special app to bootstrap native esp32 binaries from the SD card
    // application("Bootstrap", "apps", "bin elf", "bootstrap", 0);

    if (rg_system_get_stats().freeMemory > 0x100000)
        crc_cache_init();
}
