// mlnl.c - parse the optional `mLnL` RIFF chunk from a WAV file (schema v2).
// Payload is UTF-8 JSON Lines, one flat object per line.
#include "mlnl.h"

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

// ============================================================================
// RIFF walker — find the `mLnL` chunk payload.
// ============================================================================

static unsigned int ReadU32LE(const unsigned char* p)
{
    return (unsigned int)p[0] | ((unsigned int)p[1] << 8) |
           ((unsigned int)p[2] << 16) | ((unsigned int)p[3] << 24);
}

// Returns a malloc'd payload buffer (NUL-terminated for splitline convenience)
// and sets *outLen. NULL if the file isn't WAV or has no mLnL chunk.
static char* FindMlnlChunk(const char* path, size_t* outLen)
{
    FILE* f = fopen(path, "rb");
    if (!f) return NULL;

    unsigned char hdr[12];
    if (fread(hdr, 1, 12, f) != 12) { fclose(f); return NULL; }
    if (memcmp(hdr, "RIFF", 4) != 0 || memcmp(hdr + 8, "WAVE", 4) != 0) {
        fclose(f); return NULL;
    }

    for (;;) {
        unsigned char ch[8];
        size_t got = fread(ch, 1, 8, f);
        if (got != 8) break;
        unsigned int sz = ReadU32LE(ch + 4);
        if (memcmp(ch, "mLnL", 4) == 0) {
            // Cap at 64 MiB to avoid a corrupt size eating memory.
            if (sz > 64u * 1024u * 1024u) { fclose(f); return NULL; }
            char* buf = (char*)malloc((size_t)sz + 1);
            if (!buf) { fclose(f); return NULL; }
            if (fread(buf, 1, sz, f) != sz) { free(buf); fclose(f); return NULL; }
            buf[sz] = '\0';
            *outLen = sz;
            fclose(f);
            return buf;
        }
        // Skip the payload (+ 1 pad byte if size is odd).
        long skip = (long)sz + (long)(sz & 1u);
        if (fseek(f, skip, SEEK_CUR) != 0) break;
    }
    fclose(f);
    return NULL;
}

// ============================================================================
// Tiny JSON-object-per-line parser. Flat objects with scalar values.
// Nested objects/arrays in unknown fields are tolerated by being skipped.
// ============================================================================

typedef struct {
    const char* s;
    int pos;
    int len;
} Scanner;

static void SkipWs(Scanner* sc)
{
    while (sc->pos < sc->len && (sc->s[sc->pos] == ' ' || sc->s[sc->pos] == '\t')) sc->pos++;
}

static bool ReadString(Scanner* sc, char* out, int outCap)
{
    if (sc->pos >= sc->len || sc->s[sc->pos] != '"') return false;
    sc->pos++;
    int w = 0;
    while (sc->pos < sc->len && sc->s[sc->pos] != '"') {
        char c = sc->s[sc->pos++];
        if (c == '\\' && sc->pos < sc->len) {
            char e = sc->s[sc->pos++];
            switch (e) {
                case 'n': c = '\n'; break;
                case 't': c = '\t'; break;
                case 'r': c = '\r'; break;
                case '"': case '\\': case '/': c = e; break;
                case 'u': {
                    // Bare ASCII passthrough — drop the 4 hex digits, write '?'.
                    if (sc->pos + 4 <= sc->len) sc->pos += 4;
                    c = '?';
                    break;
                }
                default: c = e; break;
            }
        }
        if (w + 1 < outCap) out[w++] = c;
    }
    if (sc->pos >= sc->len) return false;
    sc->pos++;  // consume closing quote
    if (w < outCap) out[w] = '\0';
    else if (outCap > 0) out[outCap - 1] = '\0';
    return true;
}

static void SkipValue(Scanner* sc)
{
    SkipWs(sc);
    if (sc->pos >= sc->len) return;
    char c = sc->s[sc->pos];
    if (c == '"') {
        char trash[4];
        ReadString(sc, trash, sizeof(trash));
        return;
    }
    if (c == '{' || c == '[') {
        char open = c, close = (c == '{') ? '}' : ']';
        int depth = 1;
        sc->pos++;
        while (sc->pos < sc->len && depth > 0) {
            char k = sc->s[sc->pos];
            if (k == '"') {
                char trash[4];
                ReadString(sc, trash, sizeof(trash));
                continue;
            }
            if (k == open) depth++;
            else if (k == close) depth--;
            sc->pos++;
        }
        return;
    }
    while (sc->pos < sc->len) {
        char k = sc->s[sc->pos];
        if (k == ',' || k == '}' || k == ']') break;
        sc->pos++;
    }
}

static bool ReadNumber(Scanner* sc, double* out)
{
    SkipWs(sc);
    int start = sc->pos;
    if (sc->pos < sc->len && (sc->s[sc->pos] == '-' || sc->s[sc->pos] == '+')) sc->pos++;
    bool any = false;
    while (sc->pos < sc->len) {
        char c = sc->s[sc->pos];
        if ((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') {
            sc->pos++; any = true;
        } else break;
    }
    if (!any) return false;
    char tmp[64];
    int n = sc->pos - start;
    if (n >= (int)sizeof(tmp)) n = (int)sizeof(tmp) - 1;
    memcpy(tmp, sc->s + start, n);
    tmp[n] = '\0';
    *out = strtod(tmp, NULL);
    return true;
}

static MlnlKind LookupKind(const char* s)
{
    if (strcmp(s, "channel_up") == 0)       return MLNL_KIND_CHANNEL_UP;
    if (strcmp(s, "channel_down") == 0)     return MLNL_KIND_CHANNEL_DOWN;
    if (strcmp(s, "control") == 0)          return MLNL_KIND_CONTROL;
    if (strcmp(s, "tx_frame") == 0)         return MLNL_KIND_TX_FRAME;
    if (strcmp(s, "tx_burst") == 0)         return MLNL_KIND_TX_BURST;
    if (strcmp(s, "assertion_passed") == 0) return MLNL_KIND_ASSERTION_PASSED;
    if (strcmp(s, "assertion_failed") == 0) return MLNL_KIND_ASSERTION_FAILED;
    if (strcmp(s, "impairment_fire") == 0)  return MLNL_KIND_IMPAIRMENT_FIRE;
    if (strcmp(s, "gain_change") == 0)      return MLNL_KIND_GAIN_CHANGE;
    return MLNL_KIND_UNKNOWN;
}

const char* MlnlKindName(MlnlKind k)
{
    switch (k) {
        case MLNL_KIND_CHANNEL_UP:        return "channel_up";
        case MLNL_KIND_CHANNEL_DOWN:      return "channel_down";
        case MLNL_KIND_CONTROL:           return "control";
        case MLNL_KIND_TX_FRAME:          return "tx_frame";
        case MLNL_KIND_TX_BURST:          return "tx_burst";
        case MLNL_KIND_ASSERTION_PASSED:  return "assertion_passed";
        case MLNL_KIND_ASSERTION_FAILED:  return "assertion_failed";
        case MLNL_KIND_IMPAIRMENT_FIRE:   return "impairment_fire";
        case MLNL_KIND_GAIN_CHANGE:       return "gain_change";
        default: return "?";
    }
}

// Parse a "#RRGGBB" string into r/g/b. Returns false on malformed input.
static bool ParseHexColor(const char* s, unsigned char* r, unsigned char* g, unsigned char* b)
{
    if (!s || s[0] != '#') return false;
    unsigned int v = 0;
    int n = 0;
    for (int i = 1; i < 7 && s[i]; i++) {
        char c = s[i];
        unsigned int d;
        if (c >= '0' && c <= '9') d = c - '0';
        else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
        else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
        else return false;
        v = (v << 4) | d;
        n++;
    }
    if (n != 6) return false;
    *r = (v >> 16) & 0xff;
    *g = (v >> 8) & 0xff;
    *b = v & 0xff;
    return true;
}

// Returns true if the line produced an event for the array; false if it was
// the truncation sentinel or empty/malformed.
static bool ParseLine(const char* line, int n, MlnlEvent* ev, bool* truncatedOut)
{
    Scanner sc = { .s = line, .pos = 0, .len = n };
    SkipWs(&sc);
    if (sc.pos >= sc.len || sc.s[sc.pos] != '{') return false;
    sc.pos++;

    memset(ev, 0, sizeof(*ev));

    // f_center/f_bw resolve into f_lo/f_hi after the line is fully parsed.
    double f_center = 0, f_bw = 0;
    bool have_f_lo = false, have_f_hi = false;
    bool have_f_center = false, have_f_bw = false;

    for (;;) {
        SkipWs(&sc);
        if (sc.pos >= sc.len) return false;
        if (sc.s[sc.pos] == '}') { sc.pos++; break; }
        if (sc.s[sc.pos] == ',') { sc.pos++; continue; }

        char key[32] = {0};
        if (!ReadString(&sc, key, sizeof(key))) return false;
        SkipWs(&sc);
        if (sc.pos >= sc.len || sc.s[sc.pos] != ':') return false;
        sc.pos++;
        SkipWs(&sc);

        if (strcmp(key, "t_start") == 0) {
            ReadNumber(&sc, &ev->t_start);
        } else if (strcmp(key, "t_end") == 0) {
            ReadNumber(&sc, &ev->t_end);
        } else if (strcmp(key, "kind") == 0) {
            ReadString(&sc, ev->kindStr, sizeof(ev->kindStr));
            ev->kind = LookupKind(ev->kindStr);
        } else if (strcmp(key, "f_lo") == 0) {
            ReadNumber(&sc, &ev->f_lo_hz); have_f_lo = true;
        } else if (strcmp(key, "f_hi") == 0) {
            ReadNumber(&sc, &ev->f_hi_hz); have_f_hi = true;
        } else if (strcmp(key, "f_center") == 0) {
            ReadNumber(&sc, &f_center); have_f_center = true;
        } else if (strcmp(key, "f_bw") == 0) {
            ReadNumber(&sc, &f_bw); have_f_bw = true;
        } else if (strcmp(key, "note") == 0) {
            ReadString(&sc, ev->note, sizeof(ev->note));
            ev->has_note = true;
        } else if (strcmp(key, "color") == 0) {
            char hex[16] = {0};
            ReadString(&sc, hex, sizeof(hex));
            if (ParseHexColor(hex, &ev->colorR, &ev->colorG, &ev->colorB))
                ev->has_color = true;
        } else if (strcmp(key, "node") == 0) {
            double v = 0;
            if (ReadNumber(&sc, &v)) { ev->node = (unsigned int)v; ev->has_node = true; }
            else SkipValue(&sc);
        } else if (strcmp(key, "command") == 0) {
            ReadString(&sc, ev->command, sizeof(ev->command));
            ev->has_command = true;
        } else if (strcmp(key, "name") == 0) {
            ReadString(&sc, ev->name, sizeof(ev->name));
            ev->has_name = true;
        } else if (strcmp(key, "reason") == 0) {
            ReadString(&sc, ev->reason, sizeof(ev->reason));
            ev->has_reason = true;
        } else if (strcmp(key, "peak") == 0) {
            ReadNumber(&sc, &ev->peak); ev->has_stats = true;
        } else if (strcmp(key, "rms") == 0) {
            ReadNumber(&sc, &ev->rms); ev->has_stats = true;
        } else if (strcmp(key, "papr_db") == 0) {
            ReadNumber(&sc, &ev->papr_db); ev->has_stats = true;
        } else if (strcmp(key, "slack_s") == 0) {
            ReadNumber(&sc, &ev->slack_s); ev->has_slack = true;
        } else if (strcmp(key, "id") == 0) {
            double v = 0;
            if (ReadNumber(&sc, &v)) { ev->id = (unsigned int)v; ev->has_id = true; }
            else SkipValue(&sc);
        } else if (strcmp(key, "label") == 0) {
            ReadString(&sc, ev->label, sizeof(ev->label));
            ev->has_label = (ev->label[0] != '\0');
        } else if (strcmp(key, "seq") == 0) {
            double v = 0;
            if (ReadNumber(&sc, &v)) ev->seq = (int)v;
            else SkipValue(&sc);
        } else if (strcmp(key, "n") == 0) {
            double v = 0;
            if (ReadNumber(&sc, &v)) { ev->nFrames = (int)v; ev->has_seqn = true; }
            else SkipValue(&sc);
        } else if (strcmp(key, "frame") == 0) {
            ReadString(&sc, ev->frame, sizeof(ev->frame));
            ev->has_frame = (ev->frame[0] != '\0');
        } else if (strcmp(key, "ch") == 0) {
            ReadString(&sc, ev->ch, sizeof(ev->ch));
            ev->has_ch = (ev->ch[0] != '\0');
        } else if (strcmp(key, "rate") == 0) {
            ReadString(&sc, ev->rate, sizeof(ev->rate));
            ev->has_rate = (ev->rate[0] != '\0');
        } else if (strcmp(key, "sched_offset_ms") == 0) {
            if (ReadNumber(&sc, &ev->sched_offset_ms)) ev->has_sched_offset = true;
            else SkipValue(&sc);
        } else if (strcmp(key, "truncated") == 0) {
            if (sc.pos < sc.len && (sc.s[sc.pos] == 't' || sc.s[sc.pos] == 'T'))
                *truncatedOut = true;
            SkipValue(&sc);
        } else {
            // Unknown field — swallow scalar/composite alike.
            SkipValue(&sc);
        }
    }

    // Resolve frequency: prefer f_lo/f_hi if present, else expand f_center/f_bw.
    if (have_f_lo && have_f_hi) {
        ev->has_freq = true;
    } else if (have_f_center && have_f_bw) {
        ev->f_lo_hz = f_center - f_bw * 0.5;
        ev->f_hi_hz = f_center + f_bw * 0.5;
        ev->has_freq = true;
    }
    if (ev->has_freq && ev->f_hi_hz < ev->f_lo_hz) {
        double t = ev->f_lo_hz; ev->f_lo_hz = ev->f_hi_hz; ev->f_hi_hz = t;
    }

    // The truncation line carries no kind; treat it as a non-event so it
    // doesn't bloat the events array (the truncated flag still propagates).
    if (ev->kindStr[0] == '\0') return false;
    return true;
}

// ============================================================================
// Public API
// ============================================================================

bool LoadMlnlFromWav(const char* path, MlnlAnnotations* out)
{
    memset(out, 0, sizeof(*out));
    size_t len = 0;
    char* payload = FindMlnlChunk(path, &len);
    if (!payload) return false;

    int lineCount = 0;
    for (size_t i = 0; i < len; i++) if (payload[i] == '\n') lineCount++;
    if (lineCount == 0) lineCount = 1;

    out->events = (MlnlEvent*)calloc(lineCount, sizeof(MlnlEvent));
    if (!out->events) { free(payload); return false; }

    int lineStart = 0;
    for (size_t i = 0; i <= len; i++) {
        if (i == len || payload[i] == '\n') {
            int n = (int)i - lineStart;
            if (n > 0 && payload[lineStart + n - 1] == '\r') n--;
            if (n > 0) {
                MlnlEvent ev = {0};
                if (ParseLine(payload + lineStart, n, &ev, &out->truncated)) {
                    out->events[out->eventCount++] = ev;
                    if (ev.kind >= 0 && ev.kind < MLNL_KIND_MAX)
                        out->kindPresent[ev.kind] = true;
                }
            }
            lineStart = (int)i + 1;
        }
    }

    out->loaded = true;
    free(payload);
    return true;
}

void FreeMlnl(MlnlAnnotations* a)
{
    if (!a) return;
    free(a->events);
    memset(a, 0, sizeof(*a));
}
