#include "rg_system.h"
#include "rg_gui.h"

#include <cJSON.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include "bitmaps/image_hourglass.h"
#include "fonts/fonts.h"

static struct
{
    uint16_t *screen_buffer, *draw_buffer;
    size_t draw_buffer_size;
    int screen_width, screen_height;
    rg_margins_t margins;
    struct
    {
        rg_color_t box_background;
        rg_color_t box_header;
        rg_color_t box_border;
        rg_color_t item_standard;
        rg_color_t item_disabled;
        rg_color_t item_message;
        rg_color_t scrollbar;
        rg_color_t shadow;
    } style;
    char theme_name[32];
    cJSON *theme_obj;
    const rg_font_t *font;
    int font_index;
    int font_height;
    bool show_clock;
    bool initialized;
} gui;

#define SETTING_FONTTYPE    "FontType"
#define SETTING_CLOCK       "Clock"
#define SETTING_THEME       "Theme"
#define SETTING_WIFI_ENABLE "Enable"
#define SETTING_WIFI_SLOT   "Slot"
#define SETTING_LANGUAGE    "Language"

static uint16_t *get_draw_buffer(int width, int height, rg_color_t fill_color)
{
    size_t pixels = width * height;
    if (pixels > gui.draw_buffer_size)
    {
        if (gui.draw_buffer != NULL)
        {
            RG_LOGW("Growing drawing buffer to %dx%d...", width, height);
            free(gui.draw_buffer);
        }
        gui.draw_buffer = rg_alloc(pixels * 2, MEM_SLOW);
        gui.draw_buffer_size = pixels;
    }

    if (!gui.draw_buffer)
        RG_PANIC("Failed to allocate draw buffer!");

    if (fill_color != C_NONE)
    {
        for (size_t i = 0; i < pixels; ++i)
            gui.draw_buffer[i] = fill_color;
    }

    return gui.draw_buffer;
}

static int get_horizontal_position(int x_pos, int width)
{
    int type = (x_pos & 0xFF0000) | 0x8000;
    int offset = (x_pos & 0xFFFF) - 0x8000;
    if (type == RG_GUI_CENTER)
        return ((gui.screen_width - width) / 2) + offset;
    else if (type == RG_GUI_LEFT)
        return 0 + offset;
    else if (type == RG_GUI_RIGHT)
        return (gui.screen_width - width) - offset;
    else if (x_pos < 0)
        return x_pos + gui.screen_width;
    return x_pos;
}

static int get_vertical_position(int y_pos, int height)
{
    int type = (y_pos & 0xFF0000) | 0x8000;
    int offset = (y_pos & 0xFFFF) - 0x8000;
    if (type == RG_GUI_CENTER)
        return ((gui.screen_height - height) / 2) + offset;
    else if (type == RG_GUI_TOP)
        return 0 + offset;
    else if (type == RG_GUI_BOTTOM)
        return (gui.screen_height - height) - offset;
    else if (y_pos < 0)
        return y_pos + gui.screen_height;
    return y_pos;
}

static void gui_update_geometry(void)
{
    gui.screen_width = rg_display_get_width();
    gui.screen_height = rg_display_get_height();
    // FIXME: RG_SCREEN_SAFE_AREA being added on top of RG_SCREEN_VISIBLE_AREA might not be super intuitive
    //        because of how this is defined in config.h. It should be documented somewhere...
    gui.margins = (rg_margins_t)RG_SCREEN_SAFE_AREA;
    gui.draw_buffer = get_draw_buffer(gui.screen_width, 18, C_BLACK);
}

void rg_gui_init(void)
{
    gui_update_geometry();
    gui.show_clock = rg_settings_get_boolean(NS_GLOBAL, SETTING_CLOCK, false);
    if (!rg_gui_set_language_id(rg_settings_get_number(NS_GLOBAL, SETTING_LANGUAGE, RG_LANG_DEFAULT)))
        rg_gui_set_language_id(0);
    if (!rg_gui_set_font(rg_settings_get_number(NS_GLOBAL, SETTING_FONTTYPE, RG_FONT_DEFAULT)))
        rg_gui_set_font(0); //默认使用支持中文的字体
    rg_gui_set_theme(rg_settings_get_string(NS_GLOBAL, SETTING_THEME, NULL));
    gui.initialized = true;
}

void rg_gui_update_geometry(void)
{
    if (gui.initialized)
        gui_update_geometry();
}

bool rg_gui_set_language_id(int index)
{
    if (rg_localization_set_language_id(index))
    {
        rg_settings_set_number(NS_GLOBAL, SETTING_LANGUAGE, index);
        RG_LOGI("Language set to: %s (%d)", rg_localization_get_language_name(index), index);
        return true;
    }
    RG_LOGE("Invalid language id %d!", index);
    return false;
}

bool rg_gui_set_theme(const char *theme_name)
{
    char pathbuf[RG_PATH_MAX];
    cJSON *new_theme = NULL;

    // Cleanup the current theme
    cJSON_Delete(gui.theme_obj);
    gui.theme_obj = NULL;

    if (theme_name && theme_name[0])
    {
        snprintf(pathbuf, RG_PATH_MAX, "%s/%s/theme.json", RG_BASE_PATH_THEMES, theme_name);
        char *data;
        size_t data_len;
        if (rg_storage_read_file(pathbuf, (void **)&data, &data_len, 0))
        {
            new_theme = cJSON_Parse(data);
            if (!new_theme) // Parse failure, clean the markup and try again
                new_theme = cJSON_Parse(rg_json_fixup(data));
            free(data);
        }
        if (!new_theme)
            RG_LOGE("Failed to load theme JSON from '%s'!\n", pathbuf);
    }

    if (new_theme)
    {
        rg_settings_set_string(NS_GLOBAL, SETTING_THEME, theme_name);
        strcpy(gui.theme_name, theme_name);
        // FIXME: Keeping the theme around uses quite a lot of internal memory (about 3KB)...
        //        We should probably convert it to a regular array or hashmap.
        gui.theme_obj = new_theme;
        RG_LOGI("Theme set to '%s'!\n", theme_name);
    }
    else
    {
        rg_settings_set_string(NS_GLOBAL, SETTING_THEME, NULL);
        strcpy(gui.theme_name, "");
        gui.theme_obj = NULL;
        RG_LOGI("Using built-in theme!\n");
    }

    gui.style.box_background = rg_gui_get_theme_color("dialog", "background", C_BLACK);
    gui.style.box_header = rg_gui_get_theme_color("dialog", "header", C_BRIGHT_YELLOW);
    gui.style.box_border = rg_gui_get_theme_color("dialog", "border", C_OLIVE_YELLOW);
    gui.style.item_standard = rg_gui_get_theme_color("dialog", "item_standard", C_BRIGHT_YELLOW);
    gui.style.item_disabled = rg_gui_get_theme_color("dialog", "item_disabled", C_DARK_YELLOW);
    gui.style.item_message = rg_gui_get_theme_color("dialog", "item_message", C_BRIGHT_YELLOW);
    gui.style.scrollbar = rg_gui_get_theme_color("dialog", "scrollbar", C_BRIGHT_YELLOW);
    gui.style.shadow = rg_gui_get_theme_color("dialog", "shadow", C_NONE);

    return true;
}

rg_color_t rg_gui_get_theme_color(const char *section, const char *key, rg_color_t default_value)
{
    cJSON *root = section ? cJSON_GetObjectItem(gui.theme_obj, section) : gui.theme_obj;
    cJSON *obj = cJSON_GetObjectItem(root, key);
    if (cJSON_IsNumber(obj))
        return obj->valueint;
    char *strval = cJSON_GetStringValue(obj);
    if (!strval || strlen(strval) < 4)
        return default_value;
    if (strcmp(strval, "transparent") == 0)
        return C_TRANSPARENT;
    if (strcmp(strval, "none") == 0)
        return C_NONE;
    int intval = (int)strtol(strval, NULL, 0);
    // It is better to specify colors as RGB565 to avoid data loss, but we also accept RGB888 for convenience
    if (strlen(strval) == 8 && strval[0] == '0' && strval[1] == 'x')
        return (((intval >> 19) & 0x1F) << 11) | (((intval >> 10) & 0x3F) << 5) | (((intval >> 3) & 0x1F));
    return intval;
}

rg_image_t *rg_gui_get_theme_image(const char *name)
{
    char pathbuf[RG_PATH_MAX];
    if (!name || !gui.theme_name[0])
        return NULL;
    snprintf(pathbuf, RG_PATH_MAX, "%s/%s/%s", RG_BASE_PATH_THEMES, gui.theme_name, name);
    return rg_surface_load_image_file(pathbuf, 0);
}

const char *rg_gui_get_theme_name(void)
{
    return gui.theme_name[0] ? gui.theme_name : NULL;
}

bool rg_gui_set_font(int index)
{
    RG_LOGI("Setting font index: %d (RG_FONT_MAX=%d)\n", index, RG_FONT_MAX);
    if (index < 0 || index > RG_FONT_MAX - 1)
        return false;

    gui.font = fonts[index];
    gui.font_index = index;
    gui.font_height = gui.font->height; //使用字体本身高度

    rg_settings_set_number(NS_GLOBAL, SETTING_FONTTYPE, index);

    RG_LOGI("Font set to: %s (height=%d, scaling=%.2f)\n",
        gui.font->name, gui.font_height, (float)gui.font_height / gui.font->height);

    return true;
}

void rg_gui_set_surface(rg_surface_t *surface)
{
    gui.screen_buffer = surface ? surface->data : NULL;
}

rg_margins_t rg_gui_get_safe_area(void)
{
    return gui.margins;
}

void rg_gui_copy_buffer(int left, int top, int width, int height, int stride, const uint16_t *buffer, bool transparency)
{
    left = get_horizontal_position(left, width);
    top = get_vertical_position(top, height);
    width = RG_MIN(width, gui.screen_width - left);
    height = RG_MIN(height, gui.screen_height - top);

    if (width <= 0 || height <= 0)
        return;

    if (left >= gui.screen_width || top >= gui.screen_height)
    {
        RG_LOGD("Buffer (x: %d, y:%d) is entirely outside the screen!", left, top);
        return;
    }

    if (gui.screen_buffer)
    {
        if (stride < width)
            stride = width * 2;

        for (int y = 0; y < height; ++y)
        {
            uint16_t *dst = gui.screen_buffer + (top + y) * gui.screen_width + left;
            const uint16_t *src = (void *)buffer + y * stride;
            if (transparency)
            {
                for (int x = 0; x < width; ++x)
                    if (src[x] != C_TRANSPARENT)
                        dst[x] = src[x];
            }
            else
            {
                memcpy(dst, src, width * 2);
            }
        }
    }
    else
    {
        rg_display_write_rect(left, top, width, height, stride, buffer, 0);
    }
}

static size_t get_glyph(uint32_t *output, const rg_font_t *font, int points, int c)
{
    // Some glyphs are always zero width
    if (!font || c == '\r' || c == '\n' || c == 0) // || c < 8 || c > 0xFFFF)
        return 0;

    if (points <= 0)
        points = font->height;


    const uint8_t *ptr = font->data;
    const rg_font_glyph_t *glyph = (rg_font_glyph_t *)ptr;
    if(font->type == 2){

        if (c <= 255)
        {
            int times =0;
            while (glyph->code && glyph->code != c && times++ <=255)
            {
                if (glyph->width != 0)
                    ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;
                ptr += sizeof(rg_font_glyph_t);
                glyph = (rg_font_glyph_t *)ptr;
            }
        }else if(c >= font->map_start_code){
            uint32_t map_index =  c - font->map_start_code;
            if (map_index < font->map_len)
            {
                uint32_t data_index = font->map[map_index];
                glyph = (rg_font_glyph_t *)(ptr + data_index);
            }
        }
    }
    else
    {
        // for (size_t i = 0; i < font->chars && glyph->code && glyph->code != c; ++i)
        while (glyph->code && glyph->code != c)
        {
            if (glyph->width != 0)
                ptr += (((glyph->width * glyph->height) - 1) / 8) + 1;
            ptr += sizeof(rg_font_glyph_t);
            glyph = (rg_font_glyph_t *)ptr;
        }
    }

    if (glyph && glyph->code == c) // Glyph found
    {
        // Based on code by Boris Lovosevic (https://github.com/loboris)
        int yOffset = glyph->yOffset;
        int width = glyph->width;
        int height = glyph->height;
        int xOffset = glyph->xOffset < 0x80 ? glyph->xOffset : -(0xFF - glyph->xOffset);
        int xDelta = glyph->xDelta;
        const uint8_t *data = glyph->data;
        if (output)
        {
            memset(output, 0, points * 4);
            int ch = 0, mask = 0x80;
            for (int y = 0; y < height; y++)
            {
                uint32_t row = 0;
                for (int x = 0; x < width; x++)
                {
                    if (((x + (y * width)) % 8) == 0)
                    {
                        mask = 0x80;
                        ch = *data++;
                    }
                    if ((ch & mask) != 0)
                        row |= (1 << (xOffset + x));
                    mask >>= 1;
                }
                output[yOffset + y] = row;
            }
            // Vertical stretching
            if (points != font->height)
            {
                float scale = (float)points / font->height;
                for (int y = points - 1; y >= 0; y--)
                    output[y] = output[(int)(y / scale)];
            }
        }
        return RG_MAX(width, xDelta);
    }
    // else if (font != &font_basic8x8) // Glyph not found, try fallback font
    // {
    //     return get_glyph(output, &font_basic8x8, points, c);
    // }
    else // Glyph not found, no fallback
    {
        size_t box_width = font->width ?: 8;
        if (output) // draw missing box
        {
            uint32_t mask = ~((0xFFFFFFFF << (box_width - 1)) | 1);
            for (size_t i = 0; i < points; ++i)
                output[i] = (0xAAAAAAAA << (i & 1)) & mask;
        }
        return box_width;
    }
}

rg_rect_t rg_gui_draw_text(int x_pos, int y_pos, int width, const char *text, // const rg_font_t *font,
                           rg_color_t color_fg, rg_color_t color_bg, uint32_t flags)
{
    const rg_font_t *font = gui.font;
    int padding = (flags & RG_TEXT_NO_PADDING) ? 0 : 1;
    int font_height = (flags & RG_TEXT_BIGGER) ? gui.font_height * 2 : gui.font_height;
    int monospace = ((flags & RG_TEXT_MONOSPACE) || font->type == 0) ? font->width : 0;
    int line_height = font_height + padding * 2;
    int line_count = 0;
    bool transparency = color_fg == C_TRANSPARENT || color_bg == C_TRANSPARENT;
    // int16_t line_breaks[64], line_width_cache[64];

    if (!text || *text == 0)
        text = " ";

    if (width == 0)
    {
        // Find the longest line to determine our box width
        int line_width = padding * 2;
        for (const char *ptr = text; *ptr;)
        {
            int chr = rg_utf8_decode(&ptr);
            line_width += monospace ?: get_glyph(NULL, font, font_height, chr);

            if (chr == '\n' || *ptr == 0)
            {
                width = RG_MAX(line_width, width);
                line_width = padding * 2;
                line_count++;
            }
        }
    }

    x_pos = get_horizontal_position(x_pos, width);
    y_pos = get_vertical_position(y_pos, line_height);

    if (x_pos >= gui.screen_width || y_pos >= gui.screen_height)
    {
        RG_LOGD("Textbox (x: %d, y:%d) is entirely outside the screen!", x_pos, y_pos);
        return (rg_rect_t){x_pos, y_pos, 0, 0};
    }
    else if (x_pos + width > gui.screen_width || y_pos + line_height > gui.screen_height)
    {
        RG_LOGD("Textbox (pos: %dx%d, size: %dx%d) will be truncated!", x_pos, y_pos, width, line_height);
        // return;
    }

    int draw_width = RG_MIN(width, gui.screen_width - x_pos);
    int y_offset = 0;

    for (const char *ptr = text; *ptr;)
    {
        int x_offset = padding;

        if (flags & (RG_TEXT_ALIGN_RIGHT|RG_TEXT_ALIGN_CENTER))
        {
            // Find the current line's text width
            const char *line = ptr;
            while (x_offset < draw_width && *line && *line != '\n')
            {
                int chr = rg_utf8_decode(&line);
                int width = monospace ?: get_glyph(NULL, font, font_height, chr);
                if (draw_width - x_offset < width) // Do not truncate glyphs
                    break;
                x_offset += width;
            }
            if (flags & RG_TEXT_ALIGN_CENTER)
                x_offset = (draw_width - x_offset) / 2;
            else if (flags & RG_TEXT_ALIGN_RIGHT)
                x_offset = draw_width - x_offset;
        }

        uint16_t *draw_buffer = NULL;

        if (!(flags & RG_TEXT_DUMMY_DRAW))
            draw_buffer = get_draw_buffer(draw_width, line_height, color_bg);

        while (x_offset < draw_width)
        {
            uint32_t bitmap[font_height];
            const char *prev_ptr = ptr;
            int glyph_width = get_glyph(bitmap, font, font_height, rg_utf8_decode(&ptr));
            int width = monospace ?: glyph_width;

            if (draw_width - x_offset < width) // Do not truncate glyphs
            {
                if (flags & RG_TEXT_MULTILINE)
                    ptr = prev_ptr;
                break;
            }

            if (!(flags & RG_TEXT_DUMMY_DRAW))
            {
                for (int y = 0; y < font_height; y++)
                {
                    uint32_t row = bitmap[y];
                    if (row != 0) // get_draw_buffer fills the bg color, nothing to do if row empty
                    {
                        uint16_t *output = &draw_buffer[(draw_width * (y + padding)) + x_offset];
                        for (int x = 0; x < width; x++)
                            output[x] = ((row >> x) & 1) ? color_fg : color_bg;
                    }
                }
            }

            x_offset += width;

            if (*ptr == 0 || *ptr == '\n')
                break;
        }

        if (!(flags & RG_TEXT_DUMMY_DRAW))
            rg_gui_copy_buffer(x_pos, y_pos + y_offset, draw_width, line_height, 0, draw_buffer, transparency);

        y_offset += line_height;

        if (!(flags & RG_TEXT_MULTILINE))
            break;
    }

    return (rg_rect_t){x_pos, y_pos, draw_width, y_offset};
}

void rg_gui_draw_rect(int x_pos, int y_pos, int width, int height, int border_size,
                      rg_color_t border_color, rg_color_t fill_color)
{
    if (width <= 0 || height <= 0)
        return;

    x_pos = get_horizontal_position(x_pos, width);
    y_pos = get_vertical_position(y_pos, height);

    if (border_size > 0)
    {
        uint16_t *draw_buffer = get_draw_buffer(border_size, RG_MAX(width, height), border_color);
        bool transparency = border_color == C_TRANSPARENT;

        rg_gui_copy_buffer(x_pos, y_pos, width, border_size, 0, draw_buffer, transparency);                        // Top
        rg_gui_copy_buffer(x_pos, y_pos + height - border_size, width, border_size, 0, draw_buffer, transparency); // Bottom
        rg_gui_copy_buffer(x_pos, y_pos, border_size, height, 0, draw_buffer, transparency);                       // Left
        rg_gui_copy_buffer(x_pos + width - border_size, y_pos, border_size, height, 0, draw_buffer, transparency); // Right

        x_pos += border_size;
        y_pos += border_size;
        width -= border_size * 2;
        height -= border_size * 2;
    }

    if (width > 0 && height > 0 && fill_color != C_NONE)
    {
        uint16_t *draw_buffer = get_draw_buffer(width, RG_MIN(height, 16), fill_color);
        bool transparency = fill_color == C_TRANSPARENT;
        for (int y = 0; y < height; y += 16)
            rg_gui_copy_buffer(x_pos, y_pos + y, width, RG_MIN(height - y, 16), 0, draw_buffer, transparency);
    }
}

void rg_gui_draw_image(int x_pos, int y_pos, int width, int height, bool resample, const rg_image_t *img)
{
    if (img && resample && (width && height) && (width != img->width || height != img->height))
    {
        rg_image_t *new_img = rg_surface_resize(img, width, height);
        rg_gui_copy_buffer(x_pos, y_pos, width, height, new_img->width * 2, new_img->data, true);
        rg_surface_free(new_img);
    }
    else if (img)
    {
        int draw_width = width ? RG_MIN(width, img->width) : img->width;
        int draw_height = height ? RG_MIN(height, img->height) : img->height;
        rg_gui_copy_buffer(x_pos, y_pos, draw_width, draw_height, img->width * 2, img->data, true);
    }
    else // We fill a rect to show something is missing instead of abort...
    {
        rg_gui_draw_rect(x_pos, y_pos, width, height, 2, C_RED, C_BLACK);
        // rg_gui_draw_text(x_pos + 2, y_pos + 2, width - 4, "No image", C_DIM_GRAY, C_BLACK, 0);
    }
}

void rg_gui_draw_icons(void)
{
    rg_battery_t battery = rg_input_read_battery();
    // rg_network_t network = rg_network_get_info();
    rg_rect_t txt = TEXT_RECT("00:00", 0);
    int bar_height = txt.height;
    int icon_height = 0.75f * bar_height;
    int icon_top = RG_MAX(0, (bar_height - icon_height) / 2);
    int right = gui.margins.right;

    if (battery.present)
    {
        char battery_percent[8];
        snprintf(battery_percent, sizeof(battery_percent), "%d%%", (int)battery.level);
        rg_rect_t percent_rect = TEXT_RECT(battery_percent, 0);
        int percent_width = percent_rect.width;
        int percent_x = -(right + percent_width);
        int percent_y = 0;
        //百分比文字
        rg_gui_draw_text(percent_x, percent_y, 0, battery_percent, C_SILVER,
                        gui.screen_buffer ? C_TRANSPARENT : C_BLACK, 0);

        right += (percent_width + 4);

        //电池身体
        int border = 1;
        int body_width = 1.25f * bar_height;
        int body_height = icon_height;
        int body_padding = 0.1f * bar_height;
        //电量
        int bat_width = (body_width - 2*border - 2*body_padding) / 100.f * battery.level;
        int bat_height = body_height - 2*border - 2*body_padding;
        //电池头
        int heat_width = 2;
        int heat_height = 4;

        int x_pos = -(body_width + 2 + heat_width + right);
        int y_pos = icon_top;
        int bat_x = x_pos + border + body_padding;
        int bat_y = y_pos + border + body_padding;
        int heat_x = x_pos + body_width + body_padding;
        int heat_y = y_pos + (icon_height - heat_height) / 2;

        #ifdef RG_BATTERY_CHARGE_GPIO
        rg_color_t color_fill = battery.charging ? C_FOREST_GREEN : (battery.level > 15 ? (battery.level > 25 ? C_SILVER : C_ORANGE) : C_RED);
        #else
        rg_color_t color_fill = (battery.level > 15 ? (battery.level > 25 ? C_FOREST_GREEN : C_ORANGE) : C_RED);
        #endif
        rg_color_t color_border = C_SILVER;
        rg_color_t color_empty = C_BLACK;

        rg_gui_draw_rect(x_pos, y_pos, body_width, body_height, border, color_border, C_NONE); //电池身体
        rg_gui_draw_rect(x_pos + border, y_pos + border, body_width - 2*border, body_height - 2*border, body_padding, C_BLACK, C_NONE); //电池内边距
        rg_gui_draw_rect(heat_x, heat_y, heat_width, heat_height, 0, 0, color_border); //电池头
        rg_gui_draw_rect(bat_x, bat_y, bat_width, bat_height, 0, 0, color_fill); //电量
        rg_gui_draw_rect(x_pos + border + bat_width, bat_y, body_width-bat_width-2*border-2*body_padding, bat_height, 0, 0, color_empty); //空电量

        right += x_pos;
    }

    // if (network.state > RG_NETWORK_DISCONNECTED)
    // {
    //     right += 22;

    //     int width = 16;
    //     int height = icon_height;
    //     int seg_width = (width - 2 - 2) / 3;
    //     int seg_height = height / 3;
    //     int x_pos = -right;
    //     int y_pos = icon_top + height;

    //     rg_color_t color_fill = (network.state == RG_NETWORK_CONNECTED) ? C_GREEN : C_NONE;
    //     rg_color_t color_border = (network.state == RG_NETWORK_CONNECTED) ? C_SILVER : C_DIM_GRAY;

    //     rg_gui_draw_rect(x_pos, y_pos - seg_height * 1, seg_width, seg_height * 1, 1, color_border, color_fill);
    //     x_pos += seg_width + 2;
    //     rg_gui_draw_rect(x_pos, y_pos - seg_height * 2, seg_width, seg_height * 2, 1, color_border, color_fill);
    //     x_pos += seg_width + 2;
    //     rg_gui_draw_rect(x_pos, y_pos - seg_height * 3, seg_width, seg_height * 3, 1, color_border, color_fill);
    // }

    // if (gui.show_clock)
    // {
    //     right += txt.width + 4;

    //     int x_pos = -right;
    //     int y_pos = 0;
    //     char buffer[12];
    //     time_t time_sec = time(NULL);
    //     struct tm *time = localtime(&time_sec);

    //     sprintf(buffer, "%02d:%02d", time->tm_hour, time->tm_min);
    //     rg_gui_draw_text(x_pos, y_pos, 0, buffer, C_SILVER, gui.screen_buffer ? C_TRANSPARENT : C_BLACK, 0);
    // }
}

void rg_gui_draw_hourglass(void)
{
    rg_display_write_rect(
        get_horizontal_position(RG_GUI_CENTER, image_hourglass.width),
        get_vertical_position(RG_GUI_CENTER, image_hourglass.height),
        image_hourglass.width,
        image_hourglass.height,
        image_hourglass.width * 2,
        (uint16_t*)image_hourglass.pixel_data, 0);
}

void rg_gui_draw_status_bars(void)
{
    size_t max_len = gui.screen_width / 8;
    char header[max_len];
    char footer[max_len];

    const rg_app_t *app = rg_system_get_app();
    rg_stats_t stats = rg_system_get_stats();

    if (!app->initialized || app->isLauncher)
        return;

    snprintf(header, max_len, "SPEED: %d%% (%d/%d) / BUSY: %d%%",
        (int)roundf(stats.speedPercent),
        (int)roundf(stats.totalFPS),
        (int)app->frameskip,
        (int)roundf(stats.busyPercent));

    if (app->romPath && strlen(app->romPath) > max_len - 1)
        snprintf(footer, max_len, "...%s", app->romPath + (strlen(app->romPath) - (max_len - 4)));
    else if (app->romPath)
        snprintf(footer, max_len, "%s", app->romPath);
    else
        snprintf(footer, max_len, "Retro-Go %s", app->version);

    // FIXME: Respect gui.margins (draw black background full screen_width, but pad the text if needed)
    rg_gui_draw_text(0, RG_GUI_TOP, gui.screen_width, header, C_WHITE, C_BLACK, 0);
    rg_gui_draw_text(0, RG_GUI_BOTTOM, gui.screen_width, footer, C_WHITE, C_BLACK, 0);

    rg_gui_draw_icons();
}

static size_t get_dialog_items_count(const rg_gui_option_t *options)
{
    if (!options)
        return 0;

    const rg_gui_option_t *opt = options;
    while (opt->arg || opt->label || opt->value || opt->flags || opt->update_cb)
        opt++;
    return opt - options;
}

rg_rect_t rg_gui_draw_dialog(const char *title, const rg_gui_option_t *options, size_t options_count, int sel) // const rg_rect_t *rect,
{
    RG_ASSERT_ARG(options || options_count == 0);

    const int sep_width = TEXT_RECT(": ", 0).width;
    const int font_height = gui.font_height;
    const int max_box_width = 0.86f * gui.screen_width;
    const int max_box_height = 0.86f * gui.screen_height;
    const int box_padding = 0.5 * font_height;
    const int row_padding_y = 0; // now handled by draw_text
    const int row_padding_x = 8;
    const int max_inner_width = max_box_width - sep_width - (row_padding_x + box_padding) * 2;

    int box_x, box_y;
    int box_width = box_padding * 2;
    int box_height = box_padding * 2 + (title ? font_height + 6 : 0);
    int inner_width = TEXT_RECT(title, 0).width;
    int col1_width = -1;
    int col2_width = -1;
    uint8_t row_height[options_count];

    // FIXME: The information built in this loop should be cached between calls to rg_gui_draw_dialog...
    // It doesn't matter for most dialogs but the file picker with 500+ files wastes a LOT of time here.
    for (size_t i = 0; i < options_count; i++)
    {
        if (options[i].flags == RG_DIALOG_FLAG_HIDDEN)
        {
            row_height[i] = 0;
            continue;
        }

        rg_rect_t label = TEXT_RECT(options[i].label, max_inner_width);
        rg_rect_t value = {0};

        inner_width = RG_MAX(inner_width, label.width);

        if (options[i].value)
        {
            value = TEXT_RECT(options[i].value, max_inner_width - label.width);
            col1_width = RG_MAX(col1_width, label.width);
            col2_width = RG_MAX(col2_width, value.width);
        }

        row_height[i] = RG_MAX(label.height, value.height) + row_padding_y * 2;
        box_height += row_height[i];
    }

    col1_width = RG_MIN(col1_width, max_box_width);
    col2_width = RG_MIN(col2_width, max_box_width);

    if (col2_width >= 0)
        inner_width = RG_MAX(inner_width, col1_width + col2_width + sep_width);

    inner_width = RG_MIN(inner_width, max_box_width);
    col2_width = inner_width - col1_width - sep_width;
    box_width += inner_width + row_padding_x * 2;
    box_height = RG_MIN(box_height, max_box_height);
    box_x = (gui.screen_width - box_width) / 2;
    box_y = (gui.screen_height - box_height) / 2;

    int x = box_x + box_padding;
    int y = box_y + box_padding;

    if (title)
    {
        int width = inner_width + row_padding_x * 2;
        rg_gui_draw_text(x, y, width, title, gui.style.box_header, gui.style.box_background, RG_TEXT_ALIGN_CENTER);
        rg_gui_draw_rect(x, y + font_height, width, 6, 0, 0, gui.style.box_background);
        y += font_height + 6;
    }

    int list_top_i = 0;
    int list_end_i = 0;

    // Find top of page that contains selection
    for (int yy = y, i = 0; i <= sel && i < options_count; i++)
    {
        yy += row_height[i];
        if (yy >= box_y + box_height)
        {
            if (sel < i)
                break;
            yy = y + row_height[i];
            list_top_i = i;
        }
    }

    for (int i = list_top_i; i < options_count; i++)
    {
        uint16_t color, fg, bg;
        int xx = x + row_padding_x;
        int yy = y + row_padding_y;
        int height = 8;

        if (options[i].flags == RG_DIALOG_FLAG_NORMAL)
            color = gui.style.item_standard;
        else if (options[i].flags == RG_DIALOG_FLAG_MESSAGE)
            color = gui.style.item_message;
        else
            color = gui.style.item_disabled;

        bool highlight = (options[i].flags & RG_DIALOG_FLAG_MODE_MASK) != RG_DIALOG_FLAG_SKIP && i == sel;
        fg = highlight ? gui.style.box_background : color;
        bg = highlight ? color : gui.style.box_background;

        if (y + row_height[i] >= box_y + box_height)
            break;

        list_end_i = i;

        if (options[i].flags == RG_DIALOG_FLAG_HIDDEN)
            continue;

        if (false && options[i].flags == RG_DIALOG_FLAG_SEPARATOR)
        {
            // FIXME: Draw a nice dim line...
        }
        else if (options[i].value)
        {
            rg_gui_draw_text(xx, yy, col1_width, options[i].label, fg, bg, 0);
            rg_gui_draw_text(xx + col1_width, yy, sep_width, ": ", fg, bg, 0);
            height = rg_gui_draw_text(xx + col1_width + sep_width, yy, col2_width, options[i].value, fg, bg, RG_TEXT_MULTILINE).height;
            if ((height / font_height) >= 2) // Multiline value, must fill sep and label
                rg_gui_draw_rect(xx, yy + font_height + 1, inner_width - col2_width, height - font_height, 0, 0, bg);
        }
        else
        {
            height = rg_gui_draw_text(xx, yy, inner_width, options[i].label, fg, bg, RG_TEXT_MULTILINE).height;
        }

        rg_gui_draw_rect(x, yy, row_padding_x, height, 0, 0, bg);
        rg_gui_draw_rect(xx + inner_width, yy, row_padding_x, height, 0, 0, bg);
        rg_gui_draw_rect(x, y, inner_width + row_padding_x * 2, row_padding_y, 0, 0, bg);
        rg_gui_draw_rect(x, yy + height, inner_width + row_padding_x * 2, row_padding_y, 0, 0, bg);

        y += height + row_padding_y * 2;
    }

    if (y < (box_y + box_height))
    {
        rg_gui_draw_rect(box_x, y, box_width, (box_y + box_height) - y, 0, 0, gui.style.box_background);
    }

    // Draw box border
    rg_gui_draw_rect(box_x, box_y, box_width, box_height, box_padding, gui.style.box_background, C_NONE);
    rg_gui_draw_rect(box_x - 1, box_y - 1, box_width + 2, box_height + 2, 1, gui.style.box_border, C_NONE);

    // Draw box shadow
    // rg_gui_draw_rect(box_x + box_width + 4, box_y + box_height, box_width, 4, 0, C_NONE, gui.style.shadow); // Bottom
    // rg_gui_draw_rect(box_x + box_width, box_y + 4, 4, box_height, 0, C_NONE, gui.style.shadow); // left

    // Basic scroll indicators are overlayed at the end...
    if (list_top_i > 0)
    {
        int x = box_x + box_width - 10;
        int y = box_y + box_padding + 2;
        rg_gui_draw_rect(x + 0, y - 0, 6, 2, 0, 0, gui.style.scrollbar);
        rg_gui_draw_rect(x + 1, y - 2, 4, 2, 0, 0, gui.style.scrollbar);
        rg_gui_draw_rect(x + 2, y - 4, 2, 2, 0, 0, gui.style.scrollbar);
    }
    if (list_end_i + 1 < options_count)
    {
        int x = box_x + box_width - 10;
        int y = box_y + box_height - 6;
        rg_gui_draw_rect(x + 0, y - 4, 6, 2, 0, 0, gui.style.scrollbar);
        rg_gui_draw_rect(x + 1, y - 2, 4, 2, 0, 0, gui.style.scrollbar);
        rg_gui_draw_rect(x + 2, y - 0, 2, 2, 0, 0, gui.style.scrollbar);
    }

    return (rg_rect_t){box_x, box_y, box_width, box_height};
}

rg_rect_t rg_gui_draw_message(const char *format, ...) // const rg_rect_t *rect,
{
    RG_ASSERT_ARG(format);

    char buffer[512];
    va_list va;
    va_start(va, format);
    vsnprintf(buffer, sizeof(buffer), format, va);
    va_end(va);
    const rg_gui_option_t options[] = {
        {0, buffer, NULL, RG_DIALOG_FLAG_MESSAGE, NULL},
        RG_DIALOG_END,
    };
    // FIXME: Should rg_display_force_redraw() be called? Before? After? Both?
    return rg_gui_draw_dialog(NULL, options, 1, 0);
}

intptr_t rg_gui_dialog(const char *title, const rg_gui_option_t *options_const, int selected_index)
{
    rg_gui_option_t *options = (rg_gui_option_t *)options_const;
    size_t options_count = get_dialog_items_count(options_const);

    // In many cases we must create a copy of the options array because it can be mutated by the callbacks
    // (typically option->value and option->flags). No callback in the array = no way of it being mutable.
    // The entire text_buffer system is very brittle and prone to corruption. We get away with it for now
    // because most values are less than our assumed 32 bytes...
    size_t shadow_options_count = 0;
    size_t shadow_text_buffer_size = 0;
    for (size_t i = 0; i < options_count; i++)
    {
        if (options_const[i].update_cb)
            shadow_options_count = options_count;
        // if (options_const[i].value)
        //     shadow_text_buffer_size += strlen(options_const[i].value) + 1;
    }
    rg_gui_option_t shadow_options[shadow_options_count + 1];
    char *shadow_text_buffer = NULL;
    if (shadow_options_count > 0)
    {
        options = memcpy(shadow_options, options_const, sizeof(shadow_options));
        shadow_text_buffer_size = RG_MAX(options_count * 32, 1024);
        shadow_text_buffer = malloc(shadow_text_buffer_size);
        char *text_buffer_ptr = shadow_text_buffer;
        for (size_t i = 0; i < shadow_options_count; i++)
        {
            rg_gui_option_t *option = &shadow_options[i];
            if (!text_buffer_ptr || !option->value || !option->update_cb)
                continue;
            option->value = strcpy(text_buffer_ptr, option->value);
            option->update_cb(option, RG_DIALOG_INIT);
            text_buffer_ptr += RG_MAX(strlen(text_buffer_ptr), 31) + 1;
        }
    }

    if (selected_index < 0)
        selected_index += options_count;

    rg_gui_event_t event = RG_DIALOG_VOID;
    uint32_t joystick = 0, joystick_old;
    uint64_t joystick_last = 0;
    bool redraw = false;
    int sel = RG_MIN(RG_MAX(0, selected_index), options_count - 1);
    int sel_old = -1;

    rg_gui_draw_status_bars();
    rg_gui_draw_dialog(title, options, options_count, sel);
    rg_input_wait_for_key(RG_KEY_ALL, false, 1000);
    rg_task_delay(80);

    while (event != RG_DIALOG_SELECT && event != RG_DIALOG_CANCEL)
    {
        // TO DO: Add acceleration!
        joystick_old = ((rg_system_timer() - joystick_last) > 300000) ? 0 : joystick;
        joystick = rg_input_read_gamepad();
        event = RG_DIALOG_VOID;

        if (joystick ^ joystick_old)
        {
            bool active_selection = options_count && options[sel].flags == RG_DIALOG_FLAG_NORMAL;
            rg_gui_callback_t callback = active_selection ? options[sel].update_cb : NULL;

            if (joystick & RG_KEY_UP) {
                if (--sel < 0) sel = options_count - 1;
            }
            else if (joystick & RG_KEY_DOWN) {
                if (++sel > options_count - 1) sel = 0;
            }
            else if (joystick & (RG_KEY_B|RG_KEY_OPTION|RG_KEY_MENU)) {
                event = RG_DIALOG_CANCEL;
            }
            else if (joystick & RG_KEY_LEFT && callback) {
                event = callback(&options[sel], RG_DIALOG_PREV);
                redraw = true;
            }
            else if (joystick & RG_KEY_RIGHT && callback) {
                event = callback(&options[sel], RG_DIALOG_NEXT);
                redraw = true;
            }
            else if (joystick & RG_KEY_A && callback) {
                event = callback(&options[sel], RG_DIALOG_ENTER);
                redraw = true;
            }
            else if (joystick & RG_KEY_A && active_selection) {
                event = RG_DIALOG_SELECT;
            }

            joystick_last = rg_system_timer();
        }

        if (sel_old != sel)
        {
            for (size_t i = 0; i < options_count; ++i)
            {
                // If the item is selectable, we stop here
                if (options[sel].flags == RG_DIALOG_FLAG_NORMAL)
                    break;
                if (options[sel].flags == RG_DIALOG_FLAG_DISABLED)
                    break;

                // Otherwise move to the next
                sel += (joystick == RG_KEY_UP) ? -1 : 1;

                if (sel < 0)
                    sel = options_count - 1;

                if (sel >= options_count)
                    sel = 0;
            }
            if (sel_old != -1 && options[sel_old].update_cb)
                options[sel_old].update_cb(&options[sel_old], RG_DIALOG_FOCUS_LOST);
            if (options[sel].update_cb)
                options[sel].update_cb(&options[sel], RG_DIALOG_FOCUS_GAINED);
            redraw = true;
            sel_old = sel;
        }

        if (event == RG_DIALOG_REDRAW || event == RG_DIALOG_UPDATE)
        {
            for (size_t i = 0; i < options_count; i++)
            {
                if (options[i].update_cb)
                    options[i].update_cb(&options[i], RG_DIALOG_UPDATE);
            }
            if (event == RG_DIALOG_REDRAW)
            {
                rg_display_force_redraw();
                rg_gui_draw_status_bars();
            }
            redraw = true;
        }

        if (redraw)
        {
            rg_gui_draw_dialog(title, options, options_count, sel);
            redraw = false;
        }

        rg_task_delay(20);
        rg_system_tick(0);
    }

    rg_input_wait_for_key(joystick, false, 1000);
    rg_display_force_redraw();
    // free(shadow_options);
    free(shadow_text_buffer);

    if (event == RG_DIALOG_CANCEL || sel < 0)
        return RG_DIALOG_CANCELLED;

    return options[sel].arg;
}

bool rg_gui_confirm(const char *title, const char *message, bool default_yes)
{
    const rg_gui_option_t options[] = {
        {0, message,  NULL, RG_DIALOG_FLAG_MESSAGE, NULL},
        {0, "",       NULL, RG_DIALOG_FLAG_MESSAGE, NULL},
        {1, _("Yes"), NULL, RG_DIALOG_FLAG_NORMAL,  NULL},
        {0, _("No"),  NULL, RG_DIALOG_FLAG_NORMAL,  NULL},
        RG_DIALOG_END,
    };
    return rg_gui_dialog(title, message ? options : options + 1, default_yes ? -2 : -1) == 1;
}

void rg_gui_alert(const char *title, const char *message)
{
    const rg_gui_option_t options[] = {
        {0, message, NULL, RG_DIALOG_FLAG_MESSAGE, NULL},
        {0, "",      NULL, RG_DIALOG_FLAG_MESSAGE, NULL},
        {1, _("OK"), NULL, RG_DIALOG_FLAG_NORMAL,  NULL},
        RG_DIALOG_END,
    };
    rg_gui_dialog(title, message ? options : options + 1, -1);
}

void rg_gui_error(const char *message, const char *object, int _errno, bool exit_program)
{
    const char *title = exit_program ? _("Fatal Error") : _("Error");
    const char *button = exit_program ? _("Exit") : _("OK");
    char error_msg[128] = "";

    const rg_gui_option_t options[] = {
        {0, message,  NULL,           RG_DIALOG_FLAG_NORMAL,                                   NULL},
        {0, _("Obj"), (char *)object, object ? RG_DIALOG_FLAG_MESSAGE : RG_DIALOG_FLAG_HIDDEN, NULL},
        {0, _("Err"), error_msg,      _errno ? RG_DIALOG_FLAG_MESSAGE : RG_DIALOG_FLAG_HIDDEN, NULL},
        {0, "",       NULL,           RG_DIALOG_FLAG_MESSAGE,                                  NULL},
        {1, button,   NULL,           RG_DIALOG_FLAG_NORMAL,                                   NULL},
        RG_DIALOG_END,
    };

    esp_err_to_name_r(_errno, error_msg, sizeof(error_msg));

    // FIXME: show blue backdrop color and hide bars?
    rg_gui_dialog(title, options, -1);
    if (exit_program)
        rg_system_exit();
}

typedef struct
{
    rg_gui_option_t *options;
    size_t count;
    rg_bucket_t *filenames;
    bool (*validator)(const char *path);
} file_picker_opts_t;

static int file_picker_cb(const rg_scandir_t *entry, void *arg)
{
    file_picker_opts_t *f = arg;
    if (f->validator && !(f->validator)(entry->path))
        return RG_SCANDIR_SKIP;
    rg_gui_option_t *options = realloc(f->options, (f->count + 2) * sizeof(rg_gui_option_t));
    if (!options)
        return RG_SCANDIR_STOP;
    char *name = rg_bucket_insert(f->filenames, entry->basename, strlen(entry->basename) + 1);
    f->options = options;
    f->options[f->count++] = (rg_gui_option_t){(intptr_t)name, name, NULL, RG_DIALOG_FLAG_NORMAL, NULL};
    return RG_SCANDIR_CONTINUE;
}

char *rg_gui_file_picker(const char *title, const char *path, bool (*validator)(const char *path), bool browse_tree, bool none_option)
{
    file_picker_opts_t options = {
        .options = calloc(8, sizeof(rg_gui_option_t)),
        .count = 0,
        .filenames = rg_bucket_create(4096),
        .validator = validator,
    };
    char *filepath = NULL;

    if (!title)
        title = _("Select file");

    if (none_option)
        options.options[options.count++] = (rg_gui_option_t){0, _("<None>"), NULL, RG_DIALOG_FLAG_NORMAL, NULL};

    // if (browse_tree)
    //     options.options[options.count++] = (rg_gui_option_t){0, "...", NULL, RG_DIALOG_FLAG_NORMAL, NULL};

    if (!rg_storage_scandir(path, file_picker_cb, &options, 0) || options.count < 1)
    {
        rg_gui_alert(title, _("Folder is empty."));
        goto cleanup;
    }
    options.options[options.count] = (rg_gui_option_t)RG_DIALOG_END;

    char *filename = (char *)rg_gui_dialog(title, options.options, 0);
    if (filename != (void *)RG_DIALOG_CANCELLED)
    {
        char buffer[RG_PATH_MAX] = "";
        if (filename)
            snprintf(buffer, RG_PATH_MAX, "%s/%s", path, filename);
        filepath = strdup(buffer);
        // if (browse_tree && rg_storage_stat(filepath).is_dir)
        // {
        // }
    }

cleanup:
    rg_bucket_free(options.filenames);
    free(options.options);
    return filepath;
}

void rg_gui_draw_input_screen(const char *title, const char *message, const char *input_buffer,
                              const rg_keyboard_layout_t *current_layout, int cursor_pos, bool partial_redraw)
{
    const int key_width = gui.screen_width / 10 - 4;
    const int key_height = 20;
    const int keyboard_width = current_layout->columns * key_width;
    const int keyboard_height = current_layout->rows * key_height;
    const int keyboard_x = (gui.screen_width - keyboard_width) / 2;
    const int keyboard_y = gui.screen_height - keyboard_height - 40;
    const int input_box_height = 30;
    const int input_box_y = keyboard_y - input_box_height - 10;
    char text_buffer[200];

    if (!input_buffer)
        input_buffer = "";

    if (!partial_redraw)
    {
        // Clear background using same method as rg_gui_draw_dialog
        rg_gui_draw_rect(0, 0, gui.screen_width, gui.screen_height, 0, C_NONE, gui.style.box_background);

        // Draw title similar to dialog title
        if (title)
            rg_gui_draw_text(0, 10, gui.screen_width, title, gui.style.box_header, gui.style.box_background, RG_TEXT_ALIGN_CENTER);

        // Draw message similar to dialog message
        if (message)
            rg_gui_draw_text(0, title ? 35 : 10, gui.screen_width, message, gui.style.item_message, gui.style.box_background, RG_TEXT_ALIGN_CENTER);

        // Draw input box with same styling as dialog
        rg_gui_draw_rect(keyboard_x, input_box_y, keyboard_width, input_box_height, 2, gui.style.box_border, C_WHITE);

        // Draw instructions at bottom like dialog
        snprintf(text_buffer, sizeof(text_buffer), "A=Type  B=Backspace  SELECT=%3s  START=OK  MENU/OPT=Cancel", current_layout->label);
        rg_gui_draw_text(0, gui.screen_height - 15, gui.screen_width, text_buffer, gui.style.item_message, gui.style.box_background, RG_TEXT_ALIGN_CENTER);
    }

    // Draw input buffer text and blinking cursor
    // static uint32_t blink_timer = 0;
    static bool show_cursor = true;
    snprintf(text_buffer, sizeof(text_buffer), "%s%s", input_buffer, show_cursor ? "_" : " ");
    rg_gui_draw_text(keyboard_x + 5, input_box_y + 5, keyboard_width - 10, text_buffer, C_BLACK, C_WHITE, 0);

    // Draw keyboard pad
    rg_gui_draw_virtual_keyboard(keyboard_x, keyboard_y, current_layout, cursor_pos, true);
}

void rg_gui_draw_virtual_keyboard(int x_pos, int y_pos, const rg_keyboard_layout_t *current_layout, int cursor_pos, bool partial_redraw)
{
    const int key_width = gui.screen_width / 10 - 4;
    const int key_height = 20;
    const int keyboard_width = current_layout->columns * key_width;
    const int keyboard_height = current_layout->rows * key_height;
    const int keyboard_x = get_horizontal_position(x_pos, keyboard_width);
    const int keyboard_y = get_vertical_position(y_pos, keyboard_height);
    const char *layout_ptr = current_layout->layout;

    if (!partial_redraw)
    {
        // Draw keyboard container with same border style as dialog
        rg_gui_draw_rect(keyboard_x - 2, keyboard_y - 2, keyboard_width + 4, keyboard_height + 4, 2,
            gui.style.box_border, gui.style.box_background);
    }

    // Draw keyboard keys
    for (int row = 0; row < current_layout->rows; row++)
    {
        for (int col = 0; col < current_layout->columns; col++)
        {
            int key_idx = row * current_layout->columns + col;
            int x = keyboard_x + col * key_width;
            int y = keyboard_y + row * key_height;

            bool is_selected = (cursor_pos == key_idx);
            // Use same color scheme as dialog items
            rg_color_t bg_color = is_selected ? gui.style.item_standard : gui.style.box_background;
            rg_color_t fg_color = is_selected ? gui.style.box_background : gui.style.item_standard;
            rg_color_t border_color = is_selected ? gui.style.item_standard : gui.style.box_border;

            // Draw key with same border style as dialog
            rg_gui_draw_rect(x + 1, y + 1, key_width - 2, key_height - 2, 1, border_color, bg_color);

            // Draw key character
            char key_str[5] = {0, 0, 0, 0, 0};
            int key = rg_utf8_decode(&layout_ptr);
            if (key == ' ')
                strcpy(key_str, "SP");
            else
                rg_utf8_encode(key_str, key);

            rg_gui_draw_text(x + 2, y + 2, key_width - 4, key_str, fg_color, bg_color, RG_TEXT_ALIGN_CENTER);
        }
    }
}

static const rg_keyboard_layout_t keyboard_layouts[] = {
    // Lowercase letters
    {
        .layout = "1234567890"
                    "qwertyuiop"
                    "asdfghjkl "
                    "zxcvbnm.,?",
        .columns = 10,
        .rows = 4,
        .label = "ABC",
    },
    // Uppercase letters
    {
        .layout = "1234567890"
                    "QWERTYUIOP"
                    "ASDFGHJKL "
                    "ZXCVBNM.,?",
        .columns = 10,
        .rows = 4,
        .label = "abc",
    },
    // Symbols
    {
        .layout = "!@#$%^&*()"
                    "[]{}|\\:;\"'"
                    "<>?/+=_-~ "
                    "1234567890",
        .columns = 10,
        .rows = 4,
        .label = "!@#",
    }
};

// TODO: Abstract all the redundant/similar code between rg_gui_input_str and rg_gui_input_char

int rg_gui_input_char(const rg_keyboard_layout_t *map)
{
    if (!map)
        map = &keyboard_layouts[0];

    int cursor = -1;
    int count = map->columns * map->rows;

    rg_input_wait_for_key(RG_KEY_ALL, false, 1000);

    while (1)
    {
        uint32_t joystick = rg_input_read_gamepad();
        int prev_cursor = cursor;

        if (joystick & RG_KEY_A)
            return map->layout[cursor];
        if (joystick & RG_KEY_B)
            break;

        if (joystick & RG_KEY_LEFT)
            cursor--;
        if (joystick & RG_KEY_RIGHT)
            cursor++;
        if (joystick & RG_KEY_UP)
            cursor -= map->columns;
        if (joystick & RG_KEY_DOWN)
            cursor += map->columns;

        if (cursor > count - 1)
            cursor = prev_cursor;
        else if (cursor < 0)
            cursor = prev_cursor;

        cursor = RG_MIN(RG_MAX(cursor, 0), count - 1);

        if (cursor != prev_cursor)
            rg_gui_draw_virtual_keyboard(RG_GUI_CENTER, RG_GUI_BOTTOM, map, cursor, false);

        rg_input_wait_for_key(RG_KEY_ALL, false, 500);
        rg_input_wait_for_key(RG_KEY_ANY, true, 500);

        rg_system_tick(0);
    }

    return -1;
}

char *rg_gui_input_str(const char *title, const char *message, const char *default_value)
{
    // Virtual keyboard implementation for Wi-Fi credential input
    char input_buffer[128] = {0};
    if (default_value)
        strncpy(input_buffer, default_value, sizeof(input_buffer) - 1);

    int cursor_pos = 0; // Position in keyboard grid
    int layout_idx = 0; // Current keyboard layout
    int input_length = strlen(input_buffer);
    bool cancelled = false;

    const rg_keyboard_layout_t *current_layout = &keyboard_layouts[layout_idx];

    // Follow the same pattern as rg_gui_dialog
    rg_input_wait_for_key(RG_KEY_ALL, false, 1000);
    rg_task_delay(80);

    uint32_t joystick = 0, joystick_old;
    uint64_t joystick_last = 0;
    bool redraw = true;
    int redraws = 0;

    while (true)
    {
        // Handle input similar to rg_gui_dialog
        joystick_old = ((rg_system_timer() - joystick_last) > 300000) ? 0 : joystick;
        joystick = rg_input_read_gamepad();

        if (joystick ^ joystick_old)
        {
            if (joystick & RG_KEY_LEFT)
            {
                cursor_pos--;
                if (cursor_pos < 0)
                    cursor_pos = (current_layout->columns * current_layout->rows) - 1;
                redraw = true;
            }
            else if (joystick & RG_KEY_RIGHT)
            {
                cursor_pos++;
                if (cursor_pos >= current_layout->columns * current_layout->rows)
                    cursor_pos = 0;
                redraw = true;
            }
            else if (joystick & RG_KEY_UP)
            {
                cursor_pos -= current_layout->columns;
                if (cursor_pos < 0)
                    cursor_pos += current_layout->columns * current_layout->rows;
                redraw = true;
            }
            else if (joystick & RG_KEY_DOWN)
            {
                cursor_pos += current_layout->columns;
                if (cursor_pos >= current_layout->columns * current_layout->rows)
                    cursor_pos -= current_layout->columns * current_layout->rows;
                redraw = true;
            }
            else if (joystick & RG_KEY_A)
            {
                if (input_length < sizeof(input_buffer) - 4)
                {
                    const char *layout_ptr = current_layout->layout;
                    int key = 0;
                    for (int i = 0; i <= cursor_pos; ++i)
                        key = rg_utf8_decode(&layout_ptr);
                    input_length += rg_utf8_encode(&input_buffer[input_length], key);
                    input_buffer[input_length] = '\0';
                    redraw = true;
                }
            }
            else if (joystick & RG_KEY_B)
            {
                // Backspace
                while (input_length > 0)
                {
                    // Rewind until we find a valid codepoint
                    const char *ptr = &input_buffer[--input_length];
                    if (rg_utf8_decode(&ptr) != -1)
                        break;
                }
                input_buffer[input_length] = '\0';
                redraw = true;
            }
            else if (joystick & RG_KEY_SELECT)
            {
                // Toggle between layouts (Shift/Symbols)
                layout_idx = (layout_idx + 1) % RG_COUNT(keyboard_layouts);
                current_layout = &keyboard_layouts[layout_idx];
                cursor_pos = 0;
                redraw = true;
                redraws = 0;
            }
            else if (joystick & RG_KEY_START)
            {
                // OK/Enter - confirm input
                break;
            }
            else if (joystick & (RG_KEY_MENU | RG_KEY_OPTION))
            {
                // Cancel
                cancelled = true;
                break;
            }

            joystick_last = rg_system_timer();
        }

        if (redraw)
        {
            rg_gui_draw_input_screen(title, message, input_buffer, current_layout, cursor_pos, redraws++ > 0);
            redraw = false;
        }

        rg_task_delay(20);
        rg_system_tick(0);
    }

    rg_input_wait_for_key(joystick, false, 1000);
    rg_display_force_redraw();

    if (cancelled)
        return NULL;

    return input_length > 0 ? strdup(input_buffer) : NULL;
}

static rg_gui_event_t volume_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int level = rg_audio_get_volume();
    int prev_level = level;

    if (event == RG_DIALOG_PREV)
        level -= 5;
    if (event == RG_DIALOG_NEXT)
        level += 5;

    level -= (level % 5);

    if (level != prev_level)
        rg_audio_set_volume(level);

    sprintf(option->value, "%d%%", rg_audio_get_volume());

    return RG_DIALOG_VOID;
}

static rg_gui_event_t brightness_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int level = rg_display_get_backlight();
    int prev_level = level;

    if (event == RG_DIALOG_PREV)
        level -= 10;
    if (event == RG_DIALOG_NEXT)
        level += 10;

    level -= (level % 10);

    if (level != prev_level)
        rg_display_set_backlight(RG_MAX(level, 1));

    sprintf(option->value, "%d%%", rg_display_get_backlight());

    return RG_DIALOG_VOID;
}

static rg_gui_event_t audio_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    size_t count = 0;
    const rg_audio_sink_t *sinks = rg_audio_get_sinks(&count);
    const rg_audio_sink_t *ssink = rg_audio_get_sink();
    // Hide dummy unless it's the only one or we're a debug build
    int min = rg_system_get_app()->isRelease ? (1 % count) : (0);
    int max = count - 1;
    int sink = 0;

    // If there's no choice to be made we can just hide the entry
    if (min == max)
    {
        option->flags |= RG_DIALOG_FLAG_HIDDEN;
        return RG_DIALOG_VOID;
    }

    for (int i = 0; i < count; ++i)
        if (sinks[i].driver == ssink->driver && sinks[i].device == ssink->device)
            sink = i;

    int prev_sink = sink;

    if (event == RG_DIALOG_PREV && --sink < min)
        sink = max;
    if (event == RG_DIALOG_NEXT && ++sink > max)
        sink = min;

    if (sink != prev_sink)
        rg_audio_set_sink(sinks[sink].driver->name, sinks[sink].device);

    strcpy(option->value, sinks[sink].name);

    return RG_DIALOG_VOID;
}

static rg_gui_event_t filter_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int max = RG_DISPLAY_FILTER_COUNT - 1;
    int mode = rg_display_get_filter();
    int prev_mode = mode;

    if (event == RG_DIALOG_PREV && --mode < 0)
        mode = max;
    if (event == RG_DIALOG_NEXT && ++mode > max)
        mode = 0;

    if (mode != prev_mode)
    {
        rg_display_set_filter(mode);
        return RG_DIALOG_REDRAW;
    }

    if (mode == RG_DISPLAY_FILTER_OFF)
        strcpy(option->value, _("Off"));
    if (mode == RG_DISPLAY_FILTER_HORIZ)
        strcpy(option->value, _("Horiz"));
    if (mode == RG_DISPLAY_FILTER_VERT)
        strcpy(option->value, _("Vert"));
    if (mode == RG_DISPLAY_FILTER_BOTH)
        strcpy(option->value, _("Both"));

    return RG_DIALOG_VOID;
}

static rg_gui_event_t scaling_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int max = RG_DISPLAY_SCALING_COUNT - 1;
    int mode = rg_display_get_scaling();
    int prev_mode = mode;

    if (event == RG_DIALOG_PREV && --mode < 0)
        mode = max; // 0;
    if (event == RG_DIALOG_NEXT && ++mode > max)
        mode = 0; // max;

    if (mode != prev_mode)
    {
        rg_display_set_scaling(mode);
        return RG_DIALOG_REDRAW;
    }

    if (mode == RG_DISPLAY_SCALING_OFF)
        strcpy(option->value, _("Off"));
    else if (mode == RG_DISPLAY_SCALING_FIT)
        strcpy(option->value, _("Fit"));
    else if (mode == RG_DISPLAY_SCALING_FULL)
        strcpy(option->value, _("Full"));
    else if (mode == RG_DISPLAY_SCALING_ZOOM)
        strcpy(option->value, _("Zoom"));

    return RG_DIALOG_VOID;
}

static rg_gui_event_t custom_zoom_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (rg_display_get_scaling() != RG_DISPLAY_SCALING_ZOOM)
    {
        option->flags = RG_DIALOG_FLAG_HIDDEN;
        return RG_DIALOG_VOID;
    }

    if (event == RG_DIALOG_PREV)
        rg_display_set_custom_zoom(rg_display_get_custom_zoom() - 0.05);
    if (event == RG_DIALOG_NEXT)
        rg_display_set_custom_zoom(rg_display_get_custom_zoom() + 0.05);

    sprintf(option->value, "%.2f", rg_display_get_custom_zoom());
    option->flags = RG_DIALOG_FLAG_NORMAL;

    if (event == RG_DIALOG_PREV || event == RG_DIALOG_NEXT)
        return RG_DIALOG_REDRAW;
    return RG_DIALOG_VOID;
}

static rg_gui_event_t overclock_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    // if (event == RG_DIALOG_ENTER)
    // {
    //     const rg_gui_option_t options[] = {
    //         {0, _("CPU"), "-", RG_DIALOG_FLAG_NORMAL, &overclock_update_cb},
    //         {1, _("LCD"), "-", RG_DIALOG_FLAG_NORMAL, &overclock_update_cb},
    //         {2, _("SD"),  "-", RG_DIALOG_FLAG_NORMAL, &overclock_update_cb},
    //         RG_DIALOG_END,
    //     };
    //     rg_gui_dialog(option->label, options, 0);
    // }
    if (event == RG_DIALOG_PREV)
        rg_system_set_overclock(rg_system_get_overclock() - 1);
    else if (event == RG_DIALOG_NEXT)
        rg_system_set_overclock(rg_system_get_overclock() + 1);
    if (event == RG_DIALOG_INIT || event == RG_DIALOG_PREV || event == RG_DIALOG_NEXT)
        sprintf(option->value, "%d (%dMhz)", rg_system_get_overclock(), rg_system_get_cpu_speed());
    return RG_DIALOG_VOID;
}

static rg_gui_event_t speedup_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_PREV || event == RG_DIALOG_NEXT)
    {
        float change = (event == RG_DIALOG_NEXT) ? 0.5f : -0.5f;
        rg_system_set_app_speed(rg_system_get_app_speed() + change);
    }
    sprintf(option->value, "%.1fx", rg_system_get_app_speed());
    return RG_DIALOG_VOID;
}

static rg_gui_event_t led_indicator_opt_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_PREV || event == RG_DIALOG_NEXT)
    {
        rg_system_set_indicator_mask(option->arg, !rg_system_get_indicator_mask(option->arg));
    }
    strcpy(option->value, rg_system_get_indicator_mask(option->arg) ? _("On") : _("Off"));
    return RG_DIALOG_VOID;
}

static rg_gui_event_t led_indicator_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_ENTER)
    {
        const rg_gui_option_t options[] = {
            {RG_INDICATOR_ACTIVITY_SYSTEM, _("System activity"), "-", RG_DIALOG_FLAG_NORMAL, &led_indicator_opt_cb},
            {RG_INDICATOR_ACTIVITY_DISK, _("Disk activity"), "-", RG_DIALOG_FLAG_NORMAL, &led_indicator_opt_cb},
            {RG_INDICATOR_POWER_LOW, _("Low battery"), "-", RG_DIALOG_FLAG_NORMAL, &led_indicator_opt_cb},
            RG_DIALOG_END,
        };
        rg_gui_dialog(option->label, options, 0);
    }
    return RG_DIALOG_VOID;
}

// LED亮度回调（低、中、高）
static rg_gui_event_t led_brightness_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int brightness = rg_system_get_led_brightness();
    
    if (event == RG_DIALOG_PREV && --brightness < 1)
        brightness = 3;
    if (event == RG_DIALOG_NEXT && ++brightness > 3)
        brightness = 1;
    
    if (brightness != rg_system_get_led_brightness())
    {
        rg_system_set_led_brightness(brightness);
    }
    const char *brightness_text = NULL;
    brightness_text = (brightness == 1) ? _("Low") :
                      (brightness == 2) ? _("Medium") :
                                          _("High");
    strcpy(option->value, brightness_text);
    return RG_DIALOG_VOID;
}

// LED模式回调（关闭、常亮、呼吸）
static rg_gui_event_t led_mode_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int mode = rg_system_get_led_mode();
    
    if (event == RG_DIALOG_PREV && --mode < 0)
        mode = 2;
    if (event == RG_DIALOG_NEXT && ++mode > 2)
        mode = 0;
    
    if (mode != rg_system_get_led_mode())
    {
        rg_system_set_led_mode(mode);
    }
    const char *mode_text = NULL;
    mode_text = (mode == 0) ? _("Off") :
                (mode == 1) ? _("Solid") :
                            _("Breathing");
    strcpy(option->value, mode_text);
    return RG_DIALOG_VOID;
}

static rg_gui_event_t show_clock_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_PREV || event == RG_DIALOG_NEXT)
    {
        gui.show_clock = !gui.show_clock;
        rg_settings_set_boolean(NS_GLOBAL, SETTING_CLOCK, gui.show_clock);
        return RG_DIALOG_REDRAW;
    }
    strcpy(option->value, gui.show_clock ? _("On") : _("Off"));
    return RG_DIALOG_VOID;
}

static rg_gui_event_t timezone_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    const char utc_offsets[][10] = {"UTC-12:00", "UTC-11:00", "UTC-10:00", "UTC-09:00", "UTC-09:30", "UTC-08:00",
                                    "UTC-07:00", "UTC-06:00", "UTC-05:00", "UTC-04:00", "UTC-03:30", "UTC-03:00",
                                    "UTC-02:00", "UTC-01:00", "UTC+00:00", "UTC+01:00", "UTC+02:00", "UTC+03:00",
                                    "UTC+03:30", "UTC+04:00", "UTC+04:30", "UTC+05:00", "UTC+05:30", "UTC+06:00",
                                    "UTC+06:30", "UTC+07:00", "UTC+08:00", "UTC+09:00", "UTC+09:30", "UTC+10:00",
                                    "UTC+10:30", "UTC+11:00", "UTC+12:00", "UTC+13:00", "UTC+14:00"};
    int index = 14, old_index = index;
    char *TZ = rg_system_get_timezone();
    if (TZ && strncmp(TZ, "UTC", 3) == 0)
    {
        // TZ has inverted offset for whatever reason
        TZ[3] = TZ[3] == '-' ? '+' : '-';
        for (size_t i = 0; i < RG_COUNT(utc_offsets); ++i)
        {
            if (strcmp(TZ, utc_offsets[i]) == 0)
            {
                index = old_index = i;
                break;
            }
        }
    }
    free(TZ);
    if (event == RG_DIALOG_NEXT && index < RG_COUNT(utc_offsets) - 1)
        index++;
    else if (event == RG_DIALOG_PREV && index > 0)
        index--;
    if (index != old_index)
    {
        char *TZ = strdup(utc_offsets[index]);
        TZ[3] = TZ[3] == '-' ? '+' : '-';
        rg_system_set_timezone(TZ);
        free(TZ);
        if (gui.show_clock)
            return RG_DIALOG_REDRAW;
    }
    strcpy(option->value, utc_offsets[index]);
    return RG_DIALOG_VOID;
}

static rg_gui_event_t font_type_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_PREV && rg_gui_set_font(gui.font_index - 1))
        return RG_DIALOG_REDRAW;
    if (event == RG_DIALOG_NEXT && rg_gui_set_font(gui.font_index + 1))
        return RG_DIALOG_REDRAW;
    if (gui.font_height != gui.font->height)
        sprintf(option->value, "%s (%d)", gui.font->name, gui.font_height);
    else
        sprintf(option->value, "%s", gui.font->name);
    return RG_DIALOG_VOID;
}

static rg_gui_event_t theme_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_ENTER)
    {
        char *path = rg_gui_file_picker(_("Theme"), RG_BASE_PATH_THEMES, NULL, false, true);
        if (path != NULL)
        {
            const char *theme = strlen(path) > 0 ? rg_basename(path) : NULL;
            rg_gui_set_theme(theme);
            free(path);
            return RG_DIALOG_REDRAW;
        }
    }

    strcpy(option->value, rg_gui_get_theme_name() ?: _("Default"));
    return RG_DIALOG_VOID;
}

static rg_gui_event_t language_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int language_id = rg_localization_get_language_id();

    if (event == RG_DIALOG_ENTER)
    {
        rg_gui_option_t options[RG_LANG_MAX + 1];
        for (int i = 0; i < RG_LANG_MAX; i++)
            options[i] = (rg_gui_option_t){i, rg_localization_get_language_name(i), NULL, RG_DIALOG_FLAG_NORMAL, NULL};
        options[RG_LANG_MAX] = (rg_gui_option_t)RG_DIALOG_END;

        int sel = rg_gui_dialog(option->label, options, language_id);
        if (sel != RG_DIALOG_CANCELLED)
        {
            rg_gui_set_language_id(sel);
            if (rg_gui_confirm(_("Language changed!"), _("For these changes to take effect you must restart your device.\nrestart now?"), true))
            {
                rg_system_exit();
            }
            language_id = sel;
        }
        return RG_DIALOG_REDRAW;
    }

    sprintf(option->value, "%s", rg_localization_get_language_name(language_id) ?: "???");
    return RG_DIALOG_VOID;
}

static rg_gui_event_t border_update_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_ENTER)
    {
        char *path = rg_gui_file_picker("Border", RG_BASE_PATH_BORDERS, NULL, false, true);
        if (path != NULL)
        {
            rg_display_set_border(strlen(path) ? path : NULL);
            free(path);
            return RG_DIALOG_REDRAW;
        }
    }
    char *border = rg_display_get_border();
    sprintf(option->value, "%.9s", border ? rg_basename(border) : _("None"));
    free(border);
    return RG_DIALOG_VOID;
}

#ifdef RG_ENABLE_NETWORKING
static void wifi_toggle_interactive(bool enable, int slot)
{
    rg_network_state_t target_state = enable ? RG_NETWORK_CONNECTED : RG_NETWORK_DISCONNECTED;
    int64_t timeout = rg_system_timer() + 20 * 1000000;
    rg_gui_draw_message(enable ? _("Connecting...") : _("Disconnecting..."));
    rg_network_wifi_stop();
    if (enable)
    {
        rg_wifi_config_t config = {0};
        rg_network_wifi_read_config(slot, &config);
        rg_network_wifi_set_config(&config);
        if (slot == 9000)
        {
            const rg_wifi_config_t config = {
                .ssid = "retro-go",
                .password = "retro-go",
                .channel = 6,
                .ap_mode = true,
            };
            rg_network_wifi_set_config(&config);
        }
        if (!rg_network_wifi_start())
            return;
    }
    do // Always loop at least once, in case we're in a transition
    {
        rg_task_delay(100);
        if (rg_system_timer() > timeout)
            break;
        if (rg_input_read_gamepad())
            break;
    } while (rg_network_get_info().state != target_state);
}

static rg_gui_event_t wifi_status_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    rg_network_t info = rg_network_get_info();
    if (info.state != RG_NETWORK_CONNECTED)
        strcpy(option->value, _("Not connected"));
    else if (option->arg == 0x10)
        strcpy(option->value, info.name);
    else if (option->arg == 0x11)
        strcpy(option->value, info.ip_addr);
    return RG_DIALOG_VOID;
}

static rg_gui_event_t wifi_manage_slot_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int slot = option->arg;
    rg_wifi_config_t config = {0};

    if (event == RG_DIALOG_INIT || event == RG_DIALOG_UPDATE || event == RG_DIALOG_ENTER)
    {
        rg_network_wifi_read_config(slot, &config);
        strcpy(option->value, config.ssid[0] ? config.ssid : _("(add network)"));
    }

    if (event == RG_DIALOG_ENTER)
    {
        if (!config.ssid[0])
        {
            // Get SSID from user
            char *ssid = rg_gui_input_str(_("Wi-Fi SSID"), _("Enter new network name:"), "");
            if (!ssid || strlen(ssid) == 0)
            {
                free(ssid);
                return RG_DIALOG_VOID;
            }

            // Get password from user
            char *password = rg_gui_input_str(_("Wi-Fi Password"), _("Enter password (leave empty for open network):"), "");
            if (!password)
                password = strdup("");

            // Save the configuration
            rg_wifi_config_t new_config = {0};
            strncpy(new_config.ssid, ssid, sizeof(new_config.ssid) - 1);
            strncpy(new_config.password, password, sizeof(new_config.password) - 1);
            new_config.channel = 0; // Auto
            new_config.ap_mode = false;

            free(ssid);
            free(password);

            if (!rg_network_wifi_write_config(slot, &new_config))
            {
                rg_gui_alert(_("Error"), _("Failed to save network configuration"));
                return RG_DIALOG_VOID;
            }

            rg_settings_commit();
            config = new_config;
            // fall through, allowing the user to connect to the new network
        }

        char title[50];
        snprintf(title, sizeof(title), "Slot %d: %.15s", slot, config.ssid);

        const rg_gui_option_t slot_options[] = {
            {1, _("Connect"),       NULL, RG_DIALOG_FLAG_NORMAL, NULL},
            {2, _("Edit SSID"),     NULL, RG_DIALOG_FLAG_NORMAL, NULL},
            {3, _("Edit Password"), NULL, RG_DIALOG_FLAG_NORMAL, NULL},
            {4, _("Delete"),        NULL, RG_DIALOG_FLAG_NORMAL, NULL},
            RG_DIALOG_END,
        };

        int action = rg_gui_dialog(title, slot_options, 0);

        switch (action)
        {
            case 1: // Connect
                rg_settings_set_boolean(NS_WIFI, SETTING_WIFI_ENABLE, true);
                rg_settings_set_number(NS_WIFI, SETTING_WIFI_SLOT, slot);
                wifi_toggle_interactive(true, slot);
                break;

            case 2: // Edit SSID
            {
                char *new_ssid = rg_gui_input_str(_("Edit SSID"), _("Enter new network name:"), config.ssid);
                if (new_ssid && strlen(new_ssid) > 0)
                {
                    strncpy(config.ssid, new_ssid, sizeof(config.ssid) - 1);
                    config.ssid[sizeof(config.ssid) - 1] = '\0';
                    rg_network_wifi_write_config(slot, &config);
                    rg_settings_commit();
                    rg_gui_alert(_("Success"), _("SSID updated"));
                }
                free(new_ssid);
                break;
            }

            case 3: // Edit Password
            {
                char *new_password = rg_gui_input_str(_("Edit Password"), _("Enter new password:"), config.password);
                if (new_password)
                {
                    strncpy(config.password, new_password, sizeof(config.password) - 1);
                    config.password[sizeof(config.password) - 1] = '\0';
                    rg_network_wifi_write_config(slot, &config);
                    rg_settings_commit();
                    rg_gui_alert(_("Success"), _("Password updated"));
                }
                free(new_password);
                break;
            }

            case 4: // Delete
                if (rg_gui_confirm(_("Delete Network"), _("Are you sure you want to delete this network configuration?"), false))
                {
                    rg_network_wifi_delete_config(slot);
                    rg_settings_commit();
                    rg_gui_alert(_("Success"), _("Network configuration deleted"));
                }
                break;
        }

        strcpy(option->value, config.ssid[0] ? config.ssid : _("(empty)"));
        return RG_DIALOG_REDRAW;
    }

    return RG_DIALOG_VOID;
}

static rg_gui_event_t wifi_manage_networks_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_ENTER)
    {
        rg_gui_option_t slot_options[] = {
            {0, _("Slot 0"), "_", RG_DIALOG_FLAG_NORMAL, &wifi_manage_slot_cb},
            {1, _("Slot 1"), "_", RG_DIALOG_FLAG_NORMAL, &wifi_manage_slot_cb},
            {2, _("Slot 2"), "_", RG_DIALOG_FLAG_NORMAL, &wifi_manage_slot_cb},
            {3, _("Slot 3"), "_", RG_DIALOG_FLAG_NORMAL, &wifi_manage_slot_cb},
            {4, _("Slot 4"), "_", RG_DIALOG_FLAG_NORMAL, &wifi_manage_slot_cb},
            RG_DIALOG_END,
        };
        rg_gui_dialog(_("Manage Networks"), slot_options, 0);
        return RG_DIALOG_REDRAW;
    }
    return RG_DIALOG_VOID;
}

static rg_gui_event_t wifi_profile_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    int slot = rg_settings_get_number(NS_WIFI, SETTING_WIFI_SLOT, -1);
    rg_wifi_config_t config;
    if (rg_network_wifi_read_config(slot, &config))
        sprintf(option->value, "%d - %s", slot, config.ssid);
    else
        strcpy(option->value, _("None"));
    return RG_DIALOG_VOID;
}

static rg_gui_event_t wifi_access_point_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_ENTER)
    {
        if (rg_gui_confirm(_("Wi-Fi AP"), _("Start access point?\n\nSSID: retro-go\nPassword: retro-go\n\nBrowse: http://192.168.4.1/"), true))
        {
            wifi_toggle_interactive(true, 9000);
        }
        return RG_DIALOG_REDRAW;
    }
    return RG_DIALOG_VOID;
}

static rg_gui_event_t wifi_enable_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    bool enabled = rg_settings_get_boolean(NS_WIFI, SETTING_WIFI_ENABLE, false);
    if (event == RG_DIALOG_PREV || event == RG_DIALOG_NEXT || event == RG_DIALOG_ENTER)
    {
        enabled = !enabled;
        rg_settings_set_boolean(NS_WIFI, SETTING_WIFI_ENABLE, enabled);
        wifi_toggle_interactive(enabled, rg_settings_get_number(NS_WIFI, SETTING_WIFI_SLOT, -1));
        return RG_DIALOG_REDRAW;
    }
    strcpy(option->value, enabled ? _("On") : _("Off"));
    return RG_DIALOG_VOID;
}

static rg_gui_event_t wifi_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_ENTER)
    {
        const rg_gui_option_t options[] = {
            {0x00, _("Wi-Fi enable"),       "-",  RG_DIALOG_FLAG_NORMAL,  &wifi_enable_cb      },
            {0x00, _("Manage networks"),    NULL, RG_DIALOG_FLAG_NORMAL,  &wifi_manage_networks_cb },
            RG_DIALOG_SEPARATOR,
            {0x00, _("Wi-Fi access point"), NULL, RG_DIALOG_FLAG_NORMAL,  &wifi_access_point_cb},
            RG_DIALOG_SEPARATOR,
            {0x00, _("Wi-Fi profile"),      "-",  RG_DIALOG_FLAG_MESSAGE, &wifi_profile_cb     },
            {0x10, _("Network"),            "-",  RG_DIALOG_FLAG_MESSAGE, &wifi_status_cb      },
            {0x11, _("IP address"),         "-",  RG_DIALOG_FLAG_MESSAGE, &wifi_status_cb      },
            RG_DIALOG_END,
        };
        rg_gui_dialog(option->label, options, 0);
    }
    return RG_DIALOG_VOID;
}
#endif

static rg_gui_event_t app_options_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    if (event == RG_DIALOG_ENTER)
    {
        const rg_app_t *app = rg_system_get_app();
        rg_gui_option_t options[16] = {
            {0, _("None"), NULL, RG_DIALOG_FLAG_MESSAGE, 0},
            RG_DIALOG_END,
        };
        if (app->handlers.options)
            app->handlers.options(options);
        rg_display_force_redraw();
        rg_gui_dialog(option->label, options, 0);
        return RG_DIALOG_REDRAW;
    }
    return RG_DIALOG_VOID;
}

void rg_gui_options_menu(void)
{
    rg_gui_option_t options[20] = {
        #if RG_SCREEN_BACKLIGHT
        {0, _("Brightness"),    "-", RG_DIALOG_FLAG_NORMAL, &brightness_update_cb},
        #endif
        {0, _("Volume"),        "-", RG_DIALOG_FLAG_NORMAL, &volume_update_cb},
        // {0, _("Audio out"),     "-", RG_DIALOG_FLAG_NORMAL, &audio_update_cb},//音频
        RG_DIALOG_END,
    };
    const rg_gui_option_t misc_options[] = {
        // {0, _("Font type"),     "-", RG_DIALOG_FLAG_NORMAL, &font_type_cb},//字体
        {0, _("Theme"),         "-", RG_DIALOG_FLAG_NORMAL, &theme_cb},
        // {0, _("Show clock"),    "-", RG_DIALOG_FLAG_NORMAL, &show_clock_cb},//时钟
        // {0, _("Timezone"),      "-", RG_DIALOG_FLAG_NORMAL, &timezone_cb},//时区
        {0, _("Language"),      "-", RG_DIALOG_FLAG_NORMAL, &language_cb},
        #ifdef RG_GPIO_LED // Only show disk LED option if disk LED GPIO pin is defined
        {0, _("LED Brightness"), "-", RG_DIALOG_FLAG_NORMAL, &led_brightness_cb},
        {0, _("LED Mode"),      "-", RG_DIALOG_FLAG_NORMAL, &led_mode_cb},
        #endif
        #ifdef RG_ENABLE_NETWORKING
        {0, _("Wi-Fi options"), NULL, RG_DIALOG_FLAG_NORMAL, &wifi_cb},
        #endif
        {0, _("Launcher options"), NULL, RG_DIALOG_FLAG_NORMAL, &app_options_cb},
        RG_DIALOG_END,
    };
    const rg_gui_option_t game_options[] = {
        {0, _("Scaling"),       "-", RG_DIALOG_FLAG_NORMAL, &scaling_update_cb},
        {0, _("Factor"),        "-", RG_DIALOG_FLAG_HIDDEN, &custom_zoom_cb},
        {0, _("Filter"),        "-", RG_DIALOG_FLAG_NORMAL, &filter_update_cb},
        {0, _("Border"),        "-", RG_DIALOG_FLAG_NORMAL, &border_update_cb},
        {0, _("Speed"),         "-", RG_DIALOG_FLAG_NORMAL, &speedup_update_cb},
        // {0, _("Misc options"),  NULL, RG_DIALOG_FLAG_NORMAL, &misc_options_cb},
        #if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S3 // && !RG_BUILD_RELEASE
        {0, _("Overclock"),     "-", RG_DIALOG_FLAG_NORMAL, &overclock_cb},
        #endif
        {0, _("Emulator options"), NULL, RG_DIALOG_FLAG_NORMAL, &app_options_cb},
        RG_DIALOG_END,
    };

    const rg_app_t *app = rg_system_get_app();
    if (app->isLauncher)
        memcpy(options + get_dialog_items_count(options), misc_options, sizeof(misc_options));
    else
        memcpy(options + get_dialog_items_count(options), game_options, sizeof(game_options));

    rg_audio_set_mute(true);

    rg_gui_dialog(_("Options"), options, 0);
    rg_settings_commit();

    rg_audio_set_mute(false);
}

void rg_gui_about_menu(void)
{
    const rg_app_t *app = rg_system_get_app();
    bool have_option_btn = rg_input_key_is_present(RG_KEY_OPTION);

    // TODO: Add indicator whether or not the build is a release, and if it's official (built by me)
    rg_gui_option_t options[20] = {
        {0, _("Version"), (char *)app->version, RG_DIALOG_FLAG_MESSAGE, NULL},
        {0, _("Date"), (char *)app->buildDate, RG_DIALOG_FLAG_MESSAGE, NULL},
        {0, _("Target"), (char *)RG_TARGET_NAME, RG_DIALOG_FLAG_MESSAGE, NULL},
        {0, _("Website"), (char *)RG_PROJECT_WEBSITE, RG_DIALOG_FLAG_MESSAGE, NULL},
        RG_DIALOG_SEPARATOR,
        {4, _("Options"), NULL, have_option_btn ? RG_DIALOG_FLAG_HIDDEN : RG_DIALOG_FLAG_NORMAL , NULL},
        // {1, _("View credits", NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {2, _("Debug menu"), NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {3, _("Reset settings"), NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        RG_DIALOG_END,
    };

    if (app->handlers.about)
        app->handlers.about(options + get_dialog_items_count(options));

    while (true)
    {
        switch (rg_gui_dialog(_("About Retro-Go"), options, 4))
        {
            case 1:
                // FIXME: This should probably be a regular dialog so that it's scrollable!
                rg_gui_alert("Credits", RG_PROJECT_CREDITS);
                break;
            case 2:
                rg_gui_debug_menu();
                break;
            case 3:
                if (rg_gui_confirm(_("Reset all settings?"), NULL, false)) {
                    rg_storage_delete(RG_BASE_PATH_CACHE);
                    rg_settings_reset();
                    rg_system_restart();
                    return;
                }
                break;
            case 4:
                rg_gui_options_menu();
                break;
            default:
                return;
        }
    }
}

void rg_gui_debug_menu(void)
{
    char screen_res[20], source_res[20], scaled_res[20];
    char stack_hwm[20], heap_free[20], block_free[20];
    char local_time[32], timezone[32], uptime[20];
    char battery_info[20], frame_time[20], overclock[20];
    char app_name[32];

    const rg_gui_option_t options[] = {
        {0x000, "Screen res", screen_res,   RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Source res", source_res,   RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Scaled res", scaled_res,   RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Stack HWM ", stack_hwm,    RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Heap free ", heap_free,    RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Block free", block_free,   RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "App name  ", app_name,     RG_DIALOG_FLAG_NORMAL, NULL},
        // {0x000, "Network   ", network_str,  RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Local time", local_time,   RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Timezone  ", timezone,     RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Uptime    ", uptime,       RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Battery   ", battery_info, RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Blit time ", frame_time,   RG_DIALOG_FLAG_NORMAL, NULL},
        {0x000, "Overclock",  overclock,    RG_DIALOG_FLAG_NORMAL, NULL},
        RG_DIALOG_SEPARATOR,
        {0x001, "Reboot to firmware",   NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {0x002, "Clear cache    ",      NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {0x003, "Save screenshot",      NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {0x004, "Save trace",           NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {0x005, "Cheats    ",           NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {0x006, "Crash     ",           NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {0x007, "Log=debug ",           NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        RG_DIALOG_END
    };

    const rg_display_t *display = rg_display_get_info();
    rg_display_counters_t display_stats = rg_display_get_counters();
    rg_stats_t stats = rg_system_get_stats();
    time_t now = time(NULL);

    strftime(local_time, 32, "%F %T", localtime(&now));
    snprintf(timezone, 32, "%s", getenv("TZ") ?: "N/A");
    snprintf(screen_res, 20, "%dx%d", display->screen.width, display->screen.height);
    snprintf(source_res, 20, "%dx%d", display->source.width, display->source.height);
    snprintf(scaled_res, 20, "%dx%d", display->viewport.width, display->viewport.height);
    if (display_stats.totalFrames > 0)
    {
        int total = (float)display_stats.busyTime / display_stats.totalFrames / 1000.f;
        int block = (float)display_stats.blockTime / display_stats.totalFrames / 1000.f;
        snprintf(frame_time, 20, "%dms (block: %dms)", total, block);
    }
    else
        snprintf(frame_time, 20, "N/A");
    snprintf(stack_hwm, 20, "%d", stats.freeStack[0]);
    snprintf(heap_free, 20, "%d+%d", stats.freeMemoryInt, stats.freeMemoryExt);
    snprintf(block_free, 20, "%d+%d", stats.freeBlockInt, stats.freeBlockExt);
    snprintf(app_name, 32, "%s", rg_system_get_app()->name);
    snprintf(uptime, 20, "%ds", stats.uptime);
    snprintf(overclock, 20, "%d (%dMhz)", rg_system_get_overclock(), rg_system_get_cpu_speed());

    rg_battery_t battery;
    if (rg_input_read_battery_raw(&battery))
        snprintf(battery_info, sizeof(battery_info), "%.2f%% | %.2fV", battery.level, battery.volts);
    else
        snprintf(battery_info, sizeof(battery_info), "N/A");

    // rg_network_t net = rg_network_get_info();
    // if (net.state == RG_NETWORK_DISABLED)
    //     snprintf(network_str, 64, "%s", "not available");
    // else if (net.state == RG_NETWORK_CONNECTED)
    //     snprintf(network_str, 64, "%s\n%s", net.name, net.ip_addr);
    // else if (net.state == RG_NETWORK_CONNECTING)
    //     snprintf(network_str, 64, "%s\n%s", net.name, "connecting...");
    // else if (net.name[0])
    //     snprintf(network_str, 64, "%s\n%s", net.name, "disconnected");
    // else
    //     snprintf(network_str, 64, "%s", "disconnected");

    switch (rg_gui_dialog("Debugging", options, 0))
    {
    case 0x001:
        rg_system_switch_app(RG_APP_FACTORY, NULL, NULL, 0, 0);
        break;
    case 0x002:
        rg_storage_delete(RG_BASE_PATH_CACHE);
        rg_system_restart();
        break;
    case 0x003:
        rg_emu_screenshot(RG_STORAGE_ROOT "/screenshot.png", 0, 0);
        break;
    case 0x004:
        rg_system_save_trace(RG_STORAGE_ROOT "/trace.txt", 0);
        break;
    case 0x005:
        break;
    case 0x006:
        RG_PANIC("Crash test!");
        break;
    case 0x007:
        rg_system_set_log_level(RG_LOG_DEBUG);
        break;
    case 0x100:
        rg_display_clear(C_BLACK);
        rg_display_set_geometry(
            RG_MAX(64, (rand() % RG_SCREEN_WIDTH) & ~0x3),
            RG_MAX(64, (rand() % RG_SCREEN_HEIGHT) & ~0x3),
            &(rg_margins_t){rand() % 16, rand() % 16, rand() % 16, rand() % 16}
        );
        break;
    }
}

static rg_gui_event_t slot_select_cb(rg_gui_option_t *option, rg_gui_event_t event)
{
    rg_emu_slot_t *slot = (rg_emu_slot_t *)option->arg;
    if (event == RG_DIALOG_FOCUS_GAINED)
    {
        rg_image_t *preview = NULL;
        rg_color_t color = C_BLUE;
        size_t border = 3;
        char buffer[100];
        if (slot->is_used)
        {
            preview = rg_surface_load_image_file(slot->preview, 0);
            if (slot->is_lastused)
                snprintf(buffer, sizeof(buffer), _("last used"));
        }
        else
        {
            snprintf(buffer, sizeof(buffer), _("is empty"));
            color = C_RED;
        }
        rg_gui_draw_image(0, 0, gui.screen_width, gui.screen_height, true, preview);
        rg_gui_draw_rect(0, 0, gui.screen_width, gui.screen_height, border, color, C_NONE);
        rg_gui_draw_text(border, border, gui.screen_width - border * 2, buffer, C_WHITE, C_BLACK, RG_TEXT_ALIGN_CENTER|RG_TEXT_NO_PADDING);
        rg_surface_free(preview);
    }
    else if (event == RG_DIALOG_ENTER)
    {
        return RG_DIALOG_SELECT;
    }
    return RG_DIALOG_VOID;
    #undef draw_status
}

int rg_gui_savestate_menu(const char *title, const char *rom_path)
{
    rg_emu_states_t *savestates = rg_emu_get_states(rom_path, 4);
    const rg_gui_option_t choices[] = {
        {(intptr_t)&savestates->slots[0], _("Slot 0"), NULL, RG_DIALOG_FLAG_NORMAL, &slot_select_cb},
        {(intptr_t)&savestates->slots[1], _("Slot 1"), NULL, RG_DIALOG_FLAG_NORMAL, &slot_select_cb},
        {(intptr_t)&savestates->slots[2], _("Slot 2"), NULL, RG_DIALOG_FLAG_NORMAL, &slot_select_cb},
        {(intptr_t)&savestates->slots[3], _("Slot 3"), NULL, RG_DIALOG_FLAG_NORMAL, &slot_select_cb},
        RG_DIALOG_END
    };

    intptr_t ret = rg_gui_dialog(title, choices, savestates->lastused ? savestates->lastused->id : 0);
    int slot = (ret == RG_DIALOG_CANCELLED) ? -1 : ((rg_emu_slot_t *)ret)->id;
    free(savestates);
    return slot;
}

void rg_gui_game_menu(void)
{
    const char *rom_path = rg_system_get_app()->romPath;
    bool have_option_btn = rg_input_key_is_present(RG_KEY_OPTION);
    const rg_gui_option_t choices[] = {
        {1000, _("Save & Continue"), NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {2000, _("Save & Quit"),     NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {3001, _("Load game"),       NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {3000, _("Reset"),           NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        #ifdef RG_ENABLE_NETPLAY
        {5000, _("Netplay"),         NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        #endif
        {5500, _("Options"),         NULL, have_option_btn ? RG_DIALOG_FLAG_HIDDEN : RG_DIALOG_FLAG_NORMAL, NULL},
        {6000, _("About"),           NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        {7000, _("Quit"),            NULL, RG_DIALOG_FLAG_NORMAL, NULL},
        RG_DIALOG_END
    };
    int slot, sel;

    rg_audio_set_mute(true);

    sel = rg_gui_dialog("Retro-Go", choices, 0);

    if (sel == 3000)
    {
        const rg_gui_option_t choices[] = {
            {3002, _("Soft reset"), NULL, RG_DIALOG_FLAG_NORMAL, NULL},
            {3003, _("Hard reset"), NULL, RG_DIALOG_FLAG_NORMAL, NULL},
            RG_DIALOG_END
        };
        sel = rg_gui_dialog(_("Reset Emulation?"), choices, 0);
    }

    switch (sel)
    {
        case 1000: if ((slot = rg_gui_savestate_menu(_("Save"), rom_path)) >= 0) rg_emu_save_state(slot); break;
        case 2000: if ((slot = rg_gui_savestate_menu(_("Save"), rom_path)) >= 0 && rg_emu_save_state(slot)) rg_system_exit(); break;
        case 3001: if ((slot = rg_gui_savestate_menu(_("Load"), rom_path)) >= 0) rg_emu_load_state(slot); break;
        case 3002: rg_emu_reset(false); break;
        case 3003: rg_emu_reset(true); break;
    #ifdef RG_ENABLE_NETPLAY
        case 5000: rg_netplay_quick_start(); break;
    #endif
        case 5500: rg_gui_options_menu(); break;
        case 6000: rg_gui_about_menu(); break;
        case 7000: rg_system_exit(); break;
    }

    rg_audio_set_mute(false);
}
