// plip_gamebook.c — see plip_gamebook.h. Audio CYOA for the PLIP phone.
#include "plip_gamebook.h"

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

#include "cJSON.h"
#include "esp_log.h"
#include "esp_random.h"

#include "audio.h"

static const char *TAG = "plip_gb";

#define GB_DIR      "/sdcard/gamebook"
#define GB_LIBRARY  GB_DIR "/library.json"
#define GB_MENU_WAV GB_DIR "/menu.wav"
#define GB_JSON_MAX (64 * 1024)

typedef enum { GB_OFF, GB_MENU, GB_STORY } gb_mode_t;

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 char     s_current[48] = {0};

static gb_mode_t s_mode = GB_OFF;

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

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); return NULL; }
    char *buf = malloc((size_t)sz + 1);
    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);
    free(buf);
    if (!root) ESP_LOGW(TAG, "malformed JSON: %s", path);
    return root;
}

static void play_wav(const char *file)   // file = bare name under GB_DIR
{
    char path[96];
    snprintf(path, sizeof(path), "%s/%s", GB_DIR, file);
    audio_stop();                 // interrupt any current narration
    audio_play_async(path);
}

bool plip_gamebook_available(void)
{
    if (!audio_ensure_sd()) return false;
    FILE *f = fopen(GB_LIBRARY, "rb");
    if (!f) return false;
    fclose(f);
    return true;
}

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

static void enter_menu(void)
{
    free_story();
    s_mode = GB_MENU;
    audio_stop();
    audio_play_async(GB_MENU_WAV);   // absolute path
    ESP_LOGI(TAG, "menu (%d stories)", s_lib_n);
}

// Play a passage by id (its WAV narrates the text + numbered choices).
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; }
    snprintf(s_current, sizeof(s_current), "%s", pid);
    const cJSON *wav = cJSON_GetObjectItem(p, "wav");
    if (cJSON_IsString(wav) && wav->valuestring[0]) play_wav(wav->valuestring);
    const cJSON *ch = cJSON_GetObjectItem(p, "choices");
    ESP_LOGI(TAG, "passage '%s' (%d choices)", pid,
             cJSON_IsArray(ch) ? cJSON_GetArraySize(ch) : 0);
}

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;
    }
    free_story();
    s_book = book;
    s_passages = (cJSON *)passages;
    s_mode = GB_STORY;
    ESP_LOGI(TAG, "load book #%d @ '%s'", idx, start->valuestring);
    enter_passage(start->valuestring);
}

void plip_gamebook_begin(void)
{
    plip_gamebook_end();   // clean any previous run

    s_lib_root = load_json(GB_LIBRARY);
    if (!s_lib_root) 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;
        return;
    }
    s_lib_n = cJSON_GetArraySize(s_lib);
    audio_pa_set(true);    // earpiece amplifier on
    enter_menu();
}

void plip_gamebook_begin_random(void)
{
    plip_gamebook_end();   // clean any previous run

    s_lib_root = load_json(GB_LIBRARY);
    if (!s_lib_root) 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;
        return;
    }
    s_lib_n = cJSON_GetArraySize(s_lib);
    audio_pa_set(true);    // earpiece amplifier on
    int idx = (int)(esp_random() % (uint32_t)s_lib_n);
    ESP_LOGI(TAG, "auto-start random story #%d/%d", idx, s_lib_n);
    load_book(idx);        // straight into the story, no menu
}

void plip_gamebook_feed_digit(int d)
{
    if (s_mode == GB_MENU) {
        if (d >= 1 && d <= s_lib_n) load_book(d - 1);
        return;
    }
    if (s_mode == GB_STORY) {
        const cJSON *p = cJSON_GetObjectItem(s_passages, s_current);
        const cJSON *ch = cJSON_GetObjectItem(p, "choices");
        int n = cJSON_IsArray(ch) ? cJSON_GetArraySize(ch) : 0;
        if (n == 0) {                 // ending: 0 (or any) → back to menu
            enter_menu();
            return;
        }
        if (d >= 1 && d <= n) {
            const cJSON *g = cJSON_GetObjectItem(cJSON_GetArrayItem(ch, d - 1),
                                                 "goto");
            if (cJSON_IsString(g)) {
                ESP_LOGI(TAG, "choice %d -> '%s'", d, g->valuestring);
                enter_passage(g->valuestring);
            }
        }
        // out-of-range digit: ignore (player can dial again)
    }
}

void plip_gamebook_end(void)
{
    if (s_mode == GB_OFF && !s_lib_root && !s_book) return;
    s_mode = GB_OFF;
    audio_stop();
    free_story();
    if (s_lib_root) { cJSON_Delete(s_lib_root); s_lib_root = NULL; s_lib = NULL; }
    ESP_LOGI(TAG, "ended");
}
