// GLES3 backend for the GPU protocol. Android today.
//
// We use opengl.sx's runtime-loaded function-pointer variables for every
// GL call, populated via `load_gl(@eglGetProcAddress)` in `init()`.
// `#foreign` decls for the same names would collide with opengl.sx's
// global variables and silently win — that produces NULL function-
// pointer calls (PC=0 crash) because the variables are uninitialized.
//
// Conventions:
//   - One shared VAO with the renderer's fixed 12-float layout
//     (pos2 / uv2 / color4 / params4 = 48 bytes, attribute locations 0-3).
//     `set_vertex_buffer` rebinds the VBO against this VAO each time.
//   - `set_vertex_constants(slot=1, data, 64)` is treated as the 4x4
//     projection matrix and uploaded via `glUniformMatrix4fv(uProj)`,
//     matching the renderer.sx shader contract. Other slots are no-ops
//     for now.
//   - `set_texture(slot=0, tex)` binds texture unit 0 and sets
//     `uniform sampler2D uTex` to 0. Other slots are no-ops.
//   - The shader sources passed to `create_shader(vsrc, fsrc)` must be
//     a GLES vertex + fragment pair (NOT the Metal MSL combined source
//     the Metal backend takes — caller branches on OS).

#import "modules/std.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/api.sx";

// EGL bootstrap helper — used to populate opengl.sx's fn pointers once
// the EGL context is current. We declare only the loader here; EGL
// surface/context creation belongs in the consumer's `#jni_main`
// Activity (e.g. an `onCreate` body that calls `eglCreateContext`).
eglGetProcAddress :: (name: [*]u8) -> *void #foreign;


// Functions absent from opengl.sx (it was authored against the SDL
// desktop subset). We don't currently call these — destroy_* is best
// effort here; future work can wire `glDeleteProgram` etc. through
// load_gl if needed.

// ── State + resource tables ────────────────────────────────────────────

Gles3TextureSlot :: struct {
    tex: u32 = 0;
    bytes_per_pixel: u32 = 0;
}

Gles3ShaderSlot :: struct {
    program: u32 = 0;
    proj_loc: s32 = 0 - 1;
    tex_loc:  s32 = 0 - 1;
}

Gles3Gpu :: struct {
    pixel_w: s32 = 0;
    pixel_h: s32 = 0;

    // The renderer's vertex layout is fixed (see file header). One VAO,
    // reused across every set_vertex_buffer.
    vao: u32 = 0;

    current_shader: u32 = 0;
    gl_loaded: bool = false;

    shaders:  List(Gles3ShaderSlot)  = .{};
    buffers:  List(u32)              = .{};
    textures: List(Gles3TextureSlot) = .{};

    // Captured at init() time so resource creation always grows the cache
    // lists through the long-lived allocator, even when a caller (e.g.
    // glyph_cache atlas-grow during render) is currently inside a transient
    // arena context.
    parent_allocator: Allocator = .{};
}

// ── GPU impl ───────────────────────────────────────────────────────────

impl GPU for Gles3Gpu {
    // EGL is owned by AndroidPlatform; init() here populates the GL
    // function pointers (via opengl.sx's load_gl) and sets up the
    // single shared VAO. Must be called once the EGL context is
    // current (AndroidPlatform.run_frame_loop ensures this before
    // invoking the user's per-frame closure). `target` is unused.
    init :: (self: *Gles3Gpu, target: *void, pixel_w: s32, pixel_h: s32) -> bool {
        inline if OS != .android { return false; }
        self.pixel_w = pixel_w;
        self.pixel_h = pixel_h;
        self.parent_allocator = context.allocator;

        if !self.gl_loaded {
            load_gl(@eglGetProcAddress);
            self.gl_loaded = true;
        }

        // Create the VAO. Attribute layout is deferred to set_vertex_buffer
        // because `glVertexAttribPointer` captures the currently-bound
        // GL_ARRAY_BUFFER into the VAO state — we don't know which VBO
        // the renderer wants until it tells us via set_vertex_buffer.
        glGenVertexArrays(1, @self.vao);

        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        true;
    }

    shutdown :: (self: *Gles3Gpu) {
        // GLES objects clean up when the EGL context is destroyed (which
        // happens when the NativeActivity tears down).
    }

    resize :: (self: *Gles3Gpu, pixel_w: s32, pixel_h: s32) {
        self.pixel_w = pixel_w;
        self.pixel_h = pixel_h;
    }

    // The render-target setup (eglMakeCurrent + glViewport) lives in
    // AndroidPlatform.run_frame_loop; begin_frame here just clears the
    // active framebuffer. Returns true so callers don't bail out.
    begin_frame :: (self: *Gles3Gpu, clear: ClearColor) -> bool {
        inline if OS != .android { return false; }
        glViewport(0, 0, self.pixel_w, self.pixel_h);
        glClearColor(clear.r, clear.g, clear.b, clear.a);
        glClear(GL_COLOR_BUFFER_BIT);
        true;
    }

    // eglSwapBuffers happens in AndroidPlatform.end_frame; nothing for us.
    end_frame :: (self: *Gles3Gpu, target_time: f64) { }

    // ── Resources ────────────────────────────────────────────────────────

    create_shader :: (self: *Gles3Gpu, vsrc: string, fsrc: string) -> ShaderHandle {
        inline if OS != .android { return 0; }

        prog := create_program(xx vsrc, xx fsrc);
        if prog == 0 { return 0; }
        slot : Gles3ShaderSlot = .{
            program = prog,
            proj_loc = glGetUniformLocation(prog, "uProj".ptr),
            tex_loc  = glGetUniformLocation(prog, "uTex".ptr),
        };
        self.shaders.append(slot, self.parent_allocator);
        xx self.shaders.len;
    }

    create_buffer :: (self: *Gles3Gpu, size_bytes: s64) -> BufferHandle {
        inline if OS != .android { return 0; }
        if size_bytes <= 0 { return 0; }
        b : u32 = 0;
        glGenBuffers(1, @b);
        glBindBuffer(GL_ARRAY_BUFFER, b);
        glBufferData(GL_ARRAY_BUFFER, xx size_bytes, null, GL_DYNAMIC_DRAW);
        self.buffers.append(b, self.parent_allocator);
        xx self.buffers.len;
    }

    update_buffer :: (self: *Gles3Gpu, handle: BufferHandle, data: *void, size_bytes: s64) {
        inline if OS != .android { return; }
        buf := gles3_lookup_buffer(self, handle);
        if buf == 0 { return; }
        if data == null { return; }
        if size_bytes <= 0 { return; }
        glBindBuffer(GL_ARRAY_BUFFER, buf);
        glBufferSubData(GL_ARRAY_BUFFER, 0, xx size_bytes, data);
    }

    update_buffer_at :: (self: *Gles3Gpu, handle: BufferHandle, data: *void, size_bytes: s64, byte_offset: s64) {
        inline if OS != .android { return; }
        buf := gles3_lookup_buffer(self, handle);
        if buf == 0 { return; }
        if data == null { return; }
        if size_bytes <= 0 { return; }
        if byte_offset < 0 { return; }
        glBindBuffer(GL_ARRAY_BUFFER, buf);
        glBufferSubData(GL_ARRAY_BUFFER, xx byte_offset, xx size_bytes, data);
    }

    create_texture :: (self: *Gles3Gpu, w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle {
        inline if OS != .android { return 0; }
        if w <= 0 { return 0; }
        if h <= 0 { return 0; }

        internal_fmt : s32 = 0;
        ext_fmt : u32 = 0;
        bpp : u32 = 0;
        if format == .rgba8 {
            internal_fmt = xx GL_RGBA;
            ext_fmt = GL_RGBA;
            bpp = 4;
        } else {
            internal_fmt = xx GL_R8;
            ext_fmt = GL_RED;
            bpp = 1;
        }

        t : u32 = 0;
        glGenTextures(1, @t);
        glBindTexture(GL_TEXTURE_2D, t);
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
        glTexImage2D(GL_TEXTURE_2D, 0, internal_fmt, w, h, 0, ext_fmt, GL_UNSIGNED_BYTE, pixels);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);

        slot : Gles3TextureSlot = .{ tex = t, bytes_per_pixel = bpp };
        self.textures.append(slot, self.parent_allocator);
        xx self.textures.len;
    }

    update_texture_region :: (self: *Gles3Gpu, handle: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void) {
        inline if OS != .android { return; }
        if handle == 0 { return; }
        h64 : s64 = xx handle;
        if h64 > self.textures.len { return; }
        slot := self.textures.items[handle - 1];
        if slot.tex == 0 { return; }
        if pixels == null { return; }
        if w <= 0 { return; }
        if h <= 0 { return; }
        ext_fmt : u32 = if slot.bytes_per_pixel == 4 then GL_RGBA else GL_RED;
        glBindTexture(GL_TEXTURE_2D, slot.tex);
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
        glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, ext_fmt, GL_UNSIGNED_BYTE, pixels);
    }

    // glDeleteProgram / glDeleteBuffers aren't in opengl.sx's loader yet.
    // For now destroy_* is a no-op on Android — resources free on EGL
    // context teardown. Worth adding the loader entries when atlas grow
    // starts to matter in practice.
    destroy_shader  :: (self: *Gles3Gpu, sh:  ShaderHandle) { }
    destroy_buffer  :: (self: *Gles3Gpu, buf: BufferHandle) { }
    destroy_texture :: (self: *Gles3Gpu, tex: TextureHandle) {
        inline if OS != .android { return; }
        if tex == 0 { return; }
        h64 : s64 = xx tex;
        if h64 > self.textures.len { return; }
        t := self.textures.items[tex - 1].tex;
        if t == 0 { return; }
        glDeleteTextures(1, @t);
        self.textures.items[tex - 1].tex = 0;
    }

    // ── Per-draw state ───────────────────────────────────────────────────

    set_shader :: (self: *Gles3Gpu, handle: ShaderHandle) {
        inline if OS != .android { return; }
        if handle == 0 { return; }
        h64 : s64 = xx handle;
        if h64 > self.shaders.len { return; }
        prog := self.shaders.items[handle - 1].program;
        if prog == 0 { return; }
        glUseProgram(prog);
        self.current_shader = handle;
    }

    set_vertex_buffer :: (self: *Gles3Gpu, handle: BufferHandle) {
        inline if OS != .android { return; }
        buf := gles3_lookup_buffer(self, handle);
        if buf == 0 { return; }
        glBindVertexArray(self.vao);
        glBindBuffer(GL_ARRAY_BUFFER, buf);
        // glVertexAttribPointer captures the currently-bound
        // GL_ARRAY_BUFFER into the VAO state. Reconfigure each frame
        // so the VAO knows about the live buffer; otherwise drawing
        // sources attribs from the zero-buffer (invisible geometry).
        glVertexAttribPointer(0, 2, GL_FLOAT, 0, 48, xx 0);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 2, GL_FLOAT, 0, 48, xx 8);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(2, 4, GL_FLOAT, 0, 48, xx 16);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(3, 4, GL_FLOAT, 0, 48, xx 32);
        glEnableVertexAttribArray(3);
    }

    set_texture :: (self: *Gles3Gpu, slot: u32, handle: TextureHandle) {
        inline if OS != .android { return; }
        if slot != 0 { return; } // renderer only uses unit 0
        if handle == 0 { return; }
        h64 : s64 = xx handle;
        if h64 > self.textures.len { return; }
        t := self.textures.items[handle - 1].tex;
        if t == 0 { return; }
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, t);
        if self.current_shader != 0 {
            cs64 : s64 = xx self.current_shader;
            if cs64 <= self.shaders.len {
                loc := self.shaders.items[self.current_shader - 1].tex_loc;
                if loc >= 0 { glUniform1i(loc, 0); }
            }
        }
    }

    // For slot 1 + 64 bytes the renderer is uploading the 4x4 projection
    // matrix into `uniform mat4 uProj`. Look up via the current shader's
    // cached uniform location.
    set_vertex_constants :: (self: *Gles3Gpu, slot: u32, data: *void, size_bytes: s64) {
        inline if OS != .android { return; }
        if slot != 1 { return; }
        if size_bytes != 64 { return; }
        if self.current_shader == 0 { return; }
        cs64 : s64 = xx self.current_shader;
        if cs64 > self.shaders.len { return; }
        loc := self.shaders.items[self.current_shader - 1].proj_loc;
        if loc < 0 { return; }
        glUniformMatrix4fv(loc, 1, 0, xx data);
    }

    set_scissor :: (self: *Gles3Gpu, x: s32, y: s32, w: s32, h: s32) {
        inline if OS != .android { return; }
        glEnable(GL_SCISSOR_TEST);
        glScissor(x, self.pixel_h - (y + h), w, h);
    }

    disable_scissor :: (self: *Gles3Gpu) {
        inline if OS != .android { return; }
        glDisable(GL_SCISSOR_TEST);
    }

    draw_triangles :: (self: *Gles3Gpu, vertex_offset: s32, vertex_count: s32) {
        inline if OS != .android { return; }
        if vertex_count <= 0 { return; }
        glDrawArrays(GL_TRIANGLES, vertex_offset, vertex_count);
    }
}

gles3_lookup_buffer :: (self: *Gles3Gpu, handle: u32) -> u32 {
    inline if OS != .android { return 0; }
    if handle == 0 { return 0; }
    h64 : s64 = xx handle;
    if h64 > self.buffers.len { return 0; }
    self.buffers.items[handle - 1];
}
