// Pure-NDK NativeActivity backend for Android.
//
// Linking is per-target via the consumer's build.sx — for Android we
// don't need explicit framework adds because `sx build --target android`
// already links -llog -landroid -lEGL -lGLESv3 and bundles
// native_app_glue.c. The file compiles cleanly on every target because
// every NDK-touching path is gated by `inline if OS == .android`.
//
// Entry-point contract (deliberately kept symmetric with iOS):
//   - The user writes both `main` and `android_main` at top level.
//   - The user's `android_main(app)` calls `sx_android_bootstrap(app)`
//     once (to stash the NDK app pointer in this module's globals),
//     then calls `main()`. The body of `android_main` should be
//     `inline if OS == .android`-gated; on non-Android targets it
//     compiles to dead code, harmless because nothing references the
//     symbol.
//   - User's `main` constructs an `AndroidPlatform`, calls `init`,
//     then `run_frame_loop(closure)` which drives the looper until
//     destroyRequested. This file never names `main`.

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

// JNI bridge for system-bar inset queries. The .c lives in the library's
// vendor area; the compiler resolves the `#source` path through the
// stdlib search list so consumers don't need to vendor a copy.
#import c {
    #source "vendors/sx_android_jni/sx_android_jni.c";
};

// ── Foreign declarations ────────────────────────────────────────────────

__android_log_print :: (prio: s32, tag: *u8, fmt: *u8) -> s32 #foreign;
usleep :: (us: u32) -> s32 #foreign;

ALooper_pollOnce :: (ms: s32, outFd: *s32, outEvents: *s32, outData: **void) -> s32 #foreign;

ANativeWindow_getWidth  :: (window: *void) -> s32 #foreign;
ANativeWindow_getHeight :: (window: *void) -> s32 #foreign;

AConfiguration_getDensity :: (config: *void) -> s32 #foreign;

// Input event APIs (libandroid). Touch motion arrives via the queued
// AInputEvent stream that native_app_glue pumps through `onInputEvent`.
AInputEvent_getType         :: (event: *void) -> s32 #foreign;
AMotionEvent_getAction      :: (event: *void) -> s32 #foreign;
AMotionEvent_getX           :: (event: *void, pointer_index: u64) -> f32 #foreign;
AMotionEvent_getY           :: (event: *void, pointer_index: u64) -> f32 #foreign;

// JNI/glue bridges from vendors/sx_android_jni/sx_android_jni.c.
sx_android_query_safe_insets        :: (activity: *void, top: *s32, left: *s32, bottom: *s32, right: *s32) -> void #foreign;
sx_android_install_input_handler    :: (app: *void, handler: (*void, *void) -> s32) -> void #foreign;

// EGL — display/surface/context/config are opaque to us.
eglGetDisplay         :: (display_id: *void) -> *void #foreign;
eglInitialize         :: (display: *void, major: *s32, minor: *s32) -> u32 #foreign;
eglChooseConfig       :: (display: *void, attrib_list: *s32, configs: **void, config_size: s32, num_config: *s32) -> u32 #foreign;
eglCreateContext      :: (display: *void, config: *void, share: *void, attrib_list: *s32) -> *void #foreign;
eglCreateWindowSurface:: (display: *void, config: *void, window: *void, attrib_list: *s32) -> *void #foreign;
eglMakeCurrent        :: (display: *void, draw: *void, read: *void, ctx: *void) -> u32 #foreign;
eglSwapBuffers        :: (display: *void, surface: *void) -> u32 #foreign;
eglDestroySurface     :: (display: *void, surface: *void) -> u32 #foreign;
eglDestroyContext     :: (display: *void, ctx: *void) -> u32 #foreign;
eglTerminate          :: (display: *void) -> u32 #foreign;

clock_gettime :: (clk_id: s32, ts: *void) -> s32 #foreign;

// ── Constants ──────────────────────────────────────────────────────────

EGL_NONE                   :s32: 0x3038;
EGL_SURFACE_TYPE           :s32: 0x3033;
EGL_WINDOW_BIT             :s32: 0x0004;
EGL_RENDERABLE_TYPE        :s32: 0x3040;
EGL_OPENGL_ES2_BIT         :s32: 0x0004;
EGL_OPENGL_ES3_BIT         :s32: 0x0040;
EGL_BLUE_SIZE              :s32: 0x3022;
EGL_GREEN_SIZE             :s32: 0x3023;
EGL_RED_SIZE               :s32: 0x3024;
EGL_DEPTH_SIZE             :s32: 0x3025;
EGL_CONTEXT_CLIENT_VERSION :s32: 0x3098;

// AInputEvent / AMotionEvent constants used by the input handler.
AINPUT_EVENT_TYPE_MOTION       :s32: 2;
AMOTION_EVENT_ACTION_MASK      :s32: 0xff;
AMOTION_EVENT_ACTION_DOWN      :s32: 0;
AMOTION_EVENT_ACTION_UP        :s32: 1;
AMOTION_EVENT_ACTION_MOVE      :s32: 2;
AMOTION_EVENT_ACTION_CANCEL    :s32: 3;

// CLOCK_MONOTONIC = 1 on linux/bionic. Used for delta_time + target_present_time.
CLOCK_MONOTONIC :s32: 1;

// android_app + android_poll_source field offsets (NDK 29, arm64).
// Recompute via offsetof() on a host with the NDK headers if you suspect
// the layout has changed (see examples/99-android-egl-clear.sx's notes).
APP_OFF_window           :s64: 72;
APP_OFF_destroyRequested :s64: 100;
APP_OFF_config           :s64: 32;  // AConfiguration* — follows activity (24).
SRC_OFF_process          :s64: 16;  // id(4) + pad(4) + app*(8) = process fn-ptr

// timespec on linux/aarch64: tv_sec (s64) + tv_nsec (s64).
TimeSpec :: struct { sec: s64; nsec: s64; }

// ── Globals ────────────────────────────────────────────────────────────
// `g_android_app` is populated by `sx_android_bootstrap` before
// `AndroidPlatform.run_frame_loop` reads it. `g_android_plat` is set
// by `AndroidPlatform.init` so future hooks (signal handlers, JNI
// callbacks) can find the live platform.

g_android_app  : *void = null;
g_android_plat : *AndroidPlatform = null;

// `app->activity` (ANativeActivity*) at byte 24.
// `activity->assetManager` at byte 64 inside ANativeActivity.
APP_OFF_activity            :s64: 24;
ACTIVITY_OFF_assetManager   :s64: 64;
ACTIVITY_OFF_internalData   :s64: 32;

// AAssetManager handle the user's `android_main` stashes via the
// bootstrap. Consumers that want to read bundled APK assets (font.ttf,
// piece sprites, level data, ...) read this and feed it into their own
// file-IO shim — e.g. chess wires it through `vendors/file_utils.c`
// which routes `read_file_bytes` through AAssetManager_open on Android.
g_android_asset_manager : *void = null;
g_android_internal_path : *u8   = null;
g_android_config        : *void = null;
g_android_activity      : *void = null;

// ── Bootstrap (called by user's `android_main`) ────────────────────────
// Stashes the NDK app pointer the OS handed to `android_main(app)` so
// the rest of the platform module can find it. Single responsibility —
// the user's `android_main` calls this once, then calls their own
// `main()` to enter the normal cross-platform setup flow.
//
// Also extracts `AAssetManager*` and `internalDataPath` from the
// ANativeActivity that the app pointer carries — these are the two
// pieces consumers need to touch APK assets / per-app storage on
// Android. Reachable as `g_android_asset_manager` / `g_android_internal_path`.

sx_android_bootstrap :: (app: *void) {
    inline if OS == .android {
        g_android_app = app;
        base : s64 = xx app;
        activity_pp : **void = xx (base + APP_OFF_activity);
        activity_ptr := activity_pp.*;
        g_android_activity = activity_ptr;
        if activity_ptr != null {
            act_base : s64 = xx activity_ptr;
            mgr_pp : **void = xx (act_base + ACTIVITY_OFF_assetManager);
            g_android_asset_manager = mgr_pp.*;
            path_pp : **u8 = xx (act_base + ACTIVITY_OFF_internalData);
            g_android_internal_path = path_pp.*;
        }
        cfg_pp : **void = xx (base + APP_OFF_config);
        g_android_config = cfg_pp.*;
    }
}

// ── Helpers ────────────────────────────────────────────────────────────

read_ptr :: (base: s64, off: s64) -> *void {
    inline if OS != .android { return null; }
    p : **void = xx (base + off);
    p.*;
}

read_s32 :: (base: s64, off: s64) -> s32 {
    inline if OS != .android { return 0; }
    p : *s32 = xx (base + off);
    p.*;
}

monotonic_seconds :: () -> f64 {
    inline if OS != .android { return 0.0; }
    ts : TimeSpec = .{};
    clock_gettime(CLOCK_MONOTONIC, xx @ts);
    (xx ts.sec) + (xx ts.nsec) / 1000000000.0;
}

// ── Platform implementation ────────────────────────────────────────────

AndroidPlatform :: struct {
    // EGL state — created when ANativeWindow arrives via the event loop.
    egl_display: *void = null;
    egl_surface: *void = null;
    egl_context: *void = null;
    egl_config:  *void = null;

    // Latest known dimensions reported by the window. Refreshed each frame
    // from ANativeWindow_getWidth/Height — cheap and avoids missing resize
    // events when the user rotates the device.
    pixel_w: s32 = 0;
    pixel_h: s32 = 0;
    dpi_scale: f32 = 1.0;
    delta_time: f32 = 0.016;
    last_frame_time: f64 = 0.0;

    // User's per-frame closure stored when run_frame_loop is called.
    // Optional sentinel-shape — `null` means "no closure yet".
    frame_closure: ?Closure() = null;

    events: List(Event) = .{};
    last_touch: Point = .{};
    touch_active: bool = false;
    stop_requested: bool = false;

    safe_top: f32 = 0.0;
    safe_left: f32 = 0.0;
    safe_bottom: f32 = 0.0;
    safe_right: f32 = 0.0;
    safe_insets_queried: bool = false;

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

impl Platform for AndroidPlatform {
    // title/w/h are advisory only — the OS owns the surface dimensions.
    // We register the platform globally so android_main path can find it.
    init :: (self: *AndroidPlatform, title: [:0]u8, w: s32, h: s32) -> bool {
        inline if OS != .android { return false; }
        g_android_plat = self;
        if g_android_config != null {
            density := AConfiguration_getDensity(g_android_config);
            if density > 0 {
                self.dpi_scale = xx density / 160.0;
            }
        }
        true;
    }

    run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) {
        inline if OS == .android {
            self.frame_closure = frame_fn;
            if g_android_app != null {
                sx_android_install_input_handler(g_android_app, sx_android_input_event);
            }
            android_run_loop(self);
        }
    }

    poll_events :: (self: *AndroidPlatform) -> []Event {
        out : []Event = .{ ptr = xx self.events.items, len = self.events.len };
        // Drain after exposing the slice — the user iterates synchronously
        // before returning to the run loop, and new touches won't arrive
        // until the next ALooper_pollOnce drains the input queue. Setting
        // len=0 keeps the backing buffer alive so the next append reuses
        // the same allocation.
        self.events.len = 0;
        out;
    }

    begin_frame :: (self: *AndroidPlatform) -> FrameContext {
        viewport_w_f : f32 = xx self.pixel_w;
        viewport_h_f : f32 = xx self.pixel_h;
        if self.dpi_scale > 0.0 {
            viewport_w_f = viewport_w_f / self.dpi_scale;
            viewport_h_f = viewport_h_f / self.dpi_scale;
        }
        FrameContext.{
            viewport_w = viewport_w_f,
            viewport_h = viewport_h_f,
            pixel_w    = self.pixel_w,
            pixel_h    = self.pixel_h,
            dpi_scale  = self.dpi_scale,
            delta_time = self.delta_time,
            target_present_time = 0.0,
        };
    }

    end_frame :: (self: *AndroidPlatform) {
        inline if OS == .android {
            if self.egl_surface != null {
                eglSwapBuffers(self.egl_display, self.egl_surface);
            }
        }
    }

    safe_insets :: (self: *AndroidPlatform) -> EdgeInsets {
        inline if OS == .android {
            // Query once after EGL is up — getRootWindowInsets() returns
            // null until the window has been attached, so calling at
            // bootstrap is too early.
            if !self.safe_insets_queried and g_android_activity != null and self.egl_surface != null {
                t : s32 = 0; l : s32 = 0; b : s32 = 0; r : s32 = 0;
                sx_android_query_safe_insets(g_android_activity, @t, @l, @b, @r);
                inv : f32 = if self.dpi_scale > 0.0 then 1.0 / self.dpi_scale else 1.0;
                self.safe_top    = xx t * inv;
                self.safe_left   = xx l * inv;
                self.safe_bottom = xx b * inv;
                self.safe_right  = xx r * inv;
                self.safe_insets_queried = true;
            }
        }
        EdgeInsets.{
            top    = self.safe_top,
            left   = self.safe_left,
            bottom = self.safe_bottom,
            right  = self.safe_right,
        };
    }

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

    show_keyboard :: (self: *AndroidPlatform) {
        // TODO: InputMethodManager.showSoftInput via JNI.
    }

    hide_keyboard :: (self: *AndroidPlatform) {
        // TODO: InputMethodManager.hideSoftInputFromWindow via JNI.
    }

    stop :: (self: *AndroidPlatform) {
        self.stop_requested = true;
    }

    shutdown :: (self: *AndroidPlatform) {
        inline if OS == .android {
            if self.egl_display != null {
                eglMakeCurrent(self.egl_display, null, null, null);
                if self.egl_surface != null {
                    eglDestroySurface(self.egl_display, self.egl_surface);
                }
                if self.egl_context != null {
                    eglDestroyContext(self.egl_display, self.egl_context);
                }
                eglTerminate(self.egl_display);
                self.egl_display = null;
                self.egl_surface = null;
                self.egl_context = null;
            }
        }
    }
}

// ── Internal: input bridge ─────────────────────────────────────────────
//
// native_app_glue's process_input loop calls this once per AInputEvent
// pulled off the input queue. Single touch only for now — point-down /
// move / point-up — translated to sx mouse_down / mouse_moved /
// mouse_up so the existing handle_event chain (drag, scroll, square
// selection) Just Works. Coordinates from AMotionEvent_get{X,Y} are
// physical pixels; divide by dpi_scale before publishing so the
// layout-side hit-testing matches its own logical-coord frames.
//
// Returns 1 (consumed) for motion events, 0 for everything else so
// native_app_glue still routes key events through the default handler.

sx_android_input_event :: (app: *void, event: *void) -> s32 {
    inline if OS != .android { return 0; }
    if event == null { return 0; }
    if g_android_plat == null { return 0; }

    if AInputEvent_getType(event) != AINPUT_EVENT_TYPE_MOTION {
        return 0;
    }

    plat := g_android_plat;
    raw_action := AMotionEvent_getAction(event);
    action := raw_action & AMOTION_EVENT_ACTION_MASK;

    px := AMotionEvent_getX(event, 0);
    py := AMotionEvent_getY(event, 0);
    inv : f32 = if plat.dpi_scale > 0.0 then 1.0 / plat.dpi_scale else 1.0;
    pos : Point = .{ x = px * inv, y = py * inv };

    if action == AMOTION_EVENT_ACTION_DOWN {
        plat.events.append(.mouse_down(.{ position = pos, button = .left }));
        plat.last_touch = pos;
        plat.touch_active = true;
    } else if action == AMOTION_EVENT_ACTION_MOVE {
        delta : Point = .{
            x = pos.x - plat.last_touch.x,
            y = pos.y - plat.last_touch.y,
        };
        plat.events.append(.mouse_moved(.{ position = pos, delta = delta }));
        plat.last_touch = pos;
    } else if action == AMOTION_EVENT_ACTION_UP {
        plat.events.append(.mouse_up(.{ position = pos, button = .left }));
        plat.touch_active = false;
    } else if action == AMOTION_EVENT_ACTION_CANCEL {
        plat.events.append(.mouse_up(.{ position = pos, button = .left }));
        plat.touch_active = false;
    }
    1;
}

// ── Internal: event loop + EGL bringup ─────────────────────────────────

android_run_loop :: (self: *AndroidPlatform) {
    inline if OS != .android { return; }
    if g_android_app == null { return; }

    app_base : s64 = xx g_android_app;
    out_fd     : s32   = 0;
    out_events : s32   = 0;
    out_data   : *void = null;

    self.last_frame_time = monotonic_seconds();

    while self.stop_requested == false {
        // Drain pending events. Non-blocking pollOnce is mandatory on
        // Pixel 7 Pro + Android 16 — pollOnce(-1) blows the stack inside
        // Looper::pollOnce there (see examples/99-android-egl-clear.sx's
        // notes for the investigation).
        ret := ALooper_pollOnce(0, @out_fd, @out_events, @out_data);
        if ret >= 0 {
            if out_data != null {
                src_base : s64 = xx out_data;
                process_addr := read_ptr(src_base, SRC_OFF_process);
                if process_addr != null {
                    process_fn : (*void, *void) -> void = xx process_addr;
                    process_fn(g_android_app, out_data);
                }
            }
        }

        if read_s32(app_base, APP_OFF_destroyRequested) != 0 { break; }

        window := read_ptr(app_base, APP_OFF_window);
        if window != null and self.egl_surface == null {
            if android_setup_egl(self, window) == false { break; }
        }

        if self.egl_surface != null {
            // Refresh dimensions every tick — handles rotations cleanly.
            self.pixel_w = ANativeWindow_getWidth(window);
            self.pixel_h = ANativeWindow_getHeight(window);

            now := monotonic_seconds();
            dt : f64 = now - self.last_frame_time;
            if dt > 0.0 { self.delta_time = xx dt; }
            self.last_frame_time = now;

            // `if let`-style unwrap: only invoke if the closure was set.
            // User's frame closure is expected to call plat.end_frame()
            // which swaps — matches uikit.sx's begin/end contract.
            if frame_fn := self.frame_closure {
                frame_fn();
            }
        }

        usleep(16000);
    }
}

android_setup_egl :: (self: *AndroidPlatform, window: *void) -> bool {
    inline if OS != .android { return false; }

    self.egl_display = eglGetDisplay(null);
    if self.egl_display == null { return false; }

    major : s32 = 0;
    minor : s32 = 0;
    if eglInitialize(self.egl_display, @major, @minor) == 0 { return false; }

    attribs : [13]s32 = .{
        EGL_SURFACE_TYPE,    EGL_WINDOW_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_BLUE_SIZE,       8,
        EGL_GREEN_SIZE,      8,
        EGL_RED_SIZE,        8,
        EGL_DEPTH_SIZE,      0,
        EGL_NONE,
    };
    num_config : s32 = 0;
    if eglChooseConfig(self.egl_display, @attribs[0], @self.egl_config, 1, @num_config) == 0 or num_config < 1 {
        return false;
    }

    ctx_attribs : [3]s32 = .{ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
    self.egl_context = eglCreateContext(self.egl_display, self.egl_config, null, @ctx_attribs[0]);
    if self.egl_context == null { return false; }

    self.egl_surface = eglCreateWindowSurface(self.egl_display, self.egl_config, window, null);
    if self.egl_surface == null { return false; }

    if eglMakeCurrent(self.egl_display, self.egl_surface, self.egl_surface, self.egl_context) == 0 {
        return false;
    }

    self.pixel_w = ANativeWindow_getWidth(window);
    self.pixel_h = ANativeWindow_getHeight(window);
    true;
}
