/*
 * el_appkit.m — AppKit backend for the el native widget system.
 *
 * This file implements the macOS AppKit widget layer that el_seed.c calls
 * through to when EL_TARGET_MACOS is defined.
 *
 * Architecture:
 *   el program (el code)
 *       → __widget_* C builtins in el_seed.c
 *           → el_appkit_* C-callable functions declared here
 *               → NSView/NSWindow/NSStackView/NSButton/etc. via ObjC
 *
 * Widget handles: every widget (window, view, control) is assigned an int64_t
 * slot index into _el_widgets[]. The el program holds these as opaque Int values.
 * Slot 0 is never valid (reserved as null handle = -1 convention).
 *
 * Threading: All AppKit calls MUST run on the main thread. el_appkit_dispatch_main
 * bounces work to the main thread synchronously if called from a background thread.
 * el_appkit_run_loop MUST be called from the main thread; it never returns.
 *
 * Callback mechanism: when a widget fires an event (button click, text change),
 * the bridge looks up the registered callback function name, then calls
 *   dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string)
 * This resolves the El function symbol at runtime, matching the __thread_create
 * pattern already established in el_seed.c.
 *
 * Compile (MRC — no ARC; manual retain/release needed for struct-embedded id):
 *   clang -std=gnu11 -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m -o el_appkit.o
 * Then link el_appkit.o alongside el_seed.c.
 *
 * Do NOT use -fobjc-arc: the widget table stores id in a plain C struct. ARC
 * forbids explicit retain/release on struct-embedded object pointers and cannot
 * insert automatic retain/release through C struct boundaries. MRC is the
 * correct choice for this kind of C/ObjC bridge.
 *
 * Link flags required:
 *   -framework Cocoa
 */

#import <Cocoa/Cocoa.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <dlfcn.h>

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

#define EL_APPKIT_MAX_WIDGETS 4096

typedef enum {
    EL_WIDGET_FREE     = 0,
    EL_WIDGET_WINDOW   = 1,
    EL_WIDGET_VSTACK   = 2,
    EL_WIDGET_HSTACK   = 3,
    EL_WIDGET_ZSTACK   = 4,
    EL_WIDGET_SCROLL   = 5,
    EL_WIDGET_LABEL    = 6,
    EL_WIDGET_BUTTON   = 7,
    EL_WIDGET_TEXTFIELD = 8,
    EL_WIDGET_TEXTAREA  = 9,
    EL_WIDGET_IMAGE    = 10,
    EL_WIDGET_DIVIDER  = 11,
    EL_WIDGET_SPACER   = 12,
} ElWidgetKind;

typedef struct {
    ElWidgetKind  kind;
    id            obj;        /* NSWindow* or NSView* subclass, retained */
    char*         cb_click;   /* El function name for click/submit events */
    char*         cb_change;  /* El function name for value-change events */
    char*         user_data;  /* custom data string passed to click callbacks */
} ElWidget;

static ElWidget _el_widgets[EL_APPKIT_MAX_WIDGETS];
static int      _el_widget_count = 0;

/* Allocate a slot and return its index. Returns -1 if full. */
static int64_t el_widget_alloc(ElWidgetKind kind, id obj) {
    for (int i = 1; i < EL_APPKIT_MAX_WIDGETS; i++) {
        if (_el_widgets[i].kind == EL_WIDGET_FREE) {
            _el_widgets[i].kind      = kind;
            _el_widgets[i].obj       = [obj retain];
            _el_widgets[i].cb_click  = NULL;
            _el_widgets[i].cb_change = NULL;
            _el_widgets[i].user_data = NULL;
            if (i > _el_widget_count) _el_widget_count = i;
            return (int64_t)i;
        }
    }
    return -1;
}

static ElWidget* el_widget_get(int64_t handle) {
    if (handle <= 0 || handle >= EL_APPKIT_MAX_WIDGETS) return NULL;
    if (_el_widgets[handle].kind == EL_WIDGET_FREE)     return NULL;
    return &_el_widgets[handle];
}

static void el_widget_free(int64_t handle) {
    ElWidget* w = el_widget_get(handle);
    if (!w) return;
    [w->obj release];
    w->obj  = nil;
    w->kind = EL_WIDGET_FREE;
    free(w->cb_click);  w->cb_click  = NULL;
    free(w->cb_change); w->cb_change = NULL;
    free(w->user_data); w->user_data = NULL;
}

/* ── Dispatch helpers ─────────────────────────────────────────────────────── */

/* Run a block on the main thread, waiting for completion. Safe to call from
 * any thread including the main thread. */
static void el_appkit_sync_main(void (^block)(void)) {
    if ([NSThread isMainThread]) {
        block();
    } else {
        dispatch_sync(dispatch_get_main_queue(), block);
    }
}

/* ── El callback invocation ──────────────────────────────────────────────── */
/*
 * Invoke an El callback by symbol name. The El function must have the
 * signature:  fn handler(handle: Int, data: String) -> Void
 * which compiles to:  void fn_name(el_val_t handle, el_val_t data)
 */
typedef void (*ElCb2)(int64_t handle, int64_t data);

static void el_appkit_invoke_cb(const char* fn_name, int64_t handle, const char* data) {
    if (!fn_name || !*fn_name) return;
    void* sym = dlsym(RTLD_DEFAULT, fn_name);
    if (!sym) return;
    ElCb2 fn = (ElCb2)sym;
    fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
}

/* ── ObjC delegate for button clicks ─────────────────────────────────────── */

@interface ElButtonTarget : NSObject
@property int64_t widgetHandle;
@end

@implementation ElButtonTarget
- (void)buttonClicked:(id)sender {
    ElWidget* w = el_widget_get(self.widgetHandle);
    if (w && w->cb_click) {
        el_appkit_invoke_cb(w->cb_click, self.widgetHandle, w->user_data ? w->user_data : "");
    }
}
@end

/* ── ObjC delegate for text field changes / enter key ───────────────────── */

@interface ElTextFieldDelegate : NSObject <NSTextFieldDelegate>
@property int64_t widgetHandle;
@end

@implementation ElTextFieldDelegate
- (void)controlTextDidChange:(NSNotification*)notif {
    ElWidget* w = el_widget_get(self.widgetHandle);
    if (!w) return;
    NSTextField* tf = (NSTextField*)notif.object;
    const char* val = [[tf stringValue] UTF8String];
    if (w->cb_change) {
        el_appkit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : "");
    }
}
- (void)controlTextDidEndEditing:(NSNotification*)notif {
    /* Enter key (or tab) submits. The userInfo value is an NSNumber, not NSEvent. */
    NSNumber* movement = [[notif userInfo] objectForKey:@"NSTextMovement"];
    if (movement && [movement intValue] == NSTextMovementReturn) {
        ElWidget* w = el_widget_get(self.widgetHandle);
        if (!w) return;
        NSTextField* tf = (NSTextField*)notif.object;
        const char* val = [[tf stringValue] UTF8String];
        if (w->cb_click) {  /* cb_click = on_submit for text fields */
            el_appkit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : "");
        }
    }
}
@end

/* We hold strong references to our ObjC delegate objects in a side table so
 * ARC/MRC don't collect them.  Simple parallel array keyed by widget slot. */
static id _el_delegates[EL_APPKIT_MAX_WIDGETS];

/* ── Flipped wrapper for NSScrollView document views ─────────────────────────
 * NSScrollView's clip view uses the document view's coordinate system. When
 * the document view is non-flipped (origin at bottom-left), content stacks
 * from the bottom. This subclass overrides isFlipped so content starts at the
 * visual top-left, matching expected list/feed behaviour. */
@interface ElFlippedView : NSView
@end
@implementation ElFlippedView
- (BOOL)isFlipped { return YES; }
@end

/* ── ElResizableContentView ───────────────────────────────────────────────────
 * Used as NSWindow's content view instead of a plain NSView.
 * Overrides setFrameSize: to propagate the new size directly to the single
 * root child without going through the AL constraint graph.
 *
 * Why: with TAMIC:NO on the content view and 4-edge AL constraints to the
 * child, NSWindow calls [contentView fittingSize] to compute its minimum
 * resize floor.  fittingSize walks the constraint graph and returns the
 * largest minimum it finds — locking horizontal resize to the initial width.
 * By removing the AL constraints and propagating via setFrameSize: instead,
 * NSWindow has no constraint-based minimum and respects setContentMinSize: only. */
@interface ElResizableContentView : NSView
@end
@implementation ElResizableContentView
- (NSSize)fittingSize          { return NSMakeSize(1.0, 1.0); }
- (NSSize)intrinsicContentSize { return NSMakeSize(NSViewNoIntrinsicMetric, NSViewNoIntrinsicMetric); }
@end

/* ElWindow subclass — overrides minSize so the AL constraint graph cannot
 * inflate the resize floor beyond the manifest-specified minimum.
 * NSWindow normally sets minSize from the larger of the explicit setMinSize:
 * call and the AL-computed minimum content size; the override replaces that
 * with the explicit minimum only. */
@interface ElWindow : NSWindow
@property (nonatomic) NSSize elMinFrameSize;
@property (nonatomic) NSSize elInitialContentSize; /* designed width/height from manifest */
@end
@implementation ElWindow
- (NSSize)minSize { return _elMinFrameSize; }
@end

/* ── NSApplication setup ──────────────────────────────────────────────────── */

@interface ElAppDelegate : NSObject <NSApplicationDelegate>
@end

@implementation ElAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification*)notif {
    /* Nothing needed — windows are created programmatically before run loop. */
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app {
    return YES;
}
@end

static ElAppDelegate* _el_app_delegate = nil;

/* ── Public C API (called from el_seed.c) ─────────────────────────────────── */

/*
 * el_appkit_init — initialize NSApplication. Must be called once on the main
 * thread before any other el_appkit_* function. Idempotent.
 */
void el_appkit_init(void) {
    static int done = 0;
    if (done) return;
    done = 1;
    [NSApplication sharedApplication];
    [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
    _el_app_delegate = [[ElAppDelegate alloc] init];
    [NSApp setDelegate:_el_app_delegate];
}

/*
 * el_appkit_window_create — create an NSWindow and return its handle.
 * title, width, height, min_width, min_height come from manifest.el app{} block.
 */
int64_t el_appkit_window_create(const char* title, int width, int height,
                                 int min_width, int min_height) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSRect frame = NSMakeRect(0, 0, (CGFloat)width, (CGFloat)height);
        NSWindowStyleMask style =
            NSWindowStyleMaskTitled |
            NSWindowStyleMaskClosable |
            NSWindowStyleMaskMiniaturizable |
            NSWindowStyleMaskResizable;
        ElWindow* win = [[ElWindow alloc]
            initWithContentRect:frame
                      styleMask:style
                        backing:NSBackingStoreBuffered
                          defer:NO];
        [win setTitle:[NSString stringWithUTF8String:title ? title : ""]];
        /* Store the manifest minimum as our explicit frame floor. */
        NSRect minContentRect = NSMakeRect(0, 0, (CGFloat)min_width, (CGFloat)min_height);
        NSSize minFrame = [win frameRectForContentRect:minContentRect].size;
        win.elMinFrameSize = minFrame;
        [win setContentMinSize:NSMakeSize((CGFloat)min_width, (CGFloat)min_height)];
        win.elInitialContentSize = NSMakeSize((CGFloat)width, (CGFloat)height);
        [win center];

        ElResizableContentView* root = [[ElResizableContentView alloc] initWithFrame:frame];
        [win setContentView:root];

        handle = el_widget_alloc(EL_WIDGET_WINDOW, win);
        [win release];
        [root release];
    });
    return handle;
}

/*
 * el_appkit_window_show — make the window visible and bring it to front.
 */
void el_appkit_window_show(int64_t handle) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w || w->kind != EL_WIDGET_WINDOW) return;
        NSWindow* win = (NSWindow*)w->obj;
        /* AL may have shrunk the window during the build phase.
         * Restore the designed content size before showing — the window
         * is not yet visible so the user never sees the shrunken state.
         * After this, AL runs within the correct frame and required
         * 4-edge constraints keep the child filling on every resize. */
        if ([win isKindOfClass:[ElWindow class]]) {
            ElWindow* elWin = (ElWindow*)win;
            NSSize cs = elWin.elInitialContentSize;
            if (cs.width > 0) {
                [win setContentSize:cs];
                [win center];
            }
        }
        [win makeKeyAndOrderFront:nil];
        [NSApp activateIgnoringOtherApps:YES];
    });
}

/*
 * el_appkit_window_set_title — update window title at runtime.
 */
void el_appkit_window_set_title(int64_t handle, const char* title) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w || w->kind != EL_WIDGET_WINDOW) return;
        NSWindow* win = (NSWindow*)w->obj;
        [win setTitle:[NSString stringWithUTF8String:title ? title : ""]];
    });
}

/*
 * el_appkit_run_loop — start the NSApplication run loop. Never returns.
 * Must be called from the main thread as the last step of main().
 */
void el_appkit_run_loop(void) {
    [NSApp run];
}

/* ── Layout container factories ───────────────────────────────────────────── */

static int64_t make_stack(NSUserInterfaceLayoutOrientation orient,
                           CGFloat spacing) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSStackView* sv = [[NSStackView alloc] initWithFrame:NSZeroRect];
        [sv setOrientation:orient];
        /* Alignment must match the stack's cross-axis to avoid NSStackView
         * silently flipping orientation. For a vertical stack, align children
         * to the leading edge (left); for a horizontal stack, align to top.
         * Using NSLayoutAttributeLeading on a horizontal stack flips it vertical. */
        NSLayoutAttribute align = (orient == NSUserInterfaceLayoutOrientationVertical)
            ? NSLayoutAttributeLeading : NSLayoutAttributeTop;
        [sv setAlignment:align];
        [sv setSpacing:spacing];
        [sv setEdgeInsets:NSEdgeInsetsZero];
        [sv setDistribution:NSStackViewDistributionFill];
        [sv setTranslatesAutoresizingMaskIntoConstraints:NO];
        ElWidgetKind kind = (orient == NSUserInterfaceLayoutOrientationVertical)
            ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK;
        handle = el_widget_alloc(kind, sv);
        [sv release];
    });
    return handle;
}

int64_t el_appkit_vstack_create(int spacing) {
    return make_stack(NSUserInterfaceLayoutOrientationVertical, (CGFloat)spacing);
}

int64_t el_appkit_hstack_create(int spacing) {
    return make_stack(NSUserInterfaceLayoutOrientationHorizontal, (CGFloat)spacing);
}

int64_t el_appkit_zstack_create(void) {
    /* ZStack = NSView with manual child positioning (no stack manager). */
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSView* v = [[NSView alloc] initWithFrame:NSZeroRect];
        [v setTranslatesAutoresizingMaskIntoConstraints:NO];
        handle = el_widget_alloc(EL_WIDGET_ZSTACK, v);
        [v release];
    });
    return handle;
}

int64_t el_appkit_scroll_create(void) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSScrollView* sv = [[NSScrollView alloc] initWithFrame:NSZeroRect];
        [sv setHasVerticalScroller:YES];
        [sv setHasHorizontalScroller:NO];
        [sv setAutohidesScrollers:YES];
        [sv setTranslatesAutoresizingMaskIntoConstraints:NO];
        handle = el_widget_alloc(EL_WIDGET_SCROLL, sv);
        [sv release];
    });
    return handle;
}

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

int64_t el_appkit_label_create(const char* text) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSTextField* lbl = [NSTextField labelWithString:
            [NSString stringWithUTF8String:text ? text : ""]];
        [lbl setEditable:NO];
        [lbl setBordered:NO];
        [lbl setDrawsBackground:NO];
        [lbl setTranslatesAutoresizingMaskIntoConstraints:NO];
        handle = el_widget_alloc(EL_WIDGET_LABEL, lbl);
    });
    return handle;
}

int64_t el_appkit_button_create(const char* label) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSButton* btn = [[NSButton alloc] initWithFrame:NSZeroRect];
        [btn setButtonType:NSButtonTypeMomentaryLight];
        [btn setBezelStyle:NSBezelStyleShadowlessSquare];
        [btn setTitle:[NSString stringWithUTF8String:label ? label : ""]];
        [btn setAlignment:NSTextAlignmentLeft];
        [btn setTranslatesAutoresizingMaskIntoConstraints:NO];

        /* Create action target. */
        ElButtonTarget* target = [[ElButtonTarget alloc] init];
        /* handle assigned below after alloc — set after */
        [btn setTarget:target];
        [btn setAction:@selector(buttonClicked:)];

        int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn);
        target.widgetHandle = h;
        _el_delegates[h] = target; /* retain */
        handle = h;
        [btn release];
    });
    return handle;
}

int64_t el_appkit_text_field_create(const char* placeholder) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSTextField* tf = [[NSTextField alloc] initWithFrame:NSZeroRect];
        [tf setTranslatesAutoresizingMaskIntoConstraints:NO];
        if (placeholder && *placeholder) {
            [tf setPlaceholderString:[NSString stringWithUTF8String:placeholder]];
        }

        ElTextFieldDelegate* del = [[ElTextFieldDelegate alloc] init];
        [tf setDelegate:del];

        int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf);
        del.widgetHandle = h;
        _el_delegates[h] = del;
        handle = h;
        [tf release];
    });
    return handle;
}

int64_t el_appkit_text_area_create(const char* placeholder) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSScrollView* scroll = [[NSScrollView alloc] initWithFrame:NSZeroRect];
        [scroll setHasVerticalScroller:YES];
        [scroll setAutohidesScrollers:YES];
        [scroll setTranslatesAutoresizingMaskIntoConstraints:NO];

        NSTextView* tv = [[NSTextView alloc] initWithFrame:NSZeroRect];
        [tv setMinSize:NSMakeSize(0, 60)];
        [tv setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
        [tv setVerticallyResizable:YES];
        [tv setHorizontallyResizable:NO];
        [tv setAutoresizingMask:NSViewWidthSizable];
        if (placeholder && *placeholder) {
            /* NSTextView has no native placeholder; we set a hint via
             * a custom accessibility description as a minimal approach. */
            [tv setAccessibilityPlaceholderValue:
                [NSString stringWithUTF8String:placeholder]];
        }
        [[tv textContainer] setWidthTracksTextView:YES];
        [scroll setDocumentView:tv];

        int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, scroll);
        handle = h;
        [scroll release];
        [tv release];
    });
    return handle;
}

int64_t el_appkit_image_create(const char* path_or_name) {
    __block int64_t handle = -1;
    el_appkit_sync_main(^{
        NSImage* img = nil;
        if (path_or_name && *path_or_name) {
            /* Try as filesystem path first, then as named system image. */
            img = [[NSImage alloc] initWithContentsOfFile:
                [NSString stringWithUTF8String:path_or_name]];
            if (!img) {
                img = [NSImage imageNamed:[NSString stringWithUTF8String:path_or_name]];
                if (img) [img retain];
            }
        }
        NSImageView* iv = [[NSImageView alloc] initWithFrame:NSZeroRect];
        [iv setTranslatesAutoresizingMaskIntoConstraints:NO];
        if (img) {
            [iv setImage:img];
            [img release];
        }
        handle = el_widget_alloc(EL_WIDGET_IMAGE, iv);
        [iv release];
    });
    return handle;
}

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

void el_appkit_widget_set_text(int64_t handle, const char* text) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSString* s = [NSString stringWithUTF8String:text ? text : ""];
        switch (w->kind) {
            case EL_WIDGET_LABEL:
            case EL_WIDGET_TEXTFIELD:
                [(NSTextField*)w->obj setStringValue:s];
                break;
            case EL_WIDGET_BUTTON:
                [(NSButton*)w->obj setTitle:s];
                break;
            case EL_WIDGET_TEXTAREA: {
                NSScrollView* sv = (NSScrollView*)w->obj;
                NSTextView* tv   = (NSTextView*)[sv documentView];
                [tv setString:s];
                break;
            }
            case EL_WIDGET_WINDOW:
                [(NSWindow*)w->obj setTitle:s];
                break;
            default: break;
        }
    });
}

const char* el_appkit_widget_get_text(int64_t handle) {
    __block const char* result = "";
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSString* s = nil;
        switch (w->kind) {
            case EL_WIDGET_LABEL:
            case EL_WIDGET_TEXTFIELD:
                s = [(NSTextField*)w->obj stringValue];
                break;
            case EL_WIDGET_BUTTON:
                s = [(NSButton*)w->obj title];
                break;
            case EL_WIDGET_TEXTAREA: {
                NSScrollView* sv = (NSScrollView*)w->obj;
                NSTextView* tv   = (NSTextView*)[sv documentView];
                s = [tv string];
                break;
            }
            default: break;
        }
        if (s) result = strdup([s UTF8String]);
    });
    return result;
}

void el_appkit_widget_set_color(int64_t handle, float r, float g, float b, float a) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r
                                         green:(CGFloat)g
                                          blue:(CGFloat)b
                                         alpha:(CGFloat)a];
        switch (w->kind) {
            case EL_WIDGET_LABEL:
            case EL_WIDGET_TEXTFIELD:
                [(NSTextField*)w->obj setTextColor:c];
                break;
            case EL_WIDGET_BUTTON: {
                /* Update foreground color on attributed title, preserving font. */
                NSButton* btn = (NSButton*)w->obj;
                NSAttributedString* existing = [btn attributedTitle];
                NSMutableAttributedString* as;
                if (existing && [existing length] > 0) {
                    as = [[NSMutableAttributedString alloc] initWithAttributedString:existing];
                } else {
                    NSString* t = [btn title];
                    if (!t) t = @"";
                    as = [[NSMutableAttributedString alloc] initWithString:t];
                }
                [as addAttribute:NSForegroundColorAttributeName value:c
                            range:NSMakeRange(0, [as length])];
                [btn setAttributedTitle:as];
                [as release];
                break;
            }
            case EL_WIDGET_VSTACK:
            case EL_WIDGET_HSTACK:
            case EL_WIDGET_ZSTACK: {
                /* Set background via wantsLayer + layer background. */
                NSView* v = (NSView*)w->obj;
                [v setWantsLayer:YES];
                [v layer].backgroundColor = [c CGColor];
                break;
            }
            default: break;
        }
    });
}

void el_appkit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r
                                         green:(CGFloat)g
                                          blue:(CGFloat)b
                                         alpha:(CGFloat)a];
        if (w->kind == EL_WIDGET_WINDOW) {
            NSWindow* win = (NSWindow*)w->obj;
            [win setBackgroundColor:c];
            /* Also paint the contentView layer so it shows through NSStackView. */
            NSView* cv = [win contentView];
            [cv setWantsLayer:YES];
            [cv layer].backgroundColor = [c CGColor];
            return;
        }
        if (w->kind == EL_WIDGET_BUTTON) {
            /* Disable native bezel so CALayer background is visible. */
            NSButton* btn = (NSButton*)w->obj;
            [btn setBordered:NO];
            [btn setWantsLayer:YES];
            [btn layer].backgroundColor = [c CGColor];
            return;
        }
        NSView* v = (NSView*)w->obj;
        if (w->kind == EL_WIDGET_SCROLL) {
            /* NSScrollView needs setBackgroundColor, not just layer bg. */
            NSScrollView* sv = (NSScrollView*)v;
            [sv setBackgroundColor:c];
            [sv setDrawsBackground:YES];
        }
        [v setWantsLayer:YES];
        [v layer].backgroundColor = [c CGColor];
    });
}

void el_appkit_widget_set_font(int64_t handle, const char* family, int size, int bold) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSFont* font;
        if (bold) {
            font = [NSFont boldSystemFontOfSize:(CGFloat)size];
        } else if (family && *family && strcmp(family, "system") != 0) {
            font = [NSFont fontWithName:[NSString stringWithUTF8String:family]
                                   size:(CGFloat)size];
            if (!font) font = [NSFont systemFontOfSize:(CGFloat)size];
        } else {
            font = [NSFont systemFontOfSize:(CGFloat)size];
        }
        switch (w->kind) {
            case EL_WIDGET_LABEL:
            case EL_WIDGET_TEXTFIELD:
                [(NSTextField*)w->obj setFont:font];
                break;
            case EL_WIDGET_BUTTON:
                /* NSButton: set font via attributed title, preserving existing foreground color. */
                {
                    NSButton* fbtn = (NSButton*)w->obj;
                    NSAttributedString* fexisting = [fbtn attributedTitle];
                    NSMutableAttributedString* as;
                    if (fexisting && [fexisting length] > 0) {
                        as = [[NSMutableAttributedString alloc] initWithAttributedString:fexisting];
                    } else {
                        NSString* ft = [fbtn title];
                        if (!ft) ft = @"";
                        as = [[NSMutableAttributedString alloc] initWithString:ft];
                    }
                    [as addAttribute:NSFontAttributeName value:font
                                range:NSMakeRange(0, [as length])];
                    [fbtn setAttributedTitle:as];
                    [as release];
                }
                break;
            case EL_WIDGET_TEXTAREA: {
                NSScrollView* sv = (NSScrollView*)w->obj;
                NSTextView* tv   = (NSTextView*)[sv documentView];
                [tv setFont:font];
                break;
            }
            default: break;
        }
    });
}

void el_appkit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        /* Padding on stack views maps to edgeInsets. */
        if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) {
            NSStackView* sv = (NSStackView*)w->obj;
            [sv setEdgeInsets:NSEdgeInsetsMake((CGFloat)top, (CGFloat)left,
                                               (CGFloat)bottom, (CGFloat)right)];
        }
        /* For individual widgets we set a custom layout margin via constraints.
         * For now, NSTextView respects textContainerInset. */
        if (w->kind == EL_WIDGET_TEXTAREA) {
            NSScrollView* sv = (NSScrollView*)w->obj;
            NSTextView* tv   = (NSTextView*)[sv documentView];
            [tv setTextContainerInset:NSMakeSize((CGFloat)(left + right) / 2,
                                                 (CGFloat)(top + bottom) / 2)];
        }
    });
}

void el_appkit_widget_set_width(int64_t handle, int width) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSView* v = (NSView*)w->obj;
        if ([v isKindOfClass:[NSWindow class]]) return;
        /* Add a fixed-width constraint. */
        NSLayoutConstraint* c = [NSLayoutConstraint
            constraintWithItem:v
                     attribute:NSLayoutAttributeWidth
                     relatedBy:NSLayoutRelationEqual
                        toItem:nil
                     attribute:NSLayoutAttributeNotAnAttribute
                    multiplier:1.0
                      constant:(CGFloat)width];
        c.priority = NSLayoutPriorityDefaultHigh;
        [v addConstraint:c];
    });
}

void el_appkit_widget_set_height(int64_t handle, int height) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSView* v = (NSView*)w->obj;
        if ([v isKindOfClass:[NSWindow class]]) return;
        NSLayoutConstraint* c = [NSLayoutConstraint
            constraintWithItem:v
                     attribute:NSLayoutAttributeHeight
                     relatedBy:NSLayoutRelationEqual
                        toItem:nil
                     attribute:NSLayoutAttributeNotAnAttribute
                    multiplier:1.0
                      constant:(CGFloat)height];
        c.priority = NSLayoutPriorityDefaultHigh;
        [v addConstraint:c];
    });
}

void el_appkit_widget_set_flex(int64_t handle, int flex) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSView* v = (NSView*)w->obj;
        if ([v isKindOfClass:[NSWindow class]]) return;
        if (flex > 0) {
            /* flex:1 → low hugging (wants to expand) + low compression resistance
             * (willing to be compressed). This makes NSStackViewDistributionFill
             * choose this view to grow/shrink to fill available space. */
            [v setContentHuggingPriority:NSLayoutPriorityDefaultLow
                           forOrientation:NSLayoutConstraintOrientationHorizontal];
            [v setContentHuggingPriority:NSLayoutPriorityDefaultLow
                           forOrientation:NSLayoutConstraintOrientationVertical];
            [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow
                                        forOrientation:NSLayoutConstraintOrientationHorizontal];
            [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow
                                        forOrientation:NSLayoutConstraintOrientationVertical];
        } else {
            /* flex:0 → high hugging (stays at natural size) + high compression
             * resistance (resists shrinking). */
            [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh
                           forOrientation:NSLayoutConstraintOrientationHorizontal];
            [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh
                           forOrientation:NSLayoutConstraintOrientationVertical];
            [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh
                                        forOrientation:NSLayoutConstraintOrientationHorizontal];
            [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh
                                        forOrientation:NSLayoutConstraintOrientationVertical];
        }
    });
}

void el_appkit_widget_set_corner_radius(int64_t handle, int radius) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSView* v = (NSView*)w->obj;
        if ([v isKindOfClass:[NSWindow class]]) return;
        [v setWantsLayer:YES];
        [v layer].cornerRadius = (CGFloat)radius;
        [v layer].masksToBounds = YES;
    });
}

void el_appkit_widget_set_disabled(int64_t handle, int disabled) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        if (w->kind == EL_WIDGET_BUTTON) {
            [(NSButton*)w->obj setEnabled:!disabled];
        } else if (w->kind == EL_WIDGET_TEXTFIELD) {
            [(NSTextField*)w->obj setEnabled:!disabled];
        }
    });
}

void el_appkit_widget_set_hidden(int64_t handle, int hidden) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSView* v = (NSView*)w->obj;
        if (![v isKindOfClass:[NSWindow class]]) {
            [v setHidden:(BOOL)hidden];
        }
    });
}

/* ── Child management ─────────────────────────────────────────────────────── */

/*
 * el_appkit_widget_add_child — attach child widget to parent.
 *
 * - Window: child is added to contentView (NSStackView).
 * - VStack/HStack: child is added as an arranged subview.
 * - ZStack/generic view: child is added as a plain subview.
 * - ScrollView: child becomes the documentView (only first child honoured).
 */
void el_appkit_widget_add_child(int64_t parent, int64_t child) {
    el_appkit_sync_main(^{
        ElWidget* pw = el_widget_get(parent);
        ElWidget* cw = el_widget_get(child);
        if (!pw || !cw) return;

        NSView* childView = (NSView*)cw->obj;
        if ([childView isKindOfClass:[NSWindow class]]) return; /* can't add window as child */

        switch (pw->kind) {
            case EL_WIDGET_WINDOW: {
                NSWindow* win = (NSWindow*)pw->obj;
                NSView*   cv  = [win contentView];
                /* Standard required 4-edge constraints so the child always
                 * fills the content view during user-initiated resize.
                 * AL may shrink the window during the build phase (before show),
                 * but el_appkit_window_show calls setContentSize: to restore
                 * the designed size before making the window visible. */
                [childView setTranslatesAutoresizingMaskIntoConstraints:NO];
                [cv addSubview:childView];
                [NSLayoutConstraint activateConstraints:@[
                    [childView.topAnchor     constraintEqualToAnchor:cv.topAnchor],
                    [childView.bottomAnchor  constraintEqualToAnchor:cv.bottomAnchor],
                    [childView.leadingAnchor constraintEqualToAnchor:cv.leadingAnchor],
                    [childView.trailingAnchor constraintEqualToAnchor:cv.trailingAnchor],
                ]];
                break;
            }
            case EL_WIDGET_VSTACK: {
                NSStackView* sv = (NSStackView*)pw->obj;
                [sv addArrangedSubview:childView];
                /* Pin child leading+trailing to fill cross-axis (width) of the VStack.
                 * Priority 999 avoids intrinsic-size feedback through NSWindow. */
                [childView setTranslatesAutoresizingMaskIntoConstraints:NO];
                NSLayoutConstraint* vl = [NSLayoutConstraint
                    constraintWithItem:childView attribute:NSLayoutAttributeLeading
                    relatedBy:NSLayoutRelationEqual
                    toItem:sv attribute:NSLayoutAttributeLeading
                    multiplier:1.0 constant:0.0];
                NSLayoutConstraint* vr = [NSLayoutConstraint
                    constraintWithItem:childView attribute:NSLayoutAttributeTrailing
                    relatedBy:NSLayoutRelationEqual
                    toItem:sv attribute:NSLayoutAttributeTrailing
                    multiplier:1.0 constant:0.0];
                vl.priority = 999;
                vr.priority = 999;
                [sv addConstraints:@[vl, vr]];
                break;
            }
            case EL_WIDGET_HSTACK: {
                NSStackView* sv = (NSStackView*)pw->obj;
                [sv addArrangedSubview:childView];
                /* Pin child top+bottom to fill cross-axis (height) of the HStack.
                 * Priority 499 (well below required 1000) lets the window frame
                 * win over child intrinsic size, avoiding NSWindow auto-resize. */
                [childView setTranslatesAutoresizingMaskIntoConstraints:NO];
                NSLayoutConstraint* ht = [NSLayoutConstraint
                    constraintWithItem:childView attribute:NSLayoutAttributeTop
                    relatedBy:NSLayoutRelationEqual
                    toItem:sv attribute:NSLayoutAttributeTop
                    multiplier:1.0 constant:0.0];
                NSLayoutConstraint* hb = [NSLayoutConstraint
                    constraintWithItem:childView attribute:NSLayoutAttributeBottom
                    relatedBy:NSLayoutRelationEqual
                    toItem:sv attribute:NSLayoutAttributeBottom
                    multiplier:1.0 constant:0.0];
                ht.priority = 499;
                hb.priority = 499;
                [sv addConstraints:@[ht, hb]];
                break;
            }
            case EL_WIDGET_SCROLL: {
                NSScrollView* sv = (NSScrollView*)pw->obj;
                /* Wrap the child in a flipped view so content starts at the
                 * visual top-left.  Without flipping, NSScrollView places the
                 * document origin at the bottom-left and lists appear inverted. */
                ElFlippedView* flipper = [[ElFlippedView alloc] initWithFrame:NSZeroRect];
                [flipper setTranslatesAutoresizingMaskIntoConstraints:NO];
                [childView setTranslatesAutoresizingMaskIntoConstraints:NO];
                [flipper addSubview:childView];
                /* Pin child to all 4 edges of the flipper. */
                NSArray* hc = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[c]|"
                    options:0 metrics:nil views:@{@"c": childView}];
                NSArray* vc = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[c]|"
                    options:0 metrics:nil views:@{@"c": childView}];
                [flipper addConstraints:hc];
                [flipper addConstraints:vc];
                [sv setDocumentView:flipper];
                /* Constrain flipper width to the scroll view so items fill the
                 * full panel width (vertical-only scrolling). */
                NSLayoutConstraint* wc = [NSLayoutConstraint
                    constraintWithItem:flipper attribute:NSLayoutAttributeWidth
                    relatedBy:NSLayoutRelationEqual
                    toItem:sv attribute:NSLayoutAttributeWidth
                    multiplier:1.0 constant:0.0];
                [sv addConstraint:wc];
                [flipper release];
                break;
            }
            case EL_WIDGET_ZSTACK:
            default: {
                NSView* pv = (NSView*)pw->obj;
                [pv addSubview:childView];
                break;
            }
        }
    });
}

/*
 * el_appkit_widget_remove_child — remove a child widget from its parent.
 */
void el_appkit_widget_remove_child(int64_t parent, int64_t child) {
    el_appkit_sync_main(^{
        ElWidget* cw = el_widget_get(child);
        if (!cw) return;
        NSView* v = (NSView*)cw->obj;
        if (![v isKindOfClass:[NSWindow class]]) {
            [v removeFromSuperview];
        }
    });
}

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

void el_appkit_widget_set_data(int64_t handle, const char* data_str) {
    ElWidget* w = el_widget_get(handle);
    if (!w) return;
    free(w->user_data);
    w->user_data = data_str && *data_str ? strdup(data_str) : NULL;
}

void el_appkit_widget_on_click(int64_t handle, const char* fn_name) {
    ElWidget* w = el_widget_get(handle);
    if (!w) return;
    free(w->cb_click);
    w->cb_click = fn_name && *fn_name ? strdup(fn_name) : NULL;
}

void el_appkit_widget_on_change(int64_t handle, const char* fn_name) {
    ElWidget* w = el_widget_get(handle);
    if (!w) return;
    free(w->cb_change);
    w->cb_change = fn_name && *fn_name ? strdup(fn_name) : NULL;
}

void el_appkit_widget_on_submit(int64_t handle, const char* fn_name) {
    /* For text fields, submit = pressing Enter → stored in cb_click slot. */
    el_appkit_widget_on_click(handle, fn_name);
}

/* ── Widget destroy ───────────────────────────────────────────────────────── */

void el_appkit_widget_destroy(int64_t handle) {
    el_appkit_sync_main(^{
        ElWidget* w = el_widget_get(handle);
        if (!w) return;
        NSView* v = (NSView*)w->obj;
        if (![v isKindOfClass:[NSWindow class]]) {
            [v removeFromSuperview];
        } else {
            [(NSWindow*)w->obj close];
        }
        [_el_delegates[handle] release];
        _el_delegates[handle] = nil;
        el_widget_free(handle);
    });
}
