// Pure UIKit + CAEAGLLayer + CADisplayLink backend for iOS.
//
// Linking is per-target via the game's build.sx (`opts.add_framework("UIKit")`
// + `opts.add_framework("OpenGLES")` + `opts.add_framework("QuartzCore")` on
// `.ios`). The file compiles cleanly on every target — the UIKit-touching
// bodies live behind `inline if OS == .ios` guards, so non-iOS builds never
// reach the unresolved Obj-C symbols.

#import "modules/std.sx";
#import "modules/std/objc.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/ui/types.sx";
#import "modules/ui/events.sx";
#import "modules/platform/types.sx";
#import "modules/platform/api.sx";

UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_class: *void) -> s32 #foreign;
dlsym             :: (handle: *void, name: [*]u8) -> *void #foreign;
chdir             :: (path: [*]u8) -> s32 #foreign;

// QuartzCore's wall-clock helper used by CoreAnimation. Seconds since boot,
// monotonic. We use it as the timebase for keyboard-inset lockstep so the
// per-frame interpolation lines up with UIKit's own animation timestamp.
CACurrentMediaTime :: () -> f64 #foreign;


// kEAGLRenderingAPIOpenGLES3 = 3
EAGL_API_GLES3 :: 3;

// CGFloat is a `double` on 64-bit Apple platforms; CGPoint = {x, y} fits in
// 16 bytes and returns via the FP-register path on arm64.
CGPoint :: struct { x: f64; y: f64; }

// UIEdgeInsets = {top, left, bottom, right} CGFloats — 32 bytes; exceeds the
// 16-byte registers cutoff so it returns via the x8 indirect-result-pointer
// convention. sx generates the right call sequence when the function-pointer
// type declares it as a by-value struct return.
UIEdgeInsets :: struct {
    top: f64;
    left: f64;
    bottom: f64;
    right: f64;
}

// CGRect for unwrapping NSValue-wrapped rects (e.g. keyboard end frame). Same
// 32-byte indirect-return shape as UIEdgeInsets.
CGRect :: struct {
    x: f64;
    y: f64;
    width: f64;
    height: f64;
}

// GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's
// loader path (they live on the framework's symbol table directly).
GL_RENDERBUFFER          :u32: 0x8D41;
GL_FRAMEBUFFER           :u32: 0x8D40;
GL_COLOR_ATTACHMENT0     :u32: 0x8CE0;
GL_FRAMEBUFFER_COMPLETE  :u32: 0x8CD5;

g_uikit_plat : *UIKitPlatform = null;

// Which GPU API the UIKit backend wires up. `.gles` keeps the existing
// CAEAGLLayer + EAGLContext + renderbuffer path; `.metal` swaps the view
// for a CAMetalLayer-backed one and leaves rendering to MetalGPU. Set
// before calling `init`; default `.gles` preserves prior behavior.
GpuMode :: enum {
    gles;
    metal;
}

UIKitPlatform :: struct {
    window: *void = null;   // UIWindow*
    root_vc: *void = null;  // UIViewController*
    gl_view: *void = null;  // SxGLView* OR SxMetalView* (depending on gpu_mode)
    gl_layer: *void = null; // CAEAGLLayer* OR CAMetalLayer* (= gl_view.layer)
    gl_ctx: *void = null;   // EAGLContext* (null in metal mode)
    display_link: *void = null;
    color_renderbuffer: u32 = 0;
    framebuffer: u32 = 0;
    gl_initialized: bool = false;
    gpu_mode: GpuMode = .gles;

    // Hidden UITextField; firstResponder ⇆ keyboard visibility.
    text_field: *void = null;

    viewport_w: f32 = 0.0;
    viewport_h: f32 = 0.0;
    pixel_w: s32 = 0;
    pixel_h: s32 = 0;
    dpi_scale: f32 = 1.0;

    delta_time: f32 = 0.016;
    // Latest CADisplayLink.targetTimestamp captured each tick — forwarded
    // through FrameContext to MetalGPU.end_frame() for presentDrawable:atTime:.
    last_target_ts: f64 = 0.0;

    frame_closure: Closure() = ---;
    has_frame_closure: bool = false;

    events: List(Event) = .{};

    safe_top: f32 = 0.0;
    safe_left: f32 = 0.0;
    safe_bottom: f32 = 0.0;
    safe_right: f32 = 0.0;

    keyboard_visible: bool = false;
    keyboard_height: f32 = 0.0;

    // Keyboard inset lockstep: when willChangeFrame fires we read the
    // animation duration from the notification's userInfo and interpolate
    // `keyboard_height` over that window on each display-link tick. Each
    // animation has a fresh start time and target — if a second event
    // arrives mid-animation, the next interpolation starts from the
    // currently-interpolated value (not from the previous animation's
    // origin). The easing is `smoothstep` (cubic Hermite) which closely
    // approximates UIKit's keyboard curve to within a frame at the
    // standard 0.25s slide duration.
    kb_anim_from: f32 = 0.0;
    kb_anim_to: f32 = 0.0;
    kb_anim_start: f64 = 0.0;
    kb_anim_dur: f64 = 0.0;
    kb_anim_curve: u64 = 0;
    kb_animating: bool = false;

    saved_title: [*]u8 = null;
}

impl Platform for UIKitPlatform {
    init :: (self: *UIKitPlatform, title: [:0]u8, w: s32, h: s32) -> bool {
        self.dpi_scale = 1.0;
        self.delta_time = 0.016;
        self.has_frame_closure = false;
        self.gl_initialized = false;
        self.keyboard_visible = false;
        self.keyboard_height = 0.0;
        self.saved_title = title.ptr;
        g_uikit_plat = self;

        // iOS apps start with CWD=/. chdir to the bundle's resourcePath so the
        // game's relative `fopen("assets/...")` calls find their data — must
        // happen BEFORE any code that loads fonts/textures from disk.
        inline if OS == .ios {
            uikit_chdir_to_bundle();
            uikit_register_classes();
            if self.gpu_mode == .gles {
                uikit_create_gl_context(self);
            } else {
                // Metal mode: skip EAGL. dpi_scale still needs to be known
                // before the window exists so callers can size resources.
                uikit_read_screen_scale(self);
            }
        }
        true;
    }

    run_frame_loop :: (self: *UIKitPlatform, frame_fn: Closure()) {
        self.frame_closure = frame_fn;
        self.has_frame_closure = true;
        g_uikit_plat = self;
        inline if OS == .ios {
            UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr));
        }
    }

    poll_events :: (self: *UIKitPlatform) -> []Event {
        result : []Event = ---;
        result.ptr = self.events.items;
        result.len = self.events.len;
        self.events.len = 0;
        result;
    }

    begin_frame :: (self: *UIKitPlatform) -> FrameContext {
        inline if OS == .ios {
            uikit_refresh_safe_insets(self);
        }
        FrameContext.{
            viewport_w = self.viewport_w,
            viewport_h = self.viewport_h,
            pixel_w = self.pixel_w,
            pixel_h = self.pixel_h,
            dpi_scale = self.dpi_scale,
            delta_time = self.delta_time,
            target_present_time = self.last_target_ts,
        };
    }

    end_frame :: (self: *UIKitPlatform) {
        inline if OS == .ios {
            if self.gpu_mode == .gles {
                uikit_present_renderbuffer(self);
            }
            // Metal mode: caller's gpu.end_frame() handles present.
        }
    }

    safe_insets :: (self: *UIKitPlatform) -> EdgeInsets {
        bottom := self.safe_bottom;
        if self.keyboard_visible {
            if self.keyboard_height > bottom { bottom = self.keyboard_height; }
        }
        EdgeInsets.{
            top = self.safe_top,
            left = self.safe_left,
            bottom = bottom,
            right = self.safe_right,
        };
    }

    keyboard :: (self: *UIKitPlatform) -> KeyboardState {
        KeyboardState.{
            visible = self.keyboard_visible,
            height = self.keyboard_height,
        };
    }

    show_keyboard :: (self: *UIKitPlatform) {
        inline if OS == .ios {
            if self.text_field == null { return; }
            #objc_call(bool)(self.text_field, "becomeFirstResponder");
        }
    }

    hide_keyboard :: (self: *UIKitPlatform) {
        inline if OS == .ios {
            if self.text_field == null { return; }
            #objc_call(bool)(self.text_field, "resignFirstResponder");
        }
    }

    stop :: (self: *UIKitPlatform) { }

    shutdown :: (self: *UIKitPlatform) { }
}

// dlsym(RTLD_DEFAULT, name) — Apple platforms. RTLD_DEFAULT is (void*)-2.
// callconv(.c) so this is callable from `load_gl`'s C-conv proc-loader slot.
ios_gl_proc :: (name: [*]u8) -> *void callconv(.c) {
    rtld_default : *void = xx (0 - 2);
    dlsym(rtld_default, name);
}

// Read a `extern NSString * const k...` global from the loaded image. The
// extern variable holds a pointer to the NSString instance — dlsym returns
// the address of that variable, which we dereference.
uikit_extern_nsstring :: (name: [*]u8) -> *void {
    rtld_default : *void = xx (0 - 2);
    p := dlsym(rtld_default, name);
    if p == null { return null; }
    pp : **void = xx p;
    pp.*;
}

// ───────────────────────────────────────────────────────────────────────────
// iOS-only helpers — only reachable from `inline if OS == .ios` call sites,
// so non-iOS builds never reference the unresolved UIKit symbols below.
// ───────────────────────────────────────────────────────────────────────────

uikit_refresh_safe_insets :: (plat: *UIKitPlatform) {
    inline if OS != .ios { return; }
    if plat.gl_view == null { return; }

    i := #objc_call(UIEdgeInsets)(plat.gl_view, "safeAreaInsets");
    plat.safe_top    = xx i.top;
    plat.safe_left   = xx i.left;
    plat.safe_bottom = xx i.bottom;
    plat.safe_right  = xx i.right;
}

uikit_chdir_to_bundle :: () {
    inline if OS != .ios { return; }
    NSBundle := objc_getClass("NSBundle".ptr);
    bundle := #objc_call(*void)(NSBundle, "mainBundle");
    rsrc := #objc_call(*void)(bundle, "resourcePath");
    if rsrc == null { return; }
    chdir(c_string(rsrc));
}

uikit_register_classes :: () {
    inline if OS == .ios {
        UIResponder := objc_getClass("UIResponder".ptr);
        SxAppDelegate := objc_allocateClassPair(UIResponder, "SxAppDelegate".ptr, 0);

        class_addMethod(SxAppDelegate,
            sel_registerName("application:didFinishLaunchingWithOptions:".ptr),
            xx uikit_did_finish_launching, "c@:@@".ptr);
        class_addMethod(SxAppDelegate,
            sel_registerName("sxKeyboardWillChangeFrame:".ptr),
            xx uikit_keyboard_will_change_frame, "v@:@".ptr);

        objc_registerClassPair(SxAppDelegate);

        // SxSceneDelegate handles the per-scene UI setup. iOS 13+ scene-based
        // lifecycle: didFinishLaunching is too early for the window — the
        // UIWindowScene doesn't connect until `scene:willConnectTo:options:`.
        // The class is named in Info.plist's UIApplicationSceneManifest →
        // UISceneDelegateClassName.
        SxSceneDelegate := objc_allocateClassPair(UIResponder, "SxSceneDelegate".ptr, 0);

        class_addMethod(SxSceneDelegate,
            sel_registerName("scene:willConnectToSession:options:".ptr),
            xx uikit_scene_will_connect, "v@:@@@".ptr);
        class_addMethod(SxSceneDelegate,
            sel_registerName("window".ptr),
            xx uikit_window_getter, "@@:".ptr);
        class_addMethod(SxSceneDelegate,
            sel_registerName("setWindow:".ptr),
            xx uikit_window_setter, "v@:@".ptr);

        // Formal protocol conformance is required for UISceneDelegate
        // (iOS checks -[cls conformsToProtocol:@protocol(UISceneDelegate)]
        // before instantiating; without it the class is silently rejected
        // with "does not conform to the UISceneDelegate protocol" in the
        // log and a default scene with no delegate is created instead).
        // Add the protocol BEFORE registerClassPair — the runtime locks
        // the class layout after registration.
        UISceneDelegateProto       := objc_getProtocol("UISceneDelegate".ptr);
        UIWindowSceneDelegateProto := objc_getProtocol("UIWindowSceneDelegate".ptr);
        if UISceneDelegateProto != null {
            class_addProtocol(SxSceneDelegate, UISceneDelegateProto);
        } else {
            NSLog(ns_string("[sx] WARN: UISceneDelegate protocol not found (dead-stripped)\n".ptr));
        }
        if UIWindowSceneDelegateProto != null {
            class_addProtocol(SxSceneDelegate, UIWindowSceneDelegateProto);
        }

        objc_registerClassPair(SxSceneDelegate);

        uikit_register_gl_view_class();
        uikit_register_metal_view_class();
    }
}

// Read [UIScreen mainScreen].nativeScale into plat.dpi_scale. Used by the
// metal-mode init path which doesn't go through uikit_create_gl_context
// (that's where the gles path picks the scale up).
uikit_read_screen_scale :: (plat: *UIKitPlatform) {
    inline if OS != .ios { return; }
    UIScreen := objc_getClass("UIScreen".ptr);
    screen := #objc_call(*void)(UIScreen, "mainScreen");
    scale_d := #objc_call(f64)(screen, "nativeScale");
    plat.dpi_scale = xx scale_d;
}

// NSNotification callback. The notification's userInfo dict has the
// keyboard's end-frame and animation curve/duration.
//   UIKeyboardFrameEndUserInfoKey       → NSValue wrapping CGRect (screen coords)
//   UIKeyboardAnimationDurationUserInfoKey → NSNumber wrapping double
// Keyboard height = how much of the screen the keyboard covers from the bottom.
// If the keyboard's end Y >= screen.height, the keyboard is offscreen (hiding).
uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *void) callconv(.c) {
    if g_uikit_plat == null { return; }
    plat := g_uikit_plat;

    user_info := #objc_call(*void)(notification, "userInfo");
    if user_info == null { return; }

    end_value := #objc_call(*void)(user_info, "objectForKey:",
        ns_string("UIKeyboardFrameEndUserInfoKey".ptr));
    if end_value == null { return; }
    end_rect := #objc_call(CGRect)(end_value, "CGRectValue");

    dur_value := #objc_call(*void)(user_info, "objectForKey:",
        ns_string("UIKeyboardAnimationDurationUserInfoKey".ptr));
    anim_dur : f64 = 0.0;
    if dur_value != null { anim_dur = #objc_call(f64)(dur_value, "doubleValue"); }

    curve_value := #objc_call(*void)(user_info, "objectForKey:",
        ns_string("UIKeyboardAnimationCurveUserInfoKey".ptr));
    curve_int : u64 = 0;
    if curve_value != null { curve_int = #objc_call(u64)(curve_value, "unsignedLongValue"); }

    // Screen height in points. The window lives on the connected scene's screen.
    if plat.window == null { return; }
    win_screen := #objc_call(*void)(plat.window, "screen");
    screen_bounds := #objc_call(CGRect)(win_screen, "bounds");

    // Keyboard height = how much of the screen the keyboard covers from the
    // bottom. When the keyboard is hiding, its end-frame.y == screen.height,
    // so the height comes out 0 and visible becomes false.
    kb_top : f64 = end_rect.y;
    sh     : f64 = screen_bounds.height;
    h := sh - kb_top;
    if h < 0.0 { h = 0.0; }
    if h > sh  { h = sh;  }
    target_h : f32 = xx h;

    plat.keyboard_visible = h > 0.5;

    if anim_dur <= 0.0 {
        // No animation window — snap.
        plat.keyboard_height = target_h;
        plat.kb_animating = false;
        return;
    }

    // Capture the animation params for the sx-side per-tick interpolation
    // that drives `keyboard_height` (consumers like the chess UI's safe-area
    // calc read it each frame). The interpolation uses cubic ease-out as a
    // close approximation of UIKit's keyboard curve. For perfect lockstep
    // on a UIView consumer in user code, drive a property via
    // `[UIView animateWithDuration:plat.kb_anim_dur delay:0
    //                       options:(plat.kb_anim_curve << 16) | 4
    //                    animations:^{ ... }
    //                    completion:nil]`
    // — UIKit's internal options-to-CAMediaTimingFunction table handles
    // even the private keyboard curve 7 correctly when packed this way.
    plat.kb_anim_from = plat.keyboard_height;
    plat.kb_anim_to = target_h;
    plat.kb_anim_start = CACurrentMediaTime();
    plat.kb_anim_dur = anim_dur;
    plat.kb_anim_curve = curve_int;
    plat.kb_animating = true;
}

uikit_create_gl_context :: (plat: *UIKitPlatform) {
    inline if OS != .ios { return; }

    EAGLContext := objc_getClass("EAGLContext".ptr);
    UIScreen    := objc_getClass("UIScreen".ptr);

    // Read the screen scale up-front so callers can size font caches and
    // textures with the right DPI before the window even exists.
    screen := #objc_call(*void)(UIScreen, "mainScreen");
    scale_d := #objc_call(f64)(screen, "nativeScale");
    plat.dpi_scale = xx scale_d;

    ctx_raw := #objc_call(*void)(EAGLContext, "alloc");
    plat.gl_ctx = #objc_call(*void)(ctx_raw, "initWithAPI:", EAGL_API_GLES3);
    #objc_call(void)(EAGLContext, "setCurrentContext:", plat.gl_ctx);

    load_gl(@ios_gl_proc);
}

uikit_window_getter :: (self: *void, _cmd: *void) -> *void callconv(.c) {
    if g_uikit_plat == null { return xx 0; }
    g_uikit_plat.window;
}

uikit_window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) {
    if g_uikit_plat == null { return; }
    g_uikit_plat.window = w;
}

uikit_did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) {
    inline if OS == .ios {
        if g_uikit_plat != null {
            uikit_subscribe_keyboard_notifications(self);
        }
    }
    1;
}

uikit_subscribe_keyboard_notifications :: (delegate: *void) {
    inline if OS != .ios { return; }
    NSNotificationCenter := objc_getClass("NSNotificationCenter".ptr);
    center := #objc_call(*void)(NSNotificationCenter, "defaultCenter");
    #objc_call(void)(center, "addObserver:selector:name:object:",
        delegate,
        sel_registerName("sxKeyboardWillChangeFrame:".ptr),
        ns_string("UIKeyboardWillChangeFrameNotification".ptr),
        xx 0);
}

uikit_scene_will_connect :: (self: *void, _cmd: *void, scene: *void, session: *void, options: *void) callconv(.c) {
    inline if OS == .ios {
        uikit_scene_will_connect_ios(self, scene);
    }
}

uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
    if g_uikit_plat == null {
        NSLog(ns_string("[sx] no platform\n".ptr));
        return;
    }
    plat := g_uikit_plat;

    UIWindow         := objc_getClass("UIWindow".ptr);
    UIViewController := objc_getClass("UIViewController".ptr);
    SxGLView         := objc_getClass("SxGLView".ptr);
    SxMetalView      := objc_getClass("SxMetalView".ptr);
    CADisplayLink    := objc_getClass("CADisplayLink".ptr);
    NSRunLoop        := objc_getClass("NSRunLoop".ptr);

    win_raw := #objc_call(*void)(UIWindow, "alloc");
    plat.window = #objc_call(*void)(win_raw, "initWithWindowScene:", scene);

    // Make the scene delegate own the window so iOS retains it. Per the
    // scene-based lifecycle, the scene delegate is expected to provide the
    // UIWindow via -window/-setWindow:.
    #objc_call(void)(delegate, "setWindow:", plat.window);

    vc_raw := #objc_call(*void)(UIViewController, "alloc");
    plat.root_vc = #objc_call(*void)(vc_raw, "init");

    // Allocate either SxGLView or SxMetalView based on gpu_mode and install
    // it as the VC's view. The view's +layerClass override gives us the
    // right CAEAGLLayer / CAMetalLayer subclass. Setting it BEFORE
    // setRootViewController avoids the VC lazy-loading a default view.
    view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView;
    glv_raw := #objc_call(*void)(view_class, "alloc");
    plat.gl_view = #objc_call(*void)(glv_raw, "init");
    #objc_call(void)(plat.root_vc, "setView:", plat.gl_view);

    #objc_call(void)(plat.window, "setRootViewController:", plat.root_vc);

    plat.gl_layer = #objc_call(*void)(plat.gl_view, "layer");

    // Mark the layer opaque (no compositor blend). Required for EAGL +
    // recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't
    // hurt to be explicit).
    #objc_call(void)(plat.gl_layer, "setOpaque:", xx 1);

    if plat.gpu_mode == .gles {
        // EAGL drawable properties dict required by
        // EAGLContext.renderbufferStorage:fromDrawable: (color format,
        // non-retained backing). Without this dict the renderbuffer
        // allocation silently fails and the framebuffer reports INCOMPLETE.
        NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr);
        NSNumber            := objc_getClass("NSNumber".ptr);

        ns_no := #objc_call(*void)(NSNumber, "numberWithBool:", xx 0);

        // The EAGL dict keys/values must be the framework-provided NSString
        // constants (pointer identity is checked) — dlsym them from OpenGLES.
        retained_key   := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr);
        colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
        rgba8_value    := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);

        dict := #objc_call(*void)(NSMutableDictionary, "dictionary");
        #objc_call(void)(dict, "setObject:forKey:", ns_no,       retained_key);
        #objc_call(void)(dict, "setObject:forKey:", rgba8_value, colorformat_key);
        #objc_call(void)(plat.gl_layer, "setDrawableProperties:", dict);
    }

    // EAGLContext + load_gl were already done in uikit_create_gl_context()
    // back when the game's main called plat.init() — so shaders/textures
    // built before the window exists already work.

    // Match the layer's drawable scale to the screen's native scale so we get
    // pixel-accurate rendering on retina displays. CGFloat is `double` on
    // 64-bit Apple platforms; reading as f32 would clobber the value.
    screen := #objc_call(*void)(plat.window, "screen");
    scale  := #objc_call(f64)(screen, "nativeScale");
    plat.dpi_scale = xx scale;
    #objc_call(void)(plat.gl_view, "setContentScaleFactor:", scale);

    // Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once
    // the layer has its real on-screen bounds. makeKeyAndVisible triggers
    // a layout pass; layoutSubviews calls uikit_setup_renderbuffer.
    #objc_call(void)(plat.window, "makeKeyAndVisible");

    // Hidden UITextField as the firstResponder source for show_keyboard /
    // hide_keyboard. Lives as a subview of the GL view so it's in the
    // responder chain but is sized 0×0 so it can't be tapped.
    UITextField := objc_getClass("UITextField".ptr);
    tf_raw := #objc_call(*void)(UITextField, "alloc");
    plat.text_field = #objc_call(*void)(tf_raw, "init");
    #objc_call(void)(plat.gl_view, "addSubview:", plat.text_field);


    // (Keyboard observer is registered in didFinishLaunching via
    //  uikit_subscribe_keyboard_notifications — it's app-level, not scene-
    //  level, so it doesn't belong here.)

    // CADisplayLink: vsync-driven tick into our SxGLView. The second arg is
    // a SEL value (not a dispatch selector), so it still goes through
    // sel_registerName.
    sel_tick := sel_registerName("sxTick:".ptr);
    plat.display_link = #objc_call(*void)(CADisplayLink, "displayLinkWithTarget:selector:", plat.gl_view, sel_tick);
    runloop := #objc_call(*void)(NSRunLoop, "currentRunLoop");
    mode := ns_string("kCFRunLoopDefaultMode".ptr);
    #objc_call(void)(plat.display_link, "addToRunLoop:forMode:", runloop, mode);

    NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr));
}

// Allocate the color renderbuffer + framebuffer and bind them. The renderbuffer
// gets its pixel storage from the CAEAGLLayer via
// `[ctx renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]`, so the layer
// must already be in the view hierarchy at the right size when this runs.
uikit_setup_renderbuffer :: (plat: *UIKitPlatform) {
    inline if OS != .ios { return; }

    glGenFramebuffers(1, @plat.framebuffer);
    glGenRenderbuffers(1, @plat.color_renderbuffer);

    glBindRenderbuffer(GL_RENDERBUFFER, plat.color_renderbuffer);

    #objc_call(bool)(plat.gl_ctx, "renderbufferStorage:fromDrawable:", GL_RENDERBUFFER, plat.gl_layer);

    glBindFramebuffer(GL_FRAMEBUFFER, plat.framebuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, plat.color_renderbuffer);

    // Query the actual pixel dimensions from the renderbuffer.
    pw : s32 = 0;
    ph : s32 = 0;
    GL_RENDERBUFFER_WIDTH  :u32: 0x8D42;
    GL_RENDERBUFFER_HEIGHT :u32: 0x8D43;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH,  @pw);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, @ph);
    plat.pixel_w = pw;
    plat.pixel_h = ph;
    plat.viewport_w = xx pw;
    plat.viewport_h = xx ph;
    if plat.dpi_scale > 0.0 {
        plat.viewport_w = plat.viewport_w / plat.dpi_scale;
        plat.viewport_h = plat.viewport_h / plat.dpi_scale;
    }

    glViewport(0, 0, pw, ph);

    status := glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if status != GL_FRAMEBUFFER_COMPLETE {
        NSLog(ns_string("[sx] framebuffer incomplete after renderbuffer setup\n".ptr));
    }
}

uikit_present_renderbuffer :: (self: *UIKitPlatform) {
    inline if OS != .ios { return; }
    glBindRenderbuffer(GL_RENDERBUFFER, self.color_renderbuffer);
    #objc_call(bool)(self.gl_ctx, "presentRenderbuffer:", GL_RENDERBUFFER);
}

// ── SxGLView class ─────────────────────────────────────────────────────────
// UIView subclass overriding `+layerClass` to return [CAEAGLLayer class].
// Instance method `sxTick:` is what CADisplayLink calls.

uikit_gl_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
    objc_getClass("CAEAGLLayer".ptr);
}

uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) {
    if g_uikit_plat == null { return; }
    plat := g_uikit_plat;

    // Keyboard-inset lockstep — sx-side cubic ease-out approximation of
    // UIKit's private keyboard curve. Sample targetTimestamp so we
    // interpolate at the time this frame will be visible. Lags by ~1
    // frame behind UIKit because UIKit's keyboard is rendered in a
    // separate process (UIRemoteKeyboardWindow) and we can't perfectly
    // sync to it from outside that scene. Refinements tried:
    // CATransaction.flush, CABasicAnimation, presentationLayer reading,
    // and keyboardLayoutGuide — none eliminated the lag without
    // cascade-breaking the GL view's frame.
    if plat.kb_animating {
        target_ts := #objc_call(f64)(link, "targetTimestamp");
        elapsed := target_ts - plat.kb_anim_start;
        // Negative elapsed can happen if the just-fired willChangeFrame
        // set kb_anim_start to a wall time AFTER the tick already
        // captured its targetTimestamp this frame. Without the clamp,
        // t < 0 makes the cubic ease-out *overshoot* in the opposite
        // direction (visible as the indicator briefly jumping past the
        // keyboard on close, then animating back).
        if elapsed < 0.0 { elapsed = 0.0; }
        if elapsed >= plat.kb_anim_dur or plat.kb_anim_dur <= 0.0 {
            plat.keyboard_height = plat.kb_anim_to;
            plat.kb_animating = false;
        } else {
            t : f32 = xx (elapsed / plat.kb_anim_dur);
            inv := 1.0 - t;
            eased := 1.0 - inv * inv * inv;
            plat.keyboard_height = plat.kb_anim_from + (plat.kb_anim_to - plat.kb_anim_from) * eased;
        }
    }

    // Indicator's position is driven by UIView.animateWithDuration kicked
    // off from willChangeFrame — it animates in lockstep with UIKit's
    // keyboard using the same curve+duration. No per-tick setFrame here.

    if !plat.has_frame_closure { return; }
    if !plat.gl_initialized { return; }

    dur_d := #objc_call(f64)(link, "duration");
    plat.delta_time = xx dur_d;
    // Stash the targetTimestamp so begin_frame can hand it down to the
    // game in FrameContext for Metal presentDrawable:atTime:.
    plat.last_target_ts = #objc_call(f64)(link, "targetTimestamp");

    fn := plat.frame_closure;
    fn();
}

uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) {
    // Call super first via objc_msgSendSuper would be cleaner, but UIView's
    // default layoutSubviews is a no-op anyway.
    if g_uikit_plat == null { return; }
    plat := g_uikit_plat;
    if plat.gl_initialized { return; }
    if plat.gpu_mode == .gles {
        uikit_setup_renderbuffer(plat);
    } else {
        uikit_compute_layer_pixel_size(plat);
    }
    plat.gl_initialized = true;
}

// Metal mode equivalent of uikit_setup_renderbuffer's "tell me how big the
// drawable is in pixels". Reads the layer's bounds in points and scales to
// pixels via dpi_scale. CAMetalLayer.drawableSize is set by MetalGPU.init
// based on these dims.
uikit_compute_layer_pixel_size :: (plat: *UIKitPlatform) {
    inline if OS != .ios { return; }
    if plat.gl_view == null { return; }

    b := #objc_call(CGRect)(plat.gl_view, "bounds");

    w_pts : f64 = b.width;
    h_pts : f64 = b.height;
    plat.viewport_w = xx w_pts;
    plat.viewport_h = xx h_pts;

    scale64 : f64 = xx plat.dpi_scale;
    pw : f64 = w_pts * scale64;
    ph : f64 = h_pts * scale64;
    plat.pixel_w = xx pw;
    plat.pixel_h = xx ph;
}

// Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an
// NSSet<UITouch *> + UIEvent. We take the first touch (single-touch model
// matching the chess game's drag-and-tap UX) and push the resulting
// Event into the platform's queue for the next poll_events drain.

uikit_touch_location :: (touch: *void, view: *void) -> Point {
    p := #objc_call(CGPoint)(touch, "locationInView:", view);
    Point.{ x = xx p.x, y = xx p.y };
}

uikit_first_touch :: (touches: *void) -> *void {
    #objc_call(*void)(touches, "anyObject");
}

uikit_gl_view_touches_began :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) {
    if g_uikit_plat == null { return; }
    touch := uikit_first_touch(touches);
    if touch == null { return; }
    pos := uikit_touch_location(touch, self);
    g_uikit_plat.events.append(.mouse_down(.{ position = pos, button = .left }));
}

uikit_gl_view_touches_moved :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) {
    if g_uikit_plat == null { return; }
    touch := uikit_first_touch(touches);
    if touch == null { return; }
    pos := uikit_touch_location(touch, self);
    g_uikit_plat.events.append(.mouse_moved(.{ position = pos, delta = Point.zero() }));
}

uikit_gl_view_touches_ended :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) {
    if g_uikit_plat == null { return; }
    touch := uikit_first_touch(touches);
    if touch == null { return; }
    pos := uikit_touch_location(touch, self);
    g_uikit_plat.events.append(.mouse_up(.{ position = pos, button = .left }));
}

uikit_register_gl_view_class :: () {
    inline if OS == .ios {
        UIView := objc_getClass("UIView".ptr);
        SxGLView := objc_allocateClassPair(UIView, "SxGLView".ptr, 0);

        // +layerClass is a CLASS method — registered on the metaclass.
        metaclass := object_getClass(SxGLView);
        class_addMethod(metaclass,
            sel_registerName("layerClass".ptr),
            xx uikit_gl_view_layer_class, "#@:".ptr);

        // -sxTick: is the CADisplayLink callback. -layoutSubviews allocates
        // the renderbuffer when the layer first gets non-zero bounds.
        class_addMethod(SxGLView,
            sel_registerName("sxTick:".ptr),
            xx uikit_gl_view_tick, "v@:@".ptr);
        class_addMethod(SxGLView,
            sel_registerName("layoutSubviews".ptr),
            xx uikit_gl_view_layout, "v@:".ptr);

        // Touch dispatch.
        class_addMethod(SxGLView,
            sel_registerName("touchesBegan:withEvent:".ptr),
            xx uikit_gl_view_touches_began, "v@:@@".ptr);
        class_addMethod(SxGLView,
            sel_registerName("touchesMoved:withEvent:".ptr),
            xx uikit_gl_view_touches_moved, "v@:@@".ptr);
        class_addMethod(SxGLView,
            sel_registerName("touchesEnded:withEvent:".ptr),
            xx uikit_gl_view_touches_ended, "v@:@@".ptr);
        class_addMethod(SxGLView,
            sel_registerName("touchesCancelled:withEvent:".ptr),
            xx uikit_gl_view_touches_ended, "v@:@@".ptr);

        objc_registerClassPair(SxGLView);
    }
}

// +layerClass IMP for SxMetalView. Class method, signature "#@:".
uikit_metal_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
    objc_getClass("CAMetalLayer".ptr);
}

// SxMetalView reuses the same tick/layout/touch IMPs as SxGLView. The IMPs
// already branch on `plat.gpu_mode` for the GL-specific bits (renderbuffer
// setup, etc.), so a single set of IMPs serves both view classes.
uikit_register_metal_view_class :: () {
    inline if OS == .ios {
        UIView := objc_getClass("UIView".ptr);
        SxMetalView := objc_allocateClassPair(UIView, "SxMetalView".ptr, 0);

        metaclass := object_getClass(SxMetalView);
        class_addMethod(metaclass,
            sel_registerName("layerClass".ptr),
            xx uikit_metal_view_layer_class, "#@:".ptr);

        class_addMethod(SxMetalView,
            sel_registerName("sxTick:".ptr),
            xx uikit_gl_view_tick, "v@:@".ptr);
        class_addMethod(SxMetalView,
            sel_registerName("layoutSubviews".ptr),
            xx uikit_gl_view_layout, "v@:".ptr);

        class_addMethod(SxMetalView,
            sel_registerName("touchesBegan:withEvent:".ptr),
            xx uikit_gl_view_touches_began, "v@:@@".ptr);
        class_addMethod(SxMetalView,
            sel_registerName("touchesMoved:withEvent:".ptr),
            xx uikit_gl_view_touches_moved, "v@:@@".ptr);
        class_addMethod(SxMetalView,
            sel_registerName("touchesEnded:withEvent:".ptr),
            xx uikit_gl_view_touches_ended, "v@:@@".ptr);
        class_addMethod(SxMetalView,
            sel_registerName("touchesCancelled:withEvent:".ptr),
            xx uikit_gl_view_touches_ended, "v@:@@".ptr);

        objc_registerClassPair(SxMetalView);
    }
}
