/**
 * DreamStack Panel IR Runtime — Implementation
 *
 * Parses Panel IR JSON (using cJSON) and creates LVGL 9 widgets.
 * This is the C port of the browser-based panel previewer logic.
 *
 * Key features:
 *   - Signal table with text template expansion ({0} → value)
 *   - Reactive updates: signal change → refresh bound labels
 *   - Event dispatch: button click → action opcode → ESP-NOW
 *   - Timer execution from IR timers[] array
 */

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "esp_log.h"
#include "cJSON.h"
#include "lvgl.h"

#include "ds_runtime.h"

static const char *TAG = "ds-runtime";

// ─── Signal Table ───
static ds_signal_t s_signals[DS_MAX_SIGNALS];
static uint16_t    s_signal_count = 0;

// ─── Timer Table ───
static ds_timer_t  s_timers[DS_MAX_TIMERS];
static uint8_t     s_timer_count = 0;

// ─── Text Binding Table ───
// Maps LVGL label objects to their IR text templates
typedef struct {
    lv_obj_t   *label;       // LVGL label widget
    char        template[128]; // Text template with {N} placeholders
    bool        used;
} ds_binding_t;

static ds_binding_t s_bindings[DS_MAX_BINDINGS];
static uint16_t     s_binding_count = 0;

// ─── Parent and Callback ───
static lv_obj_t      *s_parent = NULL;
static lv_obj_t      *s_root = NULL;
static ds_action_cb_t s_action_cb = NULL;

// ─── Forward Declarations ───
static lv_obj_t *build_node(cJSON *node, lv_obj_t *parent);
static void expand_template(const char *tpl, char *out, size_t out_len);
static void execute_action(cJSON *action);
static void refresh_bindings(void);

// ─── Action opcodes (match IR spec) ───
#define OP_INC     1
#define OP_DEC     2
#define OP_ADD     3
#define OP_SUB     4
#define OP_SET     5
#define OP_TOGGLE  6

static uint8_t parse_op(const char *op_str) {
    if (strcmp(op_str, "inc") == 0) return OP_INC;
    if (strcmp(op_str, "dec") == 0) return OP_DEC;
    if (strcmp(op_str, "add") == 0) return OP_ADD;
    if (strcmp(op_str, "sub") == 0) return OP_SUB;
    if (strcmp(op_str, "set") == 0) return OP_SET;
    if (strcmp(op_str, "toggle") == 0) return OP_TOGGLE;
    return 0;
}

// ─── Timer Callback ───
static void timer_cb(lv_timer_t *timer) {
    ds_timer_t *t = (ds_timer_t *)lv_timer_get_user_data(timer);
    if (!t) return;

    int32_t val = s_signals[t->action_sig].i;
    switch (t->action_op) {
        case OP_INC: val++; break;
        case OP_DEC: val--; break;
        case OP_ADD: val += t->action_val; break;
        case OP_SUB: val -= t->action_val; break;
        case OP_SET: val = t->action_val; break;
        case OP_TOGGLE: val = val ? 0 : 1; break;
    }
    ds_signal_update(t->action_sig, val);
}

// ─── Button Event Handler ───
typedef struct {
    cJSON *action;   // JSON action object (kept alive while UI exists)
    uint8_t node_id;
} btn_user_data_t;

static void btn_click_cb(lv_event_t *e) {
    btn_user_data_t *ud = (btn_user_data_t *)lv_event_get_user_data(e);
    if (ud && ud->action) {
        execute_action(ud->action);
        if (s_action_cb) {
            s_action_cb(ud->node_id, 0); // notify ESP-NOW
        }
    }
}

// ─── Build Helpers ───

static lv_obj_t *build_container(cJSON *node, lv_obj_t *parent, bool is_row) {
    lv_obj_t *cont = lv_obj_create(parent);
    lv_obj_set_size(cont, LV_PCT(100), LV_SIZE_CONTENT);
    lv_obj_set_flex_flow(cont, is_row ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
    lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
    lv_obj_set_style_border_width(cont, 0, 0);
    lv_obj_set_style_pad_all(cont, 4, 0);

    cJSON *gap = cJSON_GetObjectItem(node, "gap");
    if (gap) {
        lv_obj_set_style_pad_gap(cont, gap->valueint, 0);
    }

    cJSON *children = cJSON_GetObjectItem(node, "c");
    if (children && cJSON_IsArray(children)) {
        cJSON *child;
        cJSON_ArrayForEach(child, children) {
            build_node(child, cont);
        }
    }
    return cont;
}

static lv_obj_t *build_label(cJSON *node, lv_obj_t *parent) {
    lv_obj_t *lbl = lv_label_create(parent);
    cJSON *text = cJSON_GetObjectItem(node, "text");
    if (text && text->valuestring) {
        char expanded[256];
        expand_template(text->valuestring, expanded, sizeof(expanded));
        lv_label_set_text(lbl, expanded);

        // Register binding if template contains {N}
        if (strchr(text->valuestring, '{') && s_binding_count < DS_MAX_BINDINGS) {
            ds_binding_t *b = &s_bindings[s_binding_count++];
            b->label = lbl;
            strncpy(b->template, text->valuestring, sizeof(b->template) - 1);
            b->used = true;
        }
    }

    cJSON *size = cJSON_GetObjectItem(node, "size");
    if (size) {
        lv_obj_set_style_text_font(lbl,
            size->valueint >= 24 ? &lv_font_montserrat_24 :
            size->valueint >= 18 ? &lv_font_montserrat_18 :
            size->valueint >= 14 ? &lv_font_montserrat_14 :
            &lv_font_montserrat_12, 0);
    }

    return lbl;
}

static lv_obj_t *build_button(cJSON *node, lv_obj_t *parent) {
    lv_obj_t *btn = lv_btn_create(parent);
    lv_obj_t *lbl = lv_label_create(btn);

    cJSON *text = cJSON_GetObjectItem(node, "text");
    if (text && text->valuestring) {
        char expanded[256];
        expand_template(text->valuestring, expanded, sizeof(expanded));
        lv_label_set_text(lbl, expanded);

        // Register binding for button text too
        if (strchr(text->valuestring, '{') && s_binding_count < DS_MAX_BINDINGS) {
            ds_binding_t *b = &s_bindings[s_binding_count++];
            b->label = lbl;
            strncpy(b->template, text->valuestring, sizeof(b->template) - 1);
            b->used = true;
        }
    }

    cJSON *on = cJSON_GetObjectItem(node, "on");
    if (on) {
        cJSON *click = cJSON_GetObjectItem(on, "click");
        if (click) {
            btn_user_data_t *ud = malloc(sizeof(btn_user_data_t));
            ud->action = click;  // keep reference (IR JSON stays in memory)
            cJSON *id_obj = cJSON_GetObjectItem(node, "id");
            ud->node_id = id_obj ? id_obj->valueint : 0;
            lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, ud);
        }
    }

    return btn;
}

static lv_obj_t *build_slider(cJSON *node, lv_obj_t *parent) {
    lv_obj_t *slider = lv_slider_create(parent);
    lv_obj_set_width(slider, LV_PCT(80));

    cJSON *min = cJSON_GetObjectItem(node, "min");
    cJSON *max = cJSON_GetObjectItem(node, "max");
    if (min) lv_slider_set_range(slider, min->valueint, max ? max->valueint : 100);

    cJSON *bind = cJSON_GetObjectItem(node, "bind");
    if (bind) {
        int sid = bind->valueint;
        lv_slider_set_value(slider, s_signals[sid].i, LV_ANIM_OFF);
        // TODO: add slider change event → ds_signal_update
    }

    return slider;
}

static lv_obj_t *build_panel(cJSON *node, lv_obj_t *parent) {
    lv_obj_t *pnl = lv_obj_create(parent);
    lv_obj_set_size(pnl, LV_PCT(100), LV_SIZE_CONTENT);
    lv_obj_set_flex_flow(pnl, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_style_pad_all(pnl, 12, 0);
    lv_obj_set_style_radius(pnl, 8, 0);

    cJSON *title = cJSON_GetObjectItem(node, "text");
    if (title && title->valuestring) {
        lv_obj_t *t = lv_label_create(pnl);
        lv_label_set_text(t, title->valuestring);
        lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0);
    }

    cJSON *children = cJSON_GetObjectItem(node, "c");
    if (children && cJSON_IsArray(children)) {
        cJSON *child;
        cJSON_ArrayForEach(child, children) {
            build_node(child, pnl);
        }
    }

    return pnl;
}

// ─── Node Dispatcher ───
static lv_obj_t *build_node(cJSON *node, lv_obj_t *parent) {
    if (!node) return NULL;

    cJSON *type = cJSON_GetObjectItem(node, "t");
    if (!type || !type->valuestring) return NULL;

    const char *t = type->valuestring;

    if (strcmp(t, "col") == 0) return build_container(node, parent, false);
    if (strcmp(t, "row") == 0) return build_container(node, parent, true);
    if (strcmp(t, "lbl") == 0) return build_label(node, parent);
    if (strcmp(t, "btn") == 0) return build_button(node, parent);
    if (strcmp(t, "sld") == 0) return build_slider(node, parent);
    if (strcmp(t, "pnl") == 0) return build_panel(node, parent);
    // stk, inp, sw, bar, img — add as needed

    ESP_LOGW(TAG, "Unknown node type: %s", t);
    return NULL;
}

// ─── Template Expansion ───
// Replace {N} with signal values: "{2}: {0}°F" → "Kitchen: 72°F"
static void expand_template(const char *tpl, char *out, size_t out_len) {
    size_t pos = 0;
    while (*tpl && pos < out_len - 1) {
        if (*tpl == '{') {
            tpl++;
            int sig_id = 0;
            while (*tpl >= '0' && *tpl <= '9') {
                sig_id = sig_id * 10 + (*tpl - '0');
                tpl++;
            }
            if (*tpl == '}') tpl++;

            if (sig_id < DS_MAX_SIGNALS && s_signals[sig_id].used) {
                int written = snprintf(out + pos, out_len - pos, "%d",
                                       (int)s_signals[sig_id].i);
                if (written > 0) pos += written;
            }
        } else {
            out[pos++] = *tpl++;
        }
    }
    out[pos] = '\0';
}

// ─── Action Executor ───
static void execute_action(cJSON *action) {
    if (!action) return;

    // Handle arrays of actions
    if (cJSON_IsArray(action)) {
        cJSON *item;
        cJSON_ArrayForEach(item, action) {
            execute_action(item);
        }
        return;
    }

    cJSON *op_json = cJSON_GetObjectItem(action, "op");
    cJSON *s_json = cJSON_GetObjectItem(action, "s");
    if (!op_json || !s_json) return;

    uint8_t op = parse_op(op_json->valuestring);
    uint16_t sid = s_json->valueint;
    if (sid >= DS_MAX_SIGNALS) return;

    int32_t val = s_signals[sid].i;
    cJSON *v_json = cJSON_GetObjectItem(action, "v");
    int32_t v = v_json ? v_json->valueint : 0;

    switch (op) {
        case OP_INC: val++; break;
        case OP_DEC: val--; break;
        case OP_ADD: val += v; break;
        case OP_SUB: val -= v; break;
        case OP_SET: val = v; break;
        case OP_TOGGLE: val = val ? 0 : 1; break;
    }

    ds_signal_update(sid, val);
}

// ─── Refresh Bindings ───
// Called after any signal change to update bound labels
static void refresh_bindings(void) {
    for (int i = 0; i < s_binding_count; i++) {
        if (!s_bindings[i].used) continue;
        char expanded[256];
        expand_template(s_bindings[i].template, expanded, sizeof(expanded));
        lv_label_set_text(s_bindings[i].label, expanded);
    }
}

// ─── Public API ───

esp_err_t ds_runtime_init(void *parent, ds_action_cb_t action_cb) {
    s_parent = (lv_obj_t *)parent;
    s_action_cb = action_cb;
    s_root = NULL;
    s_signal_count = 0;
    s_binding_count = 0;
    s_timer_count = 0;
    memset(s_signals, 0, sizeof(s_signals));
    memset(s_bindings, 0, sizeof(s_bindings));
    memset(s_timers, 0, sizeof(s_timers));

    ESP_LOGI(TAG, "Runtime initialized");
    return ESP_OK;
}

esp_err_t ds_ui_build(const char *ir_json, size_t length) {
    // Destroy previous UI
    ds_ui_destroy();

    cJSON *ir = cJSON_ParseWithLength(ir_json, length);
    if (!ir) {
        ESP_LOGE(TAG, "Failed to parse IR JSON");
        return ESP_ERR_INVALID_ARG;
    }

    // Load signals
    cJSON *signals = cJSON_GetObjectItem(ir, "signals");
    if (signals && cJSON_IsArray(signals)) {
        cJSON *sig;
        cJSON_ArrayForEach(sig, signals) {
            cJSON *id = cJSON_GetObjectItem(sig, "id");
            cJSON *v = cJSON_GetObjectItem(sig, "v");
            if (id && id->valueint < DS_MAX_SIGNALS) {
                int sid = id->valueint;
                s_signals[sid].i = v ? v->valueint : 0;
                s_signals[sid].type = DS_SIG_INT;
                s_signals[sid].used = true;
                s_signal_count++;
            }
        }
    }

    // Build UI tree
    cJSON *root = cJSON_GetObjectItem(ir, "root");
    if (root) {
        s_root = build_node(root, s_parent);
    }

    // Load timers
    cJSON *timers = cJSON_GetObjectItem(ir, "timers");
    if (timers && cJSON_IsArray(timers)) {
        cJSON *t;
        cJSON_ArrayForEach(t, timers) {
            if (s_timer_count >= DS_MAX_TIMERS) break;
            cJSON *ms = cJSON_GetObjectItem(t, "ms");
            cJSON *action = cJSON_GetObjectItem(t, "action");
            if (ms && action) {
                ds_timer_t *timer = &s_timers[s_timer_count];
                timer->ms = ms->valueint;

                cJSON *op = cJSON_GetObjectItem(action, "op");
                cJSON *s = cJSON_GetObjectItem(action, "s");
                cJSON *v = cJSON_GetObjectItem(action, "v");
                timer->action_op = op ? parse_op(op->valuestring) : 0;
                timer->action_sig = s ? s->valueint : 0;
                timer->action_val = v ? v->valueint : 0;

                timer->timer = lv_timer_create(timer_cb, timer->ms, timer);
                s_timer_count++;
                ESP_LOGI(TAG, "Timer: every %dms → op %d on s%d",
                         (int)timer->ms, timer->action_op, timer->action_sig);
            }
        }
    }

    ESP_LOGI(TAG, "UI built: %d signals, %d bindings, %d timers",
             s_signal_count, s_binding_count, s_timer_count);

    // Keep IR JSON alive for button action references
    // (cJSON_Delete would invalidate action pointers)
    // TODO: deep-copy actions to avoid this leak

    return ESP_OK;
}

void ds_ui_destroy(void) {
    // Stop timers
    for (int i = 0; i < s_timer_count; i++) {
        if (s_timers[i].timer) {
            lv_timer_delete((lv_timer_t *)s_timers[i].timer);
            s_timers[i].timer = NULL;
        }
    }
    s_timer_count = 0;

    // Clear bindings
    s_binding_count = 0;
    memset(s_bindings, 0, sizeof(s_bindings));

    // Destroy LVGL tree
    if (s_root) {
        lv_obj_del(s_root);
        s_root = NULL;
    }

    ESP_LOGI(TAG, "UI destroyed");
}

void ds_signal_update(uint16_t signal_id, int32_t value) {
    if (signal_id >= DS_MAX_SIGNALS) return;
    s_signals[signal_id].i = value;
    s_signals[signal_id].used = true;

    // Refresh all bound labels
    refresh_bindings();
}

uint16_t ds_signal_count(void) {
    return s_signal_count;
}

int32_t ds_signal_get(uint16_t signal_id) {
    if (signal_id >= DS_MAX_SIGNALS) return 0;
    return s_signals[signal_id].i;
}
