RandomSeed PLOT_RandomSeed = {13};

bool DH_Initialized;

struct R3_Vertex {
    Vec3 pos;
    Vec2 tex;
    Vec3 color;
};
#define R3_VERTEX_BUFFER_SIZE 4096 * 8

enum R3_CommandKind {
    R3_COMMAND_NONE,
    R3_COMMAND_CUBE,
    R3_COMMAND_QUAD,
};

struct R3_CommandCube {
    R3_Vertex *vertices;
    int vertex_count;
};

struct R3_Command {
    R3_CommandKind kind;
    R3_Command *next;
    int64_t vertex_count;
    R3_Vertex *vertices;
};

struct R3_Camera {
    float pitch, roll, head;
    Vec3 pos;

    float old_roll;
    float approach_speed;
};

struct R3_Render {
    MA_Arena arena;

    unsigned atlas_id;
    R3_Camera camera;
    R3_Command *first_command;
    R3_Command *last_command;
    unsigned vbo, vao;
    R2_Shader shader;
};
R3_Render R3;

unsigned R3_UploadTexture(void *data, int x, int y) {
    unsigned id = 0;
    glCreateTextures(GL_TEXTURE_2D, 1, &id);
    glTextureParameteri(id, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTextureParameteri(id, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTextureParameteri(id, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTextureParameteri(id, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTextureStorage2D(id, 1, GL_RGBA8, x, y);
    glTextureSubImage2D(id, 0, 0, 0, x, y, GL_RGBA, GL_UNSIGNED_BYTE, data);
    return id;
}

void R3_Init() {
    MA_Init(&R3.arena);
    R3.arena.alignment = 0;

    // IO_Assert(MA_GetAlignOffset(sizeof(R3_Vertex), R3.arena.alignment) == 0);
    {
        glCreateBuffers(1, &R3.vbo);
        glNamedBufferStorage(R3.vbo, R3_VERTEX_BUFFER_SIZE * sizeof(R3_Vertex), 0, GL_DYNAMIC_STORAGE_BIT);
    }

    {
        glCreateVertexArrays(1, &R3.vao);

        GLint vbuf_index = 0;
        glVertexArrayVertexBuffer(R3.vao, vbuf_index, R3.vbo, 0, sizeof(struct R3_Vertex));

        GLint a_pos = 0;
        glVertexArrayAttribFormat(R3.vao, a_pos, 3, GL_FLOAT, GL_FALSE, offsetof(struct R3_Vertex, pos));
        glVertexArrayAttribBinding(R3.vao, a_pos, vbuf_index);
        glEnableVertexArrayAttrib(R3.vao, a_pos);

        GLint a_tex = 1;
        glVertexArrayAttribFormat(R3.vao, a_tex, 2, GL_FLOAT, GL_FALSE, offsetof(struct R3_Vertex, tex));
        glVertexArrayAttribBinding(R3.vao, a_tex, vbuf_index);
        glEnableVertexArrayAttrib(R3.vao, a_tex);

        GLint a_color = 2;
        glVertexArrayAttribFormat(R3.vao, a_color, 3, GL_FLOAT, GL_FALSE, offsetof(struct R3_Vertex, color));
        glVertexArrayAttribBinding(R3.vao, a_color, vbuf_index);
        glEnableVertexArrayAttrib(R3.vao, a_color);
    }

    {
        char *glsl_vshader =
            "#version 450 core\n"
            "#line " R2_STRINGIFY(__LINE__) "\n"
                                            R"==(

        layout (location=0) uniform mat4 UPerspective;
        layout (location=0) in vec3 InPos;
        layout (location=1) in vec2 InTex;
        layout (location=2) in vec3 InColor;


        out gl_PerVertex { vec4 gl_Position; }; // required because of ARB_separate_shader_objects
        out vec2 OutTex;
        out vec4 OutColor;

        void main() {
            vec4 pos = vec4(InPos, 1);
            pos = UPerspective * pos;

            gl_Position = pos;
            OutTex = InTex;
            OutColor = vec4(InColor, 1);
        }
        )==";

        char *glsl_fshader =
            "#version 450 core\n"
            "#line " R2_STRINGIFY(__LINE__) "\n"
                                            R"==(

        in vec2 InTex;
        in vec4 InColor;

        layout (binding=0) uniform sampler2D Tex0;
        layout (location=0) out vec4 OutColor;

        void main() {
            OutColor = InColor;
        }
        )==";
        R3.shader = R2_CreateShader(glsl_vshader, glsl_fshader);
    }
    R3.last_command = R3.first_command = MA_PushStruct(&R3.arena, R3_Command);
}

R3_Command *R3_GetCommand(R3_CommandKind kind) {
    R3_Command *cmd = R3.last_command;
    if (cmd->kind == kind) {
        if (cmd->vertex_count + 36 < R3_VERTEX_BUFFER_SIZE) {
            return cmd;
        }
    }
    R3_Command *command = MA_PushStruct(&R3.arena, R3_Command);
    command->kind = kind;
    SLL_QUEUE_ADD(R3.first_command, R3.last_command, command);
    return command;
}

void R3_PushQuad(Rect3 rect, Vec3 color, bool colored = false) {
    Vec3 top = color;
    Vec3 side = color;
    if (colored) {
        side *= 0.8f;
    }

    Rect2 tex = {};
    R3_Vertex vertices[] = {
  // counter clockwise winding (opengl default)
        {{rect.min.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.min.x, rect.min.y, rect.max.z}, {tex.min.x, tex.max.y}, side},
        {{rect.min.x, rect.max.y, rect.max.z}, {tex.max.x, tex.max.y}, side},
        {{rect.min.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.min.x, rect.max.y, rect.max.z}, {tex.max.x, tex.max.y}, side},
        {{rect.min.x, rect.max.y, rect.min.z}, {tex.max.x, tex.min.y}, side},
        {{rect.max.x, rect.max.y, rect.min.z}, {tex.max.x, tex.max.y}, side},
        {{rect.min.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.min.x, rect.max.y, rect.min.z}, {tex.min.x, tex.max.y}, side},
        {{rect.max.x, rect.max.y, rect.min.z}, {tex.max.x, tex.max.y}, side},
        {{rect.max.x, rect.min.y, rect.min.z}, {tex.max.x, tex.min.y}, side},
        {{rect.min.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.max.x, rect.min.y, rect.max.z}, {tex.max.x, tex.max.y}, side},
        {{rect.min.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.max.x, rect.min.y, rect.min.z}, {tex.max.x, tex.min.y}, side},
        {{rect.max.x, rect.min.y, rect.max.z}, {tex.max.x, tex.max.y}, side},
        {{rect.min.x, rect.min.y, rect.max.z}, {tex.min.x, tex.max.y}, side},
        {{rect.min.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.min.x, rect.max.y, rect.max.z}, {tex.min.x, tex.max.y}, side},
        {{rect.min.x, rect.min.y, rect.max.z}, {tex.min.x, tex.min.y}, side},
        {{rect.max.x, rect.min.y, rect.max.z}, {tex.max.x, tex.min.y}, side},
        {{rect.max.x, rect.max.y, rect.max.z}, {tex.max.x, tex.max.y}, side},
        {{rect.min.x, rect.max.y, rect.max.z}, {tex.min.x, tex.max.y}, side},
        {{rect.max.x, rect.min.y, rect.max.z}, {tex.max.x, tex.min.y}, side},
        {{rect.max.x, rect.max.y, rect.max.z}, {tex.max.x, tex.max.y}, side},
        {{rect.max.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.max.x, rect.max.y, rect.min.z}, {tex.max.x, tex.min.y}, side},
        {{rect.max.x, rect.min.y, rect.min.z}, {tex.min.x, tex.min.y}, side},
        {{rect.max.x, rect.max.y, rect.max.z}, {tex.max.x, tex.max.y}, side},
        {{rect.max.x, rect.min.y, rect.max.z}, {tex.min.x, tex.max.y}, side},
        {{rect.max.x, rect.max.y, rect.max.z}, {tex.max.x, tex.max.y},  top},
        {{rect.max.x, rect.max.y, rect.min.z}, {tex.max.x, tex.min.y},  top},
        {{rect.min.x, rect.max.y, rect.min.z}, {tex.min.x, tex.min.y},  top},
        {{rect.max.x, rect.max.y, rect.max.z}, {tex.max.x, tex.max.y},  top},
        {{rect.min.x, rect.max.y, rect.min.z}, {tex.min.x, tex.min.y},  top},
        {{rect.min.x, rect.max.y, rect.max.z}, {tex.min.x, tex.max.y},  top},
    };
    R3_Command *command = R3_GetCommand(R3_COMMAND_CUBE);
    void *result = MA_PushCopy(&R3.arena, vertices, sizeof(vertices));
    if (command->vertices == 0) command->vertices = (R3_Vertex *)result;
    command->vertex_count += lengthof(vertices);
}

void R3_EndFrame(Vec2 window_size, M4x4 matrix) {
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);
    glEnable(GL_BLEND);

    glBindProgramPipeline(R3.shader.pipeline);
    glProgramUniformMatrix4fv(R3.shader.vshader, 0, 1, GL_TRUE, (float *)matrix.e);

    for (R3_Command *it = R3.first_command->next; it; it = it->next) {
        switch (it->kind) {
            case R3_COMMAND_CUBE: {
                glNamedBufferSubData(R3.vbo, 0, it->vertex_count * sizeof(R3_Vertex), it->vertices);
                glBindVertexArray(R3.vao);
                GLint sampler_id = 0;
                glBindTextureUnit(sampler_id, R3.atlas_id);
                glDrawArrays(GL_TRIANGLES, 0, (int)it->vertex_count);
            } break;
            case R3_COMMAND_QUAD: {
                // glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
                glNamedBufferSubData(R3.vbo, 0, it->vertex_count * sizeof(R3_Vertex), it->vertices);
                glBindVertexArray(R3.vao);
                GLint sampler_id = 0;
                glBindTextureUnit(sampler_id, R3.atlas_id);
                glDrawArrays(GL_TRIANGLES, 0, (int)it->vertex_count);
            } break;
            case R3_COMMAND_NONE:
                IO_InvalidDefaultCase();
        }
    }

    MA_Reset(&R3.arena);
    R3.last_command = R3.first_command = MA_PushStruct(&R3.arena, R3_Command);
}

M4x4 GetCameraTransform(R3_Camera camera) {
    M4x4 result = M4_RotationZ(camera.old_roll) * M4_RotationX(camera.pitch) * M4_RotationY(camera.head) * M4_Translation(-camera.pos);
    return result;
}

M4x4 GetCameraRotation(R3_Camera camera) {
    M4x4 result = M4_RotationZ(camera.old_roll) * M4_RotationX(camera.pitch) * M4_RotationY(camera.head);
    return result;
}

S8_String Serialize(MA_Arena *arena, R3_Camera *camera) {
    S8_String result = S8_Format(arena, "Camera :: Pos:[%.02f %.02f %.02f] Pitch:%f Roll:%f Head:%f", camera->pos.x, camera->pos.y, camera->pos.z, camera->pitch, camera->roll, camera->head);
    return result;
}

void PROTOTYPE_DataHandling() {
    if (!DH_Initialized) {
        DH_Initialized = true;
        R3_Init();

        MU_ToggleFPSMode(Mu->window);
        R3.camera.pitch = 0.70f;
        R3.camera.head = -0.75f;
        R3.camera.pos = {20, 20, 20};
    }

    {
        R3_Camera &camera = R3.camera;
        camera.pitch -= (float)Mu->window->mouse.delta_pos_normalized.y * 4;
        camera.head += (float)Mu->window->mouse.delta_pos_normalized.x * 4;
        M4x4 r = GetCameraRotation(camera);
        Vec3 zdirection = GetBasisZ(r);
        Vec3 xdirection = GetBasisX(r);
        if (Mu->window->key[MU_KEY_W].down) {
            camera.pos -= zdirection * 0.05f;
        }
        if (Mu->window->key[MU_KEY_S].down) {
            camera.pos += zdirection * 0.05f;
        }
        if (Mu->window->key[MU_KEY_A].down) {
            camera.pos -= xdirection * 0.05f;
        }
        if (Mu->window->key[MU_KEY_D].down) {
            camera.pos += xdirection * 0.05f;
        }
    }

    float t1 = -0.1f;
    float t2 = 0.1f;
    float s = 15.f;
    R3_PushQuad({t1, t1, t1, s, t2, t2}, {1, 0, 0});
    R3_PushQuad({t1, t1, t1, t2, s, t2}, {1, 0, 0});
    R3_PushQuad({t1, t1, t1, t2, t2, s}, {1, 0, 0});
    R3_PushQuad({t1 + s, t1, t1, t2 + s, s, t2}, {1, 0, 0});
    R3_PushQuad({t1, t1, t1 + s, t2, s, t2 + s}, {1, 0, 0});
    R3_PushQuad({t1 + s, t1, t1 + s, t2 + s, s, t2 + s}, {1, 0, 0});
    R3_PushQuad({t1 + s, t1, t1, t2 + s, t2, s}, {1, 0, 0});
    R3_PushQuad({t1, t1, t1 + s, s, t2, t2 + s}, {1, 0, 0});
    R3_PushQuad({t1, t1 + s, t1, s, t2 + s, t2}, {1, 0, 0});
    R3_PushQuad({t1, t1 + s, t1, t2, t2 + s, s}, {1, 0, 0});
    R3_PushQuad({t1 + s, t1 + s, t1, t2 + s, t2 + s, s}, {1, 0, 0});
    R3_PushQuad({t1, t1 + s, t1 + s, s, t2 + s, t2 + s}, {1, 0, 0});

    // R3_PushQuad({t1, t1 + 15.f, t1, 0.1f, 0.1f + 15.f, 15}, {1, 0, 0});
    // for (float y = 0; y < 1.0f; y += 0.1f) {
    //     float of = y * 15.0f;
    //     R3_PushQuad({ + of, -0.1f, 0, 0.1f + of, 0.1f, 15.f}, {1, 0, 0});
    // }

    RandomSeed seed = {13};
    for (float i = 0; i < 10; i += 1) {
        Vec3 p = {
            (float)GetRandomNormal(&seed) * 10.f,
            (float)GetRandomNormal(&seed) * 10.f,
            (float)GetRandomNormal(&seed) * 10.f,
        };

        float t = 0.25f;
        Rect3 rect = {p.x - t, p.y - t, p.z - t, p.x + t, p.y + t, p.z + t};
        R3_PushQuad(rect, COLOR_data_point.rgb);
    }

    M4x4 matrix = M4_Identity();
    M4x4 camera_transform = GetCameraTransform(R3.camera);
    matrix = M4_Perspective(65, Mu->window->sizef.x, Mu->window->sizef.y, 0.1f, 100.f) * camera_transform * matrix;
    R3_EndFrame(Mu->window->sizef, matrix);

    R2_DebugString("%s", Serialize(G_Frame, &R3.camera).str);
}
