/*
 * el_lvgl.c — LVGL v9 backend for the el native widget system.
 *
 * This file implements the microcontroller/embedded widget layer that el_seed.c
 * calls through to when EL_TARGET_LVGL is defined.
 *
 * Architecture:
 *   el program (el code)
 *       → __widget_* C builtins in el_seed.c
 *           → el_lvgl_* C functions defined here
 *               → lv_obj_t widgets via LVGL v9
 *
 * Target platforms: ESP32, STM32, industrial panels, any system with 256KB+
 * RAM and an LVGL-compatible display driver. No OS required.
 *
 * Widget handles: every widget is assigned an int64_t slot index into
 * g_widgets[]. The el program holds these as opaque Int values.
 * Slot 0 is reserved; -1 = invalid handle.
 *
 * Window model: on embedded there is one screen. __window_create configures
 * lv_scr_act() as the root container. __window_show is a no-op (the screen
 * is always visible). __native_run_loop calls lv_task_handler() in a tight
 * loop — on RTOS this runs inside a dedicated task; on bare metal it IS the
 * main loop. The host application is responsible for initialising the display
 * driver and calling lv_tick_inc() before calling __native_run_loop.
 *
 * Callback dispatch:
 *   When EL_LVGL_NO_DLSYM is NOT defined (hosted Linux, testing):
 *     dlsym(RTLD_DEFAULT, fn_name) resolves the El function symbol at runtime.
 *   When EL_LVGL_NO_DLSYM IS defined (bare-metal ESP32/STM32):
 *     The caller must provide:
 *       el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
 *     which maps function names to function pointers via a compile-time table.
 *
 * Font mapping: LVGL v9 ships Montserrat in discrete sizes. __widget_set_font
 * maps the requested point size to the nearest available Montserrat variant.
 * Bold is approximated by stepping up two sizes (no separate bold face in the
 * default LVGL font set). Define EL_LVGL_CUSTOM_FONT to override font_select().
 *
 * Compile (hosted test build):
 *   gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
 *   # Then link with lvgl.a / lvgl source tree.
 *
 * Compile (bare-metal, no dynamic linker):
 *   arm-none-eabi-gcc -DEL_TARGET_LVGL -DEL_LVGL_NO_DLSYM \
 *       -I./lvgl el_lvgl.c -c -o el_lvgl.o
 */

#ifdef EL_TARGET_LVGL

#include "lvgl/lvgl.h"
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#ifndef EL_LVGL_NO_DLSYM
#include <dlfcn.h>
#endif

#include "el_runtime.h"

/* ── Callback dispatch macro ─────────────────────────────────────────────── */

#ifdef EL_LVGL_NO_DLSYM
/*
 * Bare-metal path. The application provides this function:
 *   el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
 * It maps string names → function pointers, typically via a switch on a hash
 * or a sorted table of {name, fn_ptr} pairs generated by elc.
 */
extern el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
#define EL_LVGL_CALL(fn_name, a, b)  el_lvgl_dispatch((fn_name), (a), (b))
#else
/*
 * Hosted path. dlsym resolves the El symbol at call time.
 * We use a compound-statement expression (GCC/Clang extension) to avoid
 * executing dlsym more than once per call.
 */
#define EL_LVGL_CALL(fn_name, a, b) \
    ({ \
        typedef el_val_t (*_el_fn_t)(el_val_t, el_val_t); \
        _el_fn_t _fn = (_el_fn_t)(uintptr_t)dlsym(RTLD_DEFAULT, (fn_name)); \
        _fn ? _fn((a), (b)) : (el_val_t)0; \
    })
#endif

/* ── Widget table ─────────────────────────────────────────────────────────── */

#define EL_LVGL_MAX_WIDGETS 4096

/*
 * Widget kinds — mirrors AppKit/GTK4 backends so future tooling can stay
 * consistent across all targets.
 */
typedef enum {
    EL_LVGL_FREE      = 0,
    EL_LVGL_WINDOW    = 1,
    EL_LVGL_VSTACK    = 2,
    EL_LVGL_HSTACK    = 3,
    EL_LVGL_ZSTACK    = 4,
    EL_LVGL_SCROLL    = 5,
    EL_LVGL_LABEL     = 6,
    EL_LVGL_BUTTON    = 7,  /* lv_btn_create; inner label at slot_btn_label */
    EL_LVGL_TEXTFIELD = 8,  /* lv_textarea, one-line */
    EL_LVGL_TEXTAREA  = 9,  /* lv_textarea, multiline */
    EL_LVGL_IMAGE     = 10,
    EL_LVGL_DIVIDER   = 11,
    EL_LVGL_SPACER    = 12,
} ElLvglKind;

/*
 * Per-slot state. Callback names are stored inline (256 bytes each) to avoid
 * heap allocation on targets with no malloc or fragmented heaps.
 */
typedef struct {
    ElLvglKind  kind;
    lv_obj_t   *obj;           /* primary LVGL object */
    lv_obj_t   *btn_label;     /* for EL_LVGL_BUTTON: inner lv_label child */
    char        cb_click[256];
    char        cb_change[256];
    char        cb_submit[256];
} ElLvglWidget;

static ElLvglWidget g_widgets[EL_LVGL_MAX_WIDGETS];

/* ── Slot helpers ─────────────────────────────────────────────────────────── */

static int64_t lvgl_slot_alloc(ElLvglKind kind, lv_obj_t *obj) {
    for (int i = 1; i < EL_LVGL_MAX_WIDGETS; i++) {
        if (g_widgets[i].kind == EL_LVGL_FREE) {
            g_widgets[i].kind      = kind;
            g_widgets[i].obj       = obj;
            g_widgets[i].btn_label = NULL;
            g_widgets[i].cb_click[0]  = '\0';
            g_widgets[i].cb_change[0] = '\0';
            g_widgets[i].cb_submit[0] = '\0';
            return (int64_t)i;
        }
    }
    return -1; /* table full */
}

static ElLvglWidget *lvgl_slot_get(int64_t handle) {
    if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return NULL;
    if (g_widgets[handle].kind == EL_LVGL_FREE)        return NULL;
    return &g_widgets[handle];
}

static void lvgl_slot_free(int64_t handle) {
    if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return;
    ElLvglWidget *w = &g_widgets[handle];
    w->kind      = EL_LVGL_FREE;
    w->obj       = NULL;
    w->btn_label = NULL;
    w->cb_click[0]  = '\0';
    w->cb_change[0] = '\0';
    w->cb_submit[0] = '\0';
}

/* ── Font selection ───────────────────────────────────────────────────────── */
/*
 * LVGL ships Montserrat in the following sizes (subset enabled by lv_conf.h):
 *   8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48
 *
 * We map the requested size to the nearest available size. Bold is
 * approximated by stepping up two sizes (no separate bold face in the default
 * font set). Define EL_LVGL_CUSTOM_FONT to replace this function entirely.
 */
#ifndef EL_LVGL_CUSTOM_FONT

static const lv_font_t *font_select(int size, int bold) {
    /* Step up two sizes for bold approximation. */
    if (bold) size += 4;

    /* Clamp to available range. */
    if (size <  8) size =  8;
    if (size > 48) size = 48;

    /* Round to nearest even size >= 8. */
    if (size % 2 != 0) size++;

    switch (size) {
#if LV_FONT_MONTSERRAT_8
        case  8:  return &lv_font_montserrat_8;
#endif
#if LV_FONT_MONTSERRAT_10
        case 10:  return &lv_font_montserrat_10;
#endif
#if LV_FONT_MONTSERRAT_12
        case 12:  return &lv_font_montserrat_12;
#endif
#if LV_FONT_MONTSERRAT_14
        case 14:  return &lv_font_montserrat_14;
#endif
#if LV_FONT_MONTSERRAT_16
        case 16:  return &lv_font_montserrat_16;
#endif
#if LV_FONT_MONTSERRAT_18
        case 18:  return &lv_font_montserrat_18;
#endif
#if LV_FONT_MONTSERRAT_20
        case 20:  return &lv_font_montserrat_20;
#endif
#if LV_FONT_MONTSERRAT_22
        case 22:  return &lv_font_montserrat_22;
#endif
#if LV_FONT_MONTSERRAT_24
        case 24:  return &lv_font_montserrat_24;
#endif
#if LV_FONT_MONTSERRAT_26
        case 26:  return &lv_font_montserrat_26;
#endif
#if LV_FONT_MONTSERRAT_28
        case 28:  return &lv_font_montserrat_28;
#endif
#if LV_FONT_MONTSERRAT_30
        case 30:  return &lv_font_montserrat_30;
#endif
#if LV_FONT_MONTSERRAT_32
        case 32:  return &lv_font_montserrat_32;
#endif
#if LV_FONT_MONTSERRAT_34
        case 34:  return &lv_font_montserrat_34;
#endif
#if LV_FONT_MONTSERRAT_36
        case 36:  return &lv_font_montserrat_36;
#endif
#if LV_FONT_MONTSERRAT_38
        case 38:  return &lv_font_montserrat_38;
#endif
#if LV_FONT_MONTSERRAT_40
        case 40:  return &lv_font_montserrat_40;
#endif
#if LV_FONT_MONTSERRAT_42
        case 42:  return &lv_font_montserrat_42;
#endif
#if LV_FONT_MONTSERRAT_44
        case 44:  return &lv_font_montserrat_44;
#endif
#if LV_FONT_MONTSERRAT_46
        case 46:  return &lv_font_montserrat_46;
#endif
#if LV_FONT_MONTSERRAT_48
        case 48:  return &lv_font_montserrat_48;
#endif
        default:
            /*
             * Requested size is not compiled in. Fall back to the default
             * theme font, which is guaranteed to be present.
             */
            return LV_FONT_DEFAULT;
    }
}

#endif /* EL_LVGL_CUSTOM_FONT */

/* ── Event callback ───────────────────────────────────────────────────────── */

/*
 * Single LVGL event callback used for all widget events. The user_data is
 * the slot index cast to (void*) via intptr_t — avoids heap allocation.
 *
 * Three event codes are handled:
 *   LV_EVENT_CLICKED      → cb_click  (buttons, any tappable widget)
 *   LV_EVENT_VALUE_CHANGED → cb_change (textarea, checkbox, etc.)
 *   LV_EVENT_READY        → cb_submit  (Enter pressed in textarea one-line mode)
 */
static void el_lvgl_event_cb(lv_event_t *e) {
    lv_event_code_t code = lv_event_get_code(e);
    intptr_t        slot = (intptr_t)lv_event_get_user_data(e);
    ElLvglWidget   *w    = lvgl_slot_get((int64_t)slot);
    if (!w) return;

    if (code == LV_EVENT_CLICKED && w->cb_click[0]) {
        EL_LVGL_CALL(w->cb_click, (el_val_t)slot, (el_val_t)0);
    }

    if (code == LV_EVENT_VALUE_CHANGED && w->cb_change[0]) {
        /*
         * Retrieve current text for textarea/textfield widgets so the handler
         * receives the updated value as its second argument.
         */
        const char *txt = "";
        lv_obj_t   *target = lv_event_get_target(e);
        if (w->kind == EL_LVGL_TEXTFIELD || w->kind == EL_LVGL_TEXTAREA) {
            txt = lv_textarea_get_text(target);
            if (!txt) txt = "";
        }
        EL_LVGL_CALL(w->cb_change, (el_val_t)slot,
                     (el_val_t)(uintptr_t)txt);
    }

    if (code == LV_EVENT_READY && w->cb_submit[0]) {
        /* LV_EVENT_READY fires when Enter is pressed in a one-line textarea. */
        EL_LVGL_CALL(w->cb_submit, (el_val_t)slot, (el_val_t)0);
    }
}

/* ── Initialisation ───────────────────────────────────────────────────────── */

/*
 * el_lvgl_init — call lv_init(). The host must have already initialised the
 * display driver and input driver before this, or immediately after. Idempotent.
 */
void el_lvgl_init(void) {
    static int done = 0;
    if (done) return;
    done = 1;
    lv_init();
}

/*
 * el_lvgl_run_loop — drive lv_task_handler() indefinitely.
 *
 * On RTOS: this function should run inside a dedicated FreeRTOS/Zephyr task.
 * On bare metal: call this as the last statement of main().
 *
 * The 5 ms delay between handler calls matches the LVGL documentation
 * recommendation for a ~200 Hz refresh budget.
 *
 * On hosted Linux (EL_LVGL_SDL or similar), usleep(5000) is used. On RTOS
 * targets define EL_LVGL_RTOS_DELAY(ms) to map to vTaskDelay/k_sleep/etc.
 */
void el_lvgl_run_loop(void) {
    for (;;) {
        lv_task_handler();
#if defined(EL_LVGL_RTOS_DELAY)
        EL_LVGL_RTOS_DELAY(5);
#elif defined(__linux__) || defined(__APPLE__)
        {
            /* Hosted test build — usleep available. */
#include <unistd.h>
            usleep(5000);
        }
#endif
        /* Bare-metal without a delay macro: the HAL tick increment loop
         * is the caller's responsibility. No sleep needed if lv_tick_inc()
         * is driven from a hardware timer ISR. */
    }
}

/* ── Window ───────────────────────────────────────────────────────────────── */

/*
 * el_lvgl_window_create — configure lv_scr_act() as a vertical flex container
 * and return a slot handle wrapping it. The title is stored for informational
 * purposes (e.g., a status bar widget the host might create). Width/height
 * are ignored on embedded targets because the screen size is fixed by the
 * display driver; they are accepted for API compatibility with other backends.
 */
int64_t el_lvgl_window_create(const char *title, int width, int height,
                               int min_width, int min_height) {
    (void)width; (void)height; (void)min_width; (void)min_height;

    lv_obj_t *scr = lv_scr_act();

    /* Configure the active screen as a vertical flex container so that
     * widgets added via __widget_add_child stack naturally. */
    lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_size(scr, LV_PCT(100), LV_PCT(100));

    /* Store the window title in a user-data string on the screen object
     * so host code can retrieve it if it wants to render a title bar. */
    if (title && *title) {
        /* lv_obj_set_user_data stores a void* — we cast the string pointer.
         * The string must outlive the screen object; for literals this is
         * always true. For dynamic titles use el_lvgl_window_set_title. */
        lv_obj_set_user_data(scr, (void *)(uintptr_t)title);
    }

    /* Allocate a slot for the screen object. */
    int64_t h = lvgl_slot_alloc(EL_LVGL_WINDOW, scr);
    return h;
}

/*
 * el_lvgl_window_show — no-op on embedded. The screen is always visible.
 */
void el_lvgl_window_show(int64_t handle) {
    (void)handle;
    /* On multi-screen setups, load the screen: */
    /* ElLvglWidget *w = lvgl_slot_get(handle);
     * if (w) lv_scr_load(w->obj); */
}

/*
 * el_lvgl_window_set_title — update the user_data pointer on the screen object.
 * On embedded, "title" is typically only used by a custom host title bar.
 */
void el_lvgl_window_set_title(int64_t handle, const char *title) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w || w->kind != EL_LVGL_WINDOW) return;
    lv_obj_set_user_data(w->obj, (void *)(uintptr_t)(title ? title : ""));
}

/* ── Layout containers ────────────────────────────────────────────────────── */

/*
 * el_lvgl_vstack_create — vertical flex column with inter-item gap = spacing.
 */
int64_t el_lvgl_vstack_create(int spacing) {
    lv_obj_t *obj = lv_obj_create(lv_scr_act());
    lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_style_pad_row(obj, (lv_coord_t)spacing, 0);
    lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
    /* Remove default LVGL border and background so containers are transparent
     * by default, matching the AppKit/GTK4 backends. */
    lv_obj_set_style_border_width(obj, 0, 0);
    lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
    lv_obj_set_style_pad_all(obj, 0, 0);
    return lvgl_slot_alloc(EL_LVGL_VSTACK, obj);
}

/*
 * el_lvgl_hstack_create — horizontal flex row with inter-item gap = spacing.
 */
int64_t el_lvgl_hstack_create(int spacing) {
    lv_obj_t *obj = lv_obj_create(lv_scr_act());
    lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
    lv_obj_set_style_pad_column(obj, (lv_coord_t)spacing, 0);
    lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
    lv_obj_set_style_border_width(obj, 0, 0);
    lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
    lv_obj_set_style_pad_all(obj, 0, 0);
    return lvgl_slot_alloc(EL_LVGL_HSTACK, obj);
}

/*
 * el_lvgl_zstack_create — plain container, children positioned absolutely.
 * No flex flow is set; callers use lv_obj_set_pos() on children directly,
 * or rely on their natural 0,0 origin.
 */
int64_t el_lvgl_zstack_create(void) {
    lv_obj_t *obj = lv_obj_create(lv_scr_act());
    lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
    lv_obj_set_style_border_width(obj, 0, 0);
    lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
    lv_obj_set_style_pad_all(obj, 0, 0);
    return lvgl_slot_alloc(EL_LVGL_ZSTACK, obj);
}

/*
 * el_lvgl_scroll_create — vertically scrollable container.
 */
int64_t el_lvgl_scroll_create(void) {
    lv_obj_t *obj = lv_obj_create(lv_scr_act());
    lv_obj_set_scroll_dir(obj, LV_DIR_VER);
    lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
    lv_obj_set_style_border_width(obj, 0, 0);
    lv_obj_set_style_pad_all(obj, 0, 0);
    return lvgl_slot_alloc(EL_LVGL_SCROLL, obj);
}

/* ── Widget factories ─────────────────────────────────────────────────────── */

/*
 * el_lvgl_label_create — static text label.
 */
int64_t el_lvgl_label_create(const char *text) {
    lv_obj_t *obj = lv_label_create(lv_scr_act());
    lv_label_set_text(obj, text ? text : "");
    return lvgl_slot_alloc(EL_LVGL_LABEL, obj);
}

/*
 * el_lvgl_button_create — pressable button with a child label.
 *
 * LVGL buttons are containers; text is placed in an inner lv_label child.
 * We store the child label pointer in btn_label so set_text / get_text can
 * reach it without searching the object tree at runtime.
 */
int64_t el_lvgl_button_create(const char *label) {
    lv_obj_t *btn = lv_btn_create(lv_scr_act());
    lv_obj_t *lbl = lv_label_create(btn);
    lv_label_set_text(lbl, label ? label : "");
    lv_obj_center(lbl);

    int64_t h = lvgl_slot_alloc(EL_LVGL_BUTTON, btn);
    if (h >= 0) {
        g_widgets[h].btn_label = lbl;
        /* Register click callback immediately so button responds when a
         * callback name is registered later via __widget_on_click. */
        lv_obj_add_event_cb(btn, el_lvgl_event_cb, LV_EVENT_CLICKED,
                            (void *)(intptr_t)h);
    }
    return h;
}

/*
 * el_lvgl_text_field_create — single-line text input.
 */
int64_t el_lvgl_text_field_create(const char *placeholder) {
    lv_obj_t *obj = lv_textarea_create(lv_scr_act());
    lv_textarea_set_one_line(obj, true);
    if (placeholder && *placeholder) {
        lv_textarea_set_placeholder_text(obj, placeholder);
    }
    int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTFIELD, obj);
    if (h >= 0) {
        lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
                            (void *)(intptr_t)h);
        lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_READY,
                            (void *)(intptr_t)h);
    }
    return h;
}

/*
 * el_lvgl_text_area_create — multi-line text input.
 */
int64_t el_lvgl_text_area_create(const char *placeholder) {
    lv_obj_t *obj = lv_textarea_create(lv_scr_act());
    lv_textarea_set_one_line(obj, false);
    if (placeholder && *placeholder) {
        lv_textarea_set_placeholder_text(obj, placeholder);
    }
    int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTAREA, obj);
    if (h >= 0) {
        lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
                            (void *)(intptr_t)h);
    }
    return h;
}

/*
 * el_lvgl_image_create — image widget.
 *
 * On hosted Linux (SDL backend), path is a filesystem path.
 * On embedded with SPIFFS/LittleFS, path is a SPIFFS URI: "S:/image.bin".
 * LVGL image decoders are registered separately by the host application.
 */
int64_t el_lvgl_image_create(const char *path_or_name) {
    lv_obj_t *obj = lv_img_create(lv_scr_act());
    if (path_or_name && *path_or_name) {
        lv_img_set_src(obj, path_or_name);
    }
    return lvgl_slot_alloc(EL_LVGL_IMAGE, obj);
}

/* ── Widget property setters ─────────────────────────────────────────────── */

/*
 * el_lvgl_widget_set_text — update visible text.
 *
 * Dispatch per kind:
 *   LABEL     → lv_label_set_text
 *   BUTTON    → lv_label_set_text on inner btn_label child
 *   TEXTFIELD / TEXTAREA → lv_textarea_set_text
 *   WINDOW    → lv_obj_set_user_data (stores title string)
 */
void el_lvgl_widget_set_text(int64_t handle, const char *text) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    const char *t = text ? text : "";
    switch (w->kind) {
        case EL_LVGL_LABEL:
            lv_label_set_text(w->obj, t);
            break;
        case EL_LVGL_BUTTON:
            if (w->btn_label) lv_label_set_text(w->btn_label, t);
            break;
        case EL_LVGL_TEXTFIELD:
        case EL_LVGL_TEXTAREA:
            lv_textarea_set_text(w->obj, t);
            break;
        case EL_LVGL_WINDOW:
            lv_obj_set_user_data(w->obj, (void *)(uintptr_t)t);
            break;
        default:
            break;
    }
}

/*
 * el_lvgl_widget_get_text — retrieve visible text.
 *
 * Returns a pointer into LVGL's internal storage — valid until the next LVGL
 * operation that modifies the widget. Callers that need to hold the value
 * across LVGL calls must strdup() it.
 */
const char *el_lvgl_widget_get_text(int64_t handle) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return "";
    switch (w->kind) {
        case EL_LVGL_LABEL:
            return lv_label_get_text(w->obj);
        case EL_LVGL_BUTTON:
            return w->btn_label ? lv_label_get_text(w->btn_label) : "";
        case EL_LVGL_TEXTFIELD:
        case EL_LVGL_TEXTAREA:
            return lv_textarea_get_text(w->obj);
        default:
            return "";
    }
}

/*
 * el_lvgl_widget_set_color — foreground (text) colour.
 *
 * r/g/b are 0.0–1.0 floats bit-cast as el_val_t (see el_runtime.h).
 * LVGL lv_color_make takes uint8_t 0–255 components.
 */
void el_lvgl_widget_set_color(int64_t handle,
                               float r, float g, float b, float a) {
    (void)a; /* LVGL text colour has no per-glyph alpha channel */
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_color_t c = lv_color_make(
        (uint8_t)(r * 255.0f + 0.5f),
        (uint8_t)(g * 255.0f + 0.5f),
        (uint8_t)(b * 255.0f + 0.5f));
    lv_obj_set_style_text_color(w->obj, c, 0);
    if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
        lv_obj_set_style_text_color(w->btn_label, c, 0);
    }
}

/*
 * el_lvgl_widget_set_bg_color — background fill colour + opacity.
 */
void el_lvgl_widget_set_bg_color(int64_t handle,
                                  float r, float g, float b, float a) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_color_t c = lv_color_make(
        (uint8_t)(r * 255.0f + 0.5f),
        (uint8_t)(g * 255.0f + 0.5f),
        (uint8_t)(b * 255.0f + 0.5f));
    lv_opa_t opa = (lv_opa_t)(a * 255.0f + 0.5f);
    lv_obj_set_style_bg_color(w->obj, c, 0);
    lv_obj_set_style_bg_opa(w->obj, opa, 0);
}

/*
 * el_lvgl_widget_set_font — apply font to text-bearing widget.
 *
 * The `family` parameter is accepted for API compatibility but LVGL uses
 * compiled-in fonts only. Only the size and bold flag have effect unless
 * EL_LVGL_CUSTOM_FONT is defined by the host.
 */
void el_lvgl_widget_set_font(int64_t handle,
                              const char *family, int size, int bold) {
    (void)family; /* ignored; LVGL uses compiled-in Montserrat fonts */
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    const lv_font_t *font = font_select(size, bold);
    if (!font) return;
    lv_obj_set_style_text_font(w->obj, font, 0);
    if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
        lv_obj_set_style_text_font(w->btn_label, font, 0);
    }
}

/*
 * el_lvgl_widget_set_padding — set per-side padding (top/right/bottom/left).
 */
void el_lvgl_widget_set_padding(int64_t handle,
                                 int top, int right, int bottom, int left) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_obj_set_style_pad_top(w->obj,    (lv_coord_t)top,    0);
    lv_obj_set_style_pad_right(w->obj,  (lv_coord_t)right,  0);
    lv_obj_set_style_pad_bottom(w->obj, (lv_coord_t)bottom, 0);
    lv_obj_set_style_pad_left(w->obj,   (lv_coord_t)left,   0);
}

/*
 * el_lvgl_widget_set_width — set explicit pixel width.
 */
void el_lvgl_widget_set_width(int64_t handle, int width) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_obj_set_width(w->obj, (lv_coord_t)width);
}

/*
 * el_lvgl_widget_set_height — set explicit pixel height.
 */
void el_lvgl_widget_set_height(int64_t handle, int height) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_obj_set_height(w->obj, (lv_coord_t)height);
}

/*
 * el_lvgl_widget_set_flex — set flex grow factor.
 *
 * flex > 0 → lv_obj_set_flex_grow(obj, flex): object expands to fill
 *             remaining space proportional to its grow factor.
 * flex == 0 → lv_obj_set_flex_grow(obj, 0): object uses natural size.
 */
void el_lvgl_widget_set_flex(int64_t handle, int flex) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_obj_set_flex_grow(w->obj, (uint8_t)(flex > 0 ? flex : 0));
}

/*
 * el_lvgl_widget_set_corner_radius — set border radius.
 */
void el_lvgl_widget_set_corner_radius(int64_t handle, int radius) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_obj_set_style_radius(w->obj, (lv_coord_t)radius, 0);
}

/*
 * el_lvgl_widget_set_disabled — enable/disable interactive state.
 *
 * LV_STATE_DISABLED greys out the widget and prevents input events.
 */
void el_lvgl_widget_set_disabled(int64_t handle, int disabled) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    if (disabled) {
        lv_obj_add_state(w->obj, LV_STATE_DISABLED);
    } else {
        lv_obj_clear_state(w->obj, LV_STATE_DISABLED);
    }
}

/*
 * el_lvgl_widget_set_hidden — show/hide widget.
 *
 * LV_OBJ_FLAG_HIDDEN hides the widget and removes it from layout flow.
 */
void el_lvgl_widget_set_hidden(int64_t handle, int hidden) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    if (hidden) {
        lv_obj_add_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
    } else {
        lv_obj_clear_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
    }
}

/* ── Tree operations ──────────────────────────────────────────────────────── */

/*
 * el_lvgl_widget_add_child — attach child widget to parent.
 *
 * lv_obj_set_parent() reparents the child object inside the LVGL tree.
 * For WINDOW parents we use the screen object itself as the parent, since
 * lv_scr_act() IS the root container.
 */
void el_lvgl_widget_add_child(int64_t parent, int64_t child) {
    ElLvglWidget *pw = lvgl_slot_get(parent);
    ElLvglWidget *cw = lvgl_slot_get(child);
    if (!pw || !cw) return;
    lv_obj_set_parent(cw->obj, pw->obj);
}

/*
 * el_lvgl_widget_remove_child — detach child from its current parent.
 *
 * LVGL has no explicit "remove from parent without deleting" operation.
 * We reparent the child back to the active screen (making it a root-level
 * floating widget) and then hide it. The widget still occupies a slot and
 * can be re-attached or destroyed later.
 */
void el_lvgl_widget_remove_child(int64_t parent, int64_t child) {
    (void)parent;
    ElLvglWidget *cw = lvgl_slot_get(child);
    if (!cw) return;
    /* Move to screen root and hide. */
    lv_obj_set_parent(cw->obj, lv_scr_act());
    lv_obj_add_flag(cw->obj, LV_OBJ_FLAG_HIDDEN);
}

/*
 * el_lvgl_widget_destroy — delete widget and its children from the LVGL tree,
 * then free the slot.
 *
 * lv_obj_del() recursively deletes the object and all children. After this
 * call the handle is invalid and must not be used.
 */
void el_lvgl_widget_destroy(int64_t handle) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    lv_obj_del(w->obj);
    lvgl_slot_free(handle);
}

/* ── Event registration ───────────────────────────────────────────────────── */

/*
 * Event registration stores the El function name in the widget slot. The
 * actual lv_obj_add_event_cb() call is made here (or was made in the factory
 * for buttons/textfields where we know the relevant event codes upfront).
 *
 * For widgets that did not register their event callback in the factory (e.g.
 * labels receiving a click handler), we add the LVGL event binding now.
 */

void el_lvgl_widget_on_click(int64_t handle, const char *fn_name) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    strncpy(w->cb_click, fn_name ? fn_name : "", 255);
    w->cb_click[255] = '\0';
    /*
     * Buttons already have LV_EVENT_CLICKED registered in the factory.
     * For other widget kinds (labels, containers used as tap targets), add
     * the click flag and register the callback.
     */
    if (w->kind != EL_LVGL_BUTTON) {
        lv_obj_add_flag(w->obj, LV_OBJ_FLAG_CLICKABLE);
        lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_CLICKED,
                            (void *)(intptr_t)handle);
    }
}

void el_lvgl_widget_on_change(int64_t handle, const char *fn_name) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    strncpy(w->cb_change, fn_name ? fn_name : "", 255);
    w->cb_change[255] = '\0';
    /*
     * Textfield/textarea factories already register VALUE_CHANGED.
     * For other kinds (e.g. a custom toggle), add the binding now.
     */
    if (w->kind != EL_LVGL_TEXTFIELD && w->kind != EL_LVGL_TEXTAREA) {
        lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
                            (void *)(intptr_t)handle);
    }
}

void el_lvgl_widget_on_submit(int64_t handle, const char *fn_name) {
    ElLvglWidget *w = lvgl_slot_get(handle);
    if (!w) return;
    strncpy(w->cb_submit, fn_name ? fn_name : "", 255);
    w->cb_submit[255] = '\0';
    /*
     * LV_EVENT_READY fires when Enter is pressed in a one-line textarea.
     * Textfield factories already register READY. For other kinds, add it.
     */
    if (w->kind != EL_LVGL_TEXTFIELD) {
        lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_READY,
                            (void *)(intptr_t)handle);
    }
}

#endif /* EL_TARGET_LVGL */
