struct Vertex2D {
    Vec2  pos;
    Vec2  tex;
    Color color;
};

struct VertexNode2D {
    VertexNode2D *next;
    int           count;
    Vertex2D      vertices[1024 * 16];
    Rect2         scissor;
};

struct VertexList2D {
    VertexNode2D *first;
    VertexNode2D *last;
};

struct Shader {
    GLuint program;    // linked program (vertex+fragment)
    GLint  uni_invHalf; // uniform location for inv half-screen size
    GLint  uni_texture; // sampler location
};

VertexList2D Vertices;
int64_t      TotalVertexCount;

unsigned VBO, VAO;
Shader   Shader2D;
BlockArena RenderArena;
Rect2    CurrentScissor;

Font PrimaryFont;
Font SecondaryFont;

// ---------- shaders (ES3 / WebGL2) ----------
static const char *glsl_vshader_es3 = R"==(#version 300 es
    precision highp float;

    uniform vec2 U_InvHalfScreenSize; // same meaning as before

    layout(location = 0) in vec2 aPos;
    layout(location = 1) in vec2 aTex;
    layout(location = 2) in vec4 aColor; // normalized unsigned byte will map to 0..1 if we use normalized=true

    out vec2 vUV;
    out vec4 vColor;

    void main() {
        vec2 pos = aPos * U_InvHalfScreenSize;
        pos.y = 2.0 - pos.y; // invert Y the same way you had
        pos -= vec2(1.0, 1.0);
        gl_Position = vec4(pos, 0.0, 1.0);
        vUV = aTex;
        vColor = aColor; // already 0..1
    }
)==";

static const char *glsl_fshader_es3 = R"==(#version 300 es
    precision mediump float;

    in vec2 vUV;
    in vec4 vColor;

    uniform sampler2D S_Texture;
    out vec4 OutColor;

    void main() {
        float a = texture(S_Texture, vUV).r;
        vec4 c = vColor;
        c.a *= a;
        OutColor = c;
    }
)==";

void ReportWarningf(const char *fmt, ...);
void GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *user) {
    ReportWarningf("OpenGL message: %s", message);
    if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM) {
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "OpenGL error", message, NULL);
    }
}

// ---------- helper: compile/link ----------
static GLuint CompileShaderSrc(GLenum kind, const char *src) {
    GLuint s = glCreateShader(kind);
    glShaderSource(s, 1, &src, NULL);
    glCompileShader(s);
    GLint ok = 0;
    glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char buf[1024];
        glGetShaderInfoLog(s, sizeof(buf), NULL, buf);
        SDL_Log("%s", buf);
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
                                 (kind == GL_VERTEX_SHADER ? "Vertex shader compile error" : "Fragment shader compile error"),
                                 buf, NULL);
    }
    return s;
}

Shader CreateShaderES3(const char *vsrc, const char *fsrc) {
    Shader out = {};
    GLuint vs = CompileShaderSrc(GL_VERTEX_SHADER, vsrc);
    GLuint fs = CompileShaderSrc(GL_FRAGMENT_SHADER, fsrc);

    GLuint prog = glCreateProgram();
    glAttachShader(prog, vs);
    glAttachShader(prog, fs);

    // Bind attribute locations to match layout locations (optional because we use layout(location=...))
    // glBindAttribLocation(prog, 0, "aPos");
    // glBindAttribLocation(prog, 1, "aTex");
    // glBindAttribLocation(prog, 2, "aColor");

    glLinkProgram(prog);
    GLint linked = 0;
    glGetProgramiv(prog, GL_LINK_STATUS, &linked);
    if (!linked) {
        char buf[1024];
        glGetProgramInfoLog(prog, sizeof(buf), NULL, buf);
        SDL_Log("%s", buf);
        SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Program link error", buf, NULL);
    }

    // We can detach/delete shaders after linking
    glDetachShader(prog, vs);
    glDetachShader(prog, fs);
    glDeleteShader(vs);
    glDeleteShader(fs);

    out.program = prog;
    out.uni_invHalf = glGetUniformLocation(prog, "U_InvHalfScreenSize");
    out.uni_texture = glGetUniformLocation(prog, "S_Texture");

    return out;
}

// ---------- InitRender for ES3 ----------
void InitRender() {
#if !OS_WASM
    glDebugMessageCallback(&GLDebugCallback, NULL);
    glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
#endif

    // Create VBO (non-DSA path)
    glGenBuffers(1, &VBO);
    // reserve buffer big enough for one VertexNode2D->vertices array (same as original intent)
    GLsizeiptr vbo_size = (GLsizeiptr) (Lengthof(((VertexNode2D*)0)->vertices) * sizeof(Vertex2D));
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, vbo_size, NULL, GL_DYNAMIC_DRAW); // NULL initial, dynamic usage

    // Create and setup VAO
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // attribute 0 : pos (vec2, floats)
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (const void*)offsetof(Vertex2D, pos));
    // attribute 1 : tex (vec2)
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (const void*)offsetof(Vertex2D, tex));
    // attribute 2 : color (vec4) -- stored as 4 unsigned bytes; we want normalized floats 0..1
    glEnableVertexAttribArray(2);
    // Using normalized GL_TRUE to map 0..255 -> 0..1 directly in shader
    glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex2D), (const void*)offsetof(Vertex2D, color));

    // Unbind VAO and VBO
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // Create shader program (ES3)
    Shader2D = CreateShaderES3(glsl_vshader_es3, glsl_fshader_es3);

    // other initializations like fonts will be done elsewhere (ReloadFont)
}

void BeginFrameRender(float wx, float wy) {
    Release(&RenderArena);
    TotalVertexCount = 0;
    Vertices.first   = NULL;
    Vertices.last    = NULL;
    CurrentScissor   = Rect0Size(wx, wy);
}

// ---------- EndFrameRender for ES3 ----------
void EndFrameRender(float wx, float wy, Color color) {
    ProfileFunction();
    glEnable(GL_BLEND);
    glEnable(GL_SCISSOR_TEST);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    glViewport(0, 0, (GLsizei)wx, (GLsizei)wy);
    glScissor(0, 0, (GLsizei)wx, (GLsizei)wy);
    glClearColor(color.r, color.g, color.b, color.a);
    glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    // Use program and set uniforms
    glUseProgram(Shader2D.program);
    float xinverse = 1.f / (wx / 2.f);
    float yinverse = 1.f / (wy / 2.f);
    // set uniform (U_InvHalfScreenSize)
    if (Shader2D.uni_invHalf >= 0) {
        glUniform2f(Shader2D.uni_invHalf, xinverse, yinverse);
    }
    // set sampler to texture unit 0
    if (Shader2D.uni_texture >= 0) {
        glUniform1i(Shader2D.uni_texture, 0);
    }

    glBindVertexArray(VAO);
    for (VertexNode2D *it = Vertices.first; it; it = it->next) {
        Rect2 rect = it->scissor;
        GLint x = (GLint)rect.min.x;
        GLint y = (GLint)rect.min.y;
        GLsizei w = (GLsizei)(rect.max.x - x);
        GLsizei h = (GLsizei)(rect.max.y - y);
        // convert scissor Y to OpenGL bottom-left origin like before
        glScissor(x, (GLint)wy - (GLint)rect.max.y, w, h);

        // upload vertex data into VBO at offset 0
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, it->count * sizeof(Vertex2D), it->vertices);

        // bind texture unit 0 and the font texture
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, PrimaryFont.texture_id);

        // draw
        glDrawArrays(GL_TRIANGLES, 0, it->count);

        // unbind VBO (optional)
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
    glBindVertexArray(0);
    glUseProgram(0);
}


VertexNode2D *AllocVertexNode2D(Allocator allocator, VertexList2D *list) {
    VertexNode2D *node = AllocType(allocator, VertexNode2D);
    SLL_QUEUE_ADD(list->first, list->last, node);
    return node;
}

void SetScissor(Rect2 rect) {
    CurrentScissor = rect;

    VertexNode2D *node = Vertices.last;
    if (!node) {
        node          = AllocVertexNode2D(RenderArena, &Vertices);
        node->scissor = rect;
        return;
    }

    if (node->scissor != rect && node->count == 0) {
        node->scissor = rect;
        return;
    }

    if (node->scissor != rect) {
        node          = AllocVertexNode2D(RenderArena, &Vertices);
        node->scissor = rect;
        return;
    }
}

void SetScissor(Rect2I rect) { SetScissor(ToRect2(rect)); }

Vertex2D *AllocVertex2D(Allocator allocator, VertexList2D *list, int count) {
    VertexNode2D *node = list->last;
    if (node == 0 || node->count + count > Lengthof(node->vertices)) {
        node          = AllocVertexNode2D(allocator, list);
        node->scissor = CurrentScissor;
    }

    TotalVertexCount += count;
    Vertex2D *result = node->vertices + node->count;
    node->count += count;
    return result;
}

void PushVertex2D(Allocator allocator, VertexList2D *list, Vertex2D *vertices, int count) {
    Vertex2D *result = AllocVertex2D(allocator, list, count);
    for (int i = 0; i < count; i += 1) result[i] = vertices[i];
}

void PushQuad2D(Allocator arena, VertexList2D *list, Rect2 rect, Rect2 tex, Color color, float rotation = 0.f, Vec2 rotation_point = {}) {
    Vertex2D *v = AllocVertex2D(arena, list, 6);
    v[0] = { {rect.min.x, rect.max.y}, { tex.min.x,  tex.max.y}, color };
    v[1] = { {rect.max.x, rect.max.y}, { tex.max.x,  tex.max.y}, color };
    v[2] = { {rect.min.x, rect.min.y}, { tex.min.x,  tex.min.y}, color };
    v[3] = { {rect.min.x, rect.min.y}, { tex.min.x,  tex.min.y}, color };
    v[4] = { {rect.max.x, rect.max.y}, { tex.max.x,  tex.max.y}, color };
    v[5] = { {rect.max.x, rect.min.y}, { tex.max.x,  tex.min.y}, color };
    if (rotation != 0.f) {
        float s = sinf(rotation);
        float c = cosf(rotation);
        for (int i = 0; i < 6; i += 1) {
            v[i].pos -= rotation_point;
            v[i].pos = {v[i].pos.x * c + v[i].pos.y * (-s), v[i].pos.x * s + v[i].pos.y * c};
            v[i].pos += rotation_point;
        }
    }
}

void DrawRect(Rect2 rect, Color color) {
    PushQuad2D(RenderArena, &Vertices, rect, PrimaryFont.white_texture_bounding_box, color);
}
void DrawRect(Rect2I rect, Color color) {
    PushQuad2D(RenderArena, &Vertices, ToRect2(rect), PrimaryFont.white_texture_bounding_box, color);
}

void DrawRectOutline(Rect2 rect, Color color, float thickness = 2) {
    Rect2 a = CutLeft(&rect, thickness);
    Rect2 b = CutRight(&rect, thickness);
    Rect2 c = CutTop(&rect, thickness);
    Rect2 d = CutBottom(&rect, thickness);
    PushQuad2D(RenderArena, &Vertices, a, PrimaryFont.white_texture_bounding_box, color);
    PushQuad2D(RenderArena, &Vertices, b, PrimaryFont.white_texture_bounding_box, color);
    PushQuad2D(RenderArena, &Vertices, c, PrimaryFont.white_texture_bounding_box, color);
    PushQuad2D(RenderArena, &Vertices, d, PrimaryFont.white_texture_bounding_box, color);
}

void DrawRectOutline(Rect2I rect, Color color, Int thickness = 2) {
    DrawRectOutline(ToRect2(rect), color, (float)thickness);
}

Vec2 DrawString(Font *font, String16 string, Vec2 pos, Color color, bool draw = true) {
    pos.y += font->line_spacing + font->descent; // TODO: descent?
    Vec2 original_pos = pos;
    For(string) {
        Glyph *g    = GetGlyph(font, it);
        Rect2  rect = Rect2FromSize(pos + g->offset, g->size);
        if (draw && it != '\n' && it != ' ' && it != '\t') {
            PushQuad2D(RenderArena, &Vertices, rect, g->atlas_bounding_box, color);
        }
        pos.x += g->xadvance;
    }
    Vec2 result = {pos.x - original_pos.x, font->size};
    return result;
}

void DrawCircle(Vec2 pos, float radius, Color color) {
    const int segment_count = 16;
    const int vertex_count  = segment_count * 3;
    Vec2      points[segment_count + 1];
    for (int i = 0; i < Lengthof(points); i += 1) {
        float radians = 2.0f * PI32 * float(i) / float(segment_count);
        float x       = radius * cosf(radians);
        float y       = radius * sinf(radians);
        points[i]     = {x, y};
    }
    points[segment_count] = points[0]; // wrap around

    Vertex2D *vertices  = AllocVertex2D(RenderArena, &Vertices, vertex_count);
    int       point_i   = 0;
    int       segment_i = 0;
    for (; segment_i < vertex_count; segment_i += 3, point_i += 1) {
        Rect2     tex = PrimaryFont.white_texture_bounding_box;
        Vertex2D *it  = vertices + segment_i;
        it[0].color   = color;
        it[1].color   = color;
        it[2].color   = color;
        it[0].tex     = {tex.min.x, tex.max.y};
        it[1].tex     = {tex.max.x, tex.max.y};
        it[2].tex     = {tex.min.x, tex.min.y};

        it[0].pos = {pos + points[point_i]};
        it[1].pos = {pos + points[point_i + 1]};
        it[2].pos = {pos};
    }
}

GLuint UploadAtlas(Atlas *atlas) {
    GLuint tex = 0;
    glGenTextures(1, &tex);
    glBindTexture(GL_TEXTURE_2D, tex);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, (GLsizei)atlas->size.x, (GLsizei)atlas->size.y, 0, GL_RED, GL_UNSIGNED_BYTE, atlas->bitmap);
    glBindTexture(GL_TEXTURE_2D, 0);
    atlas->texture_id = tex;
    return tex;
}

void ReloadFont(String path, U32 size) {
    if (PrimaryFont.texture_id) {
        glDeleteTextures(1, &PrimaryFont.texture_id);
        Dealloc(&PrimaryFont.glyphs);
        PrimaryFont = {};
    }

    Scratch scratch;
    Atlas atlas = CreateAtlas(scratch, {2048, 2048});
    PrimaryFont = CreateFont(&atlas, (uint32_t)ClampBottom(2u, (U32)size), path);
    SecondaryFont = CreateFont(&atlas, 12, path);
    SecondaryFont.texture_id = PrimaryFont.texture_id = UploadAtlas(&atlas);
}
