// gamebook.c — see gamebook.h. Standalone gamebook player + library for the
// Freenove master. On boot it shows the library (a grid of story tiles);
// picking one loads and plays that book; finishing returns to the library.
#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 "display_ui.h"
#include "media_manager.h"

static const char *TAG = "gamebook";

#define GAMEBOOK_DIR   "/sdcard/gamebook"
#define LIBRARY_JSON   GAMEBOOK_DIR "/library.json"
#define JSON_MAX       (256 * 1024)  /* expanded books are ~80 KB of JSON */
#define LIB_MAX        6             /* matches the display tile grid */

typedef enum { GB_OFF, GB_LIBRARY, GB_STORY } gb_mode_t;

// Library state
static cJSON   *s_lib_root  = NULL;  /* owns library.json while loaded   */
static cJSON   *s_lib       = NULL;  /* borrowed: root->"library" array  */
static int      s_lib_n     = 0;
static int      s_lib_sel   = 0;

// Story state
static cJSON   *s_book      = NULL;  /* owns the current <book>.json      */
static cJSON   *s_passages  = NULL;  /* borrowed: book->"passages"        */
static char     s_title[48] = {0};
static char     s_current[48] = {0};
static int      s_sel       = 0;     /* highlighted choice index          */

static volatile gb_mode_t s_mode = GB_OFF;

bool gamebook_active(void) { return s_mode != GB_OFF; }

/* PSRAM allocators for cJSON — book trees are large (see gamebook_init). */
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); }

/* Read a whole JSON file from the SD into a cJSON tree (caller frees). */
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 > 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;
}

// ── Library ──────────────────────────────────────────────────────────────────

static void render_library(void)
{
    const char *titles[LIB_MAX] = {0};
    int n = (s_lib_n < LIB_MAX) ? s_lib_n : LIB_MAX;
    for (int i = 0; i < n; i++) {
        const cJSON *t = cJSON_GetObjectItem(cJSON_GetArrayItem(s_lib, i), "title");
        titles[i] = cJSON_IsString(t) ? t->valuestring : "?";
    }
    display_ui_library_show(titles, n, s_lib_sel);
}

static void free_story(void)
{
    media_manager_stop();
    if (s_book) { cJSON_Delete(s_book); s_book = NULL; }
    s_passages = NULL;
    s_current[0] = '\0';
}

static esp_err_t open_library(void)
{
    free_story();
    if (s_lib_root) { cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL; }

    s_lib_root = load_json(LIBRARY_JSON);
    if (!s_lib_root) return ESP_ERR_NOT_FOUND;
    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, "library.json has no stories");
        return ESP_ERR_INVALID_ARG;
    }
    s_lib_n = cJSON_GetArraySize(s_lib);
    s_lib_sel = 0;
    s_mode = GB_LIBRARY;
    display_ui_gamebook_hide();
    render_library();
    ESP_LOGI(TAG, "library open (%d stories)", s_lib_n);
    return ESP_OK;
}

// ── Story ─────────────────────────────────────────────────────────────────────

static const cJSON *cur_passage(void) { return cJSON_GetObjectItem(s_passages, s_current); }

static int cur_choice_count(void)
{
    const cJSON *ch = cJSON_GetObjectItem(cur_passage(), "choices");
    return cJSON_IsArray(ch) ? cJSON_GetArraySize(ch) : 0;
}

/* (Re)draw the current page. `home` true = new passage (reset scroll to top);
 * false = same passage, only the choice cursor moved (keep scroll position).
 * The bottom line shows ONE choice with < > arrows (Left/Right cycles). */
static void render_page(bool home)
{
    const cJSON *p = cur_passage();
    if (!cJSON_IsObject(p)) return;
    const cJSON *screen  = cJSON_GetObjectItem(p, "screen");
    const cJSON *text    = cJSON_GetObjectItem(p, "text");
    const cJSON *choices = cJSON_GetObjectItem(p, "choices");
    int n = cJSON_IsArray(choices) ? cJSON_GetArraySize(choices) : 0;

    char menu[160];
    if (n == 0) {
        snprintf(menu, sizeof(menu), "~ Fin ~   (clic = bibliotheque)");
    } else {
        const cJSON *lbl = cJSON_GetObjectItem(cJSON_GetArrayItem(choices, s_sel),
                                               "label");
        const char *txt = cJSON_IsString(lbl) ? lbl->valuestring : "?";
        if (n > 1) {
            /* Down cycles the answer, Click validates. */
            snprintf(menu, sizeof(menu), "%d/%d  %s   (bas / clic)",
                     s_sel + 1, n, txt);
        } else {
            snprintf(menu, sizeof(menu), "%s   (clic)", txt);
        }
    }
    display_ui_gamebook_show(
        cJSON_IsString(screen) ? screen->valuestring : s_title,
        cJSON_IsString(text)   ? text->valuestring   : "",
        menu, home);
}

static void enter_passage(const char *pid)
{
    const cJSON *p = cJSON_GetObjectItem(s_passages, pid);
    if (!cJSON_IsObject(p)) { ESP_LOGW(TAG, "passage '%s' not found", pid); return; }
    snprintf(s_current, sizeof(s_current), "%s", pid);
    s_sel = 0;
    render_page(true);

    const cJSON *wav = cJSON_GetObjectItem(p, "wav");
    if (cJSON_IsString(wav) && wav->valuestring[0]) {
        char path[96];
        snprintf(path, sizeof(path), "%s/%s", GAMEBOOK_DIR, wav->valuestring);
        media_manager_stop();   /* a choice skips the narration */
        media_manager_play(path);
    }
    ESP_LOGI(TAG, "passage '%s' (%d choices)", pid, cur_choice_count());
}

/* Load story #idx from the library and play it. */
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)) { ESP_LOGW(TAG, "library[%d] has no book", idx); return; }

    char path[128];
    snprintf(path, sizeof(path), "%s/%s", GAMEBOOK_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");
    const cJSON *title = cJSON_GetObjectItem(book, "title");
    if (!cJSON_IsObject(passages) || !cJSON_IsString(start)) {
        ESP_LOGW(TAG, "%s missing passages/start", path);
        cJSON_Delete(book);
        return;
    }
    free_story();
    s_book = book;
    s_passages = (cJSON *)passages;
    snprintf(s_title, sizeof(s_title), "%s",
             cJSON_IsString(title) ? title->valuestring : "");
    s_mode = GB_STORY;
    display_ui_library_hide();
    ESP_LOGI(TAG, "load book \"%s\" @ '%s'", s_title, start->valuestring);
    enter_passage(start->valuestring);
}

// ── Input ─────────────────────────────────────────────────────────────────────

/* MEASURED pad mapping on this hardware (ADC ladder, see calibration):
 *   key1 = CLICK (centre, ~0 mV)
 *   key2 = LEFT  (gauche, ~700 mV)
 *   key4 = DOWN  (bas,    ~1350 mV)
 *   key5 = RIGHT (droite, ~1994 mV)
 *   key3 is never produced; the physical UP button is electrically dead
 *   (held = no voltage change), so nothing can depend on it. */
#define PAD_CLICK 1
#define PAD_LEFT  2
#define PAD_DOWN  4
#define PAD_RIGHT 5

/* Library nav: Down/Right → next tile, Left → previous, Click → open.
 * Selection wraps, so the working buttons reach every story without Up. */
static bool library_key(uint8_t key)
{
    if (s_lib_n <= 0) return true;
    switch (key) {
    case PAD_DOWN: case PAD_RIGHT:
        s_lib_sel = (s_lib_sel + 1) % s_lib_n; render_library(); break;
    case PAD_LEFT:
        s_lib_sel = (s_lib_sel - 1 + s_lib_n) % s_lib_n; render_library(); break;
    case PAD_CLICK:
        load_book(s_lib_sel); break;
    default: break;
    }
    return true;
}

/* Story nav (Up is dead, so it's never used):
 *   Left  → scroll the reading text UP
 *   Right → scroll the reading text DOWN
 *   Down  → next answer (cycles through all choices, wraps around)
 *   Click → validate the highlighted answer
 * On an ending page (no choices): Left/Right still scroll, Click → library. */
static bool story_key(uint8_t key)
{
    if (key == PAD_LEFT)  { display_ui_gamebook_scroll(-1); return true; }  /* scroll up   */
    if (key == PAD_RIGHT) { display_ui_gamebook_scroll(+1); return true; }  /* scroll down */

    int n = cur_choice_count();
    if (n == 0) {                       /* ending → click returns to library */
        if (key == PAD_CLICK) open_library();
        return true;
    }
    switch (key) {
    case PAD_DOWN: s_sel = (s_sel + 1) % n; render_page(false); break;  /* next answer */
    case PAD_CLICK: {                                                   /* validate */
        const cJSON *choices = cJSON_GetObjectItem(cur_passage(), "choices");
        const cJSON *g = cJSON_GetObjectItem(cJSON_GetArrayItem(choices, s_sel), "goto");
        if (cJSON_IsString(g)) {
            ESP_LOGI(TAG, "validate #%d -> '%s'", s_sel, g->valuestring);
            enter_passage(g->valuestring);
        }
        break;
    }
    default: break;
    }
    return true;
}

static bool gamebook_key_hook(uint8_t key)
{
    switch (s_mode) {
    case GB_LIBRARY: return library_key(key);
    case GB_STORY:   return story_key(key);
    default:         return false;   /* not active → let the shell have it */
    }
}

// ── Public API ────────────────────────────────────────────────────────────────

esp_err_t gamebook_start(void)
{
    return open_library();
}

void gamebook_stop(void)
{
    if (s_mode == GB_OFF) return;
    s_mode = GB_OFF;
    free_story();
    if (s_lib_root) { cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL; }
    display_ui_gamebook_hide();
    display_ui_library_hide();
    ESP_LOGI(TAG, "stopped");
}

void gamebook_init(void)
{
    /* Keep the large book trees in PSRAM — an expanded book is ~80 KB of JSON,
     * whose parsed node tree would exhaust the internal heap on this firmware. */
    static cJSON_Hooks hooks = {
        .malloc_fn = gb_malloc,
        .free_fn   = gb_free,
    };
    cJSON_InitHooks(&hooks);

    /* Only register the pad hook here — the SD and media_manager aren't up yet
     * at this point in boot. main.c calls gamebook_start() once they are, to
     * boot into the library. */
    display_ui_set_key_hook(gamebook_key_hook);
    ESP_LOGI(TAG, "key hook installed");
}
