// gamebook.c — see gamebook.h. Touch CYOA for the ESP32-S3-BOX-3.
#include "gamebook.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "cJSON.h"
#include "esp_log.h"
#include "esp_heap_caps.h"

#include "bsp/esp-bsp.h"
#include "lvgl.h"
#include "cmd_exec.h"   // async WAV narration player

static const char *TAG = "gamebook";

// Custom fonts with French glyphs (ASCII + Latin-1 + ’ + …). See main/font_fr_*.c.
LV_FONT_DECLARE(font_fr_14);
LV_FONT_DECLARE(font_fr_24);

#define GB_DIR      "/sdcard/gamebook"
#define GB_LIBRARY  GB_DIR "/library.json"
#define GB_JSON_MAX (256 * 1024)        // expanded books are ~80 KB of JSON

// Colours (Workbench-ish palette).
#define COL_BG     0x101820
#define COL_TITLE  0xFFCC55
#define COL_TEXT   0xF0F0F0
#define COL_BTN    0x224466

static cJSON   *s_lib_root  = NULL;     // owns library.json
static cJSON   *s_lib       = NULL;     // borrowed: root->"library"
static int      s_lib_n     = 0;

static cJSON   *s_book      = NULL;     // owns the current <id>.json
static cJSON   *s_passages  = NULL;     // borrowed: book->"passages"

static lv_obj_t *s_root     = NULL;     // scrollable full-screen column

// ── PSRAM allocators for cJSON (book trees are large) ───────────────────────
static void *gb_malloc(size_t sz) { return heap_caps_malloc(sz, MALLOC_CAP_SPIRAM); }
static void  gb_free(void *p)      { heap_caps_free(p); }

static cJSON *load_json(const char *path)
{
    FILE *f = fopen(path, "rb");
    if (!f) { ESP_LOGW(TAG, "open %s failed", path); return NULL; }
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    rewind(f);
    if (sz <= 0 || sz > GB_JSON_MAX) { fclose(f); ESP_LOGW(TAG, "%s size %ld", path, sz); return NULL; }
    char *buf = heap_caps_malloc((size_t)sz + 1, MALLOC_CAP_SPIRAM);
    if (!buf) { fclose(f); return NULL; }
    size_t rd = fread(buf, 1, (size_t)sz, f);
    fclose(f);
    buf[rd] = '\0';
    cJSON *root = cJSON_Parse(buf);
    heap_caps_free(buf);
    if (!root) ESP_LOGW(TAG, "malformed JSON: %s", path);
    return root;
}

// ── Root container (created once, cleared per view) ─────────────────────────
// All UI functions below assume the LVGL lock is held (gamebook_init takes it;
// button callbacks already run inside the LVGL task / lock).
static void ensure_root(void)
{
    if (s_root) { lv_obj_clean(s_root); return; }
    lv_obj_t *scr = lv_screen_active();
    s_root = lv_obj_create(scr);
    lv_obj_set_size(s_root, LV_PCT(100), LV_PCT(100));
    lv_obj_set_pos(s_root, 0, 0);
    lv_obj_set_style_bg_color(s_root, lv_color_hex(COL_BG), 0);
    lv_obj_set_style_bg_opa(s_root, LV_OPA_COVER, 0);
    lv_obj_set_style_border_width(s_root, 0, 0);
    lv_obj_set_style_radius(s_root, 0, 0);
    lv_obj_set_style_pad_all(s_root, 8, 0);
    lv_obj_set_style_pad_row(s_root, 8, 0);
    lv_obj_set_flex_flow(s_root, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_scroll_dir(s_root, LV_DIR_VER);
}

static lv_obj_t *add_title(const char *txt)
{
    lv_obj_t *l = lv_label_create(s_root);
    lv_obj_set_style_text_font(l, &font_fr_24, 0);
    lv_obj_set_style_text_color(l, lv_color_hex(COL_TITLE), 0);
    lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
    lv_obj_set_width(l, LV_PCT(100));
    lv_label_set_text(l, txt);
    return l;
}

static lv_obj_t *add_text(const char *txt)
{
    lv_obj_t *l = lv_label_create(s_root);
    lv_obj_set_style_text_font(l, &font_fr_14, 0);
    lv_obj_set_style_text_color(l, lv_color_hex(COL_TEXT), 0);
    lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
    lv_obj_set_width(l, LV_PCT(100));
    lv_label_set_text(l, txt);
    return l;
}

static lv_obj_t *add_button(const char *txt, lv_event_cb_t cb, void *user)
{
    lv_obj_t *b = lv_button_create(s_root);
    lv_obj_set_width(b, LV_PCT(100));
    lv_obj_set_style_bg_color(b, lv_color_hex(COL_BTN), 0);
    lv_obj_set_style_pad_all(b, 10, 0);
    lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, user);
    lv_obj_t *l = lv_label_create(b);
    lv_obj_set_style_text_font(l, &font_fr_14, 0);
    lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
    lv_obj_set_width(l, LV_PCT(100));
    lv_label_set_text(l, txt);
    return b;
}

// Forward decls
static void show_library(void);
static void enter_passage(const char *pid);

static void tile_cb(lv_event_t *e);
static void choice_cb(lv_event_t *e);
static void menu_cb(lv_event_t *e);

// ── Library menu ────────────────────────────────────────────────────────────
static void show_library(void)
{
    cmd_exec_stop_play();   // silence any passage narration
    if (s_book) { cJSON_Delete(s_book); s_book = NULL; s_passages = NULL; }
    ensure_root();
    add_title("Choisis ton histoire");
    for (int i = 0; i < s_lib_n; i++) {
        const cJSON *t = cJSON_GetObjectItem(cJSON_GetArrayItem(s_lib, i), "title");
        add_button(cJSON_IsString(t) ? t->valuestring : "?",
                   tile_cb, (void *)(intptr_t)i);
    }
    lv_obj_scroll_to_y(s_root, 0, LV_ANIM_OFF);
}

// ── Passage ─────────────────────────────────────────────────────────────────
static void enter_passage(const char *pid)
{
    const cJSON *p = cJSON_GetObjectItem(s_passages, pid);
    if (!cJSON_IsObject(p)) { ESP_LOGW(TAG, "passage '%s' missing", pid); return; }
    const cJSON *screen  = cJSON_GetObjectItem(p, "screen");
    const cJSON *text    = cJSON_GetObjectItem(p, "text");
    const cJSON *choices = cJSON_GetObjectItem(p, "choices");

    ensure_root();
    add_title(cJSON_IsString(screen) ? screen->valuestring : "");
    add_text(cJSON_IsString(text) ? text->valuestring : "");

    // Narration: play this passage's WAV from the SD if the pack has audio.
    // Interruptible — tapping a choice cuts it and plays the next passage.
    const cJSON *wav = cJSON_GetObjectItem(p, "wav");
    if (cJSON_IsString(wav) && wav->valuestring[0]) {
        char path[160];
        snprintf(path, sizeof(path), "%s/%s", GB_DIR, wav->valuestring);
        cmd_exec_play_file_async(path);
    }
    int n = cJSON_IsArray(choices) ? cJSON_GetArraySize(choices) : 0;
    if (n == 0) {
        add_button("Revenir au menu", menu_cb, NULL);
    } else {
        for (int i = 0; i < n; i++) {
            const cJSON *c = cJSON_GetArrayItem(choices, i);
            const cJSON *lbl = cJSON_GetObjectItem(c, "label");
            const cJSON *g   = cJSON_GetObjectItem(c, "goto");
            // goto valuestring stays valid while s_book is alive → use as user_data
            add_button(cJSON_IsString(lbl) ? lbl->valuestring : "?",
                       choice_cb, cJSON_IsString(g) ? g->valuestring : NULL);
        }
    }
    lv_obj_scroll_to_y(s_root, 0, LV_ANIM_OFF);
    ESP_LOGI(TAG, "passage '%s' (%d choices)", pid, n);
}

static void load_book(int idx)
{
    const cJSON *e = cJSON_GetArrayItem(s_lib, idx);
    const cJSON *file = cJSON_GetObjectItem(e, "book");
    if (!cJSON_IsString(file)) return;
    char path[128];
    snprintf(path, sizeof(path), "%s/%s", GB_DIR, file->valuestring);
    cJSON *book = load_json(path);
    if (!book) return;
    const cJSON *passages = cJSON_GetObjectItem(book, "passages");
    const cJSON *start = cJSON_GetObjectItem(book, "start");
    if (!cJSON_IsObject(passages) || !cJSON_IsString(start)) {
        cJSON_Delete(book); return;
    }
    if (s_book) cJSON_Delete(s_book);
    s_book = book;
    s_passages = (cJSON *)passages;
    ESP_LOGI(TAG, "load book #%d @ '%s'", idx, start->valuestring);
    enter_passage(start->valuestring);
}

// ── Touch callbacks (run inside the LVGL task → lock already held) ───────────
static void tile_cb(lv_event_t *e)   { load_book((int)(intptr_t)lv_event_get_user_data(e)); }
static void menu_cb(lv_event_t *e)   { (void)e; show_library(); }
static void choice_cb(lv_event_t *e)
{
    const char *goto_id = (const char *)lv_event_get_user_data(e);
    if (goto_id) enter_passage(goto_id);
}

// ── Public init ─────────────────────────────────────────────────────────────
void gamebook_init(void)
{
    cJSON_Hooks hooks = { .malloc_fn = gb_malloc, .free_fn = gb_free };
    cJSON_InitHooks(&hooks);   // keep the big book trees in PSRAM

    if (bsp_sdcard_mount() != ESP_OK) {
        ESP_LOGW(TAG, "no SD card — gamebook disabled");
        return;
    }
    s_lib_root = load_json(GB_LIBRARY);
    if (!s_lib_root) { ESP_LOGW(TAG, "no library.json — gamebook disabled"); return; }
    s_lib = cJSON_GetObjectItem(s_lib_root, "library");
    if (!cJSON_IsArray(s_lib) || cJSON_GetArraySize(s_lib) == 0) {
        cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL;
        ESP_LOGW(TAG, "empty library");
        return;
    }
    s_lib_n = cJSON_GetArraySize(s_lib);

    bsp_display_brightness_set(80);
    if (bsp_display_lock(1000)) {
        show_library();
        bsp_display_unlock();
    }
    ESP_LOGI(TAG, "touch gamebook up (%d stories)", s_lib_n);
}
