#include "rendering/internal_api.h"
#include "rendering/sprites.h"
#include "rendering/triangles.h"
#include "rendering/circles.h"
#include "rendering/text.h"
#include "textures/internal_api.h"
#include "townengine/context.h"

#include <SDL2/SDL.h>
#include <glad/glad.h>
#include <stb_ds.h>

#include <stddef.h>
#include <tgmath.h>


/* TODO: have a default initialized one */
static t_matrix4 camera_projection_matrix;
static t_matrix4 camera_look_at_matrix;


void render_queue_clear(void) {
    /* this doesn't even _deserve_ a TODO */
    /* if you're gonna remove it, this is also being done in main.c */
    for (size_t i = 0; i < arrlenu(ctx.render_queue_2d); ++i) {
        if (ctx.render_queue_2d[i].type == PRIMITIVE_2D_TEXT) {
            free(ctx.render_queue_2d[i].text.text);
        }
    }
    
    /* since i don't intend to free the queues, */
    /* it's faster and simpler to just "start over" */
    /* and start overwriting the existing data */
    arrsetlen(ctx.render_queue_2d, 0);

    for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i)
        arrsetlen(ctx.uncolored_mesh_batches[i].value.primitives, 0);
}


/* rectangle */
void push_rectangle(t_frect rect, t_color color) {
    struct rect_primitive rectangle = {
        .rect = rect,
        .color = color,
    };

    struct primitive_2d primitive = {
        .type = PRIMITIVE_2D_RECT,
        .rect = rectangle,
    };

    arrput(ctx.render_queue_2d, primitive);
}


static void upload_quad_vertices(t_frect rect) {
    /* client memory needs to be reachable on glDraw*, so */
    static float vertices[6 * 2];

    vertices[0]  = rect.x;          vertices[1]  = rect.y;
    vertices[2]  = rect.x;          vertices[3]  = rect.y + rect.h;
    vertices[4]  = rect.x + rect.w; vertices[5]  = rect.y + rect.h;
    vertices[6]  = rect.x + rect.w; vertices[7]  = rect.y + rect.h;
    vertices[8]  = rect.x + rect.w; vertices[9]  = rect.y;
    vertices[10] = rect.x;          vertices[11] = rect.y;

    glVertexPointer(2, GL_FLOAT, 0, (void *)&vertices);
}


static void render_rectangle(const struct rect_primitive *rectangle) {
    glColor4ub(rectangle->color.r, rectangle->color.g,
               rectangle->color.b, rectangle->color.a);

    glEnableClientState(GL_VERTEX_ARRAY);
    upload_quad_vertices(rectangle->rect);

    glDrawArrays(GL_TRIANGLES, 0, 6);
    glDisableClientState(GL_VERTEX_ARRAY);
}


static void render_2d(void) {
    glEnable(GL_TEXTURE_2D);
    glActiveTexture(GL_TEXTURE0);
    glDisable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

    const size_t render_queue_len = arrlenu(ctx.render_queue_2d);

    size_t batch_count = 0;

    for (size_t i = 0; i < render_queue_len; ++i) {
        const struct primitive_2d *current = &ctx.render_queue_2d[i];

        switch (current->type) {
            case PRIMITIVE_2D_SPRITE: {
                const struct sprite_batch batch =
                    collect_sprite_batch(current, render_queue_len - i);

                glDepthRange((double)batch_count / UINT16_MAX, 1.0);

                render_sprites(current, batch);

                i += batch.size - 1; ++batch_count;
                break;
            }
            case PRIMITIVE_2D_RECT:
                render_rectangle(&current->rect);
                break;
            case PRIMITIVE_2D_CIRCLE:
                render_circle(&current->circle);
                break;
            case PRIMITIVE_2D_TEXT:
                render_text(&current->text);
                break;
        }
    }
}


static void render_space(void) {
    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);
    glDepthMask(GL_TRUE);
    glDepthFunc(GL_LESS);
    glDepthRange(0, 1);
    glDisable(GL_BLEND);
    glEnable(GL_ALPHA_TEST); /* TODO: infer its usage */
    glAlphaFunc(GL_EQUAL, 1.0f);
    glEnable(GL_TEXTURE_2D);
    glActiveTexture(GL_TEXTURE0);

    /* solid white, no modulation */
    glColor4ub(255, 255, 255, 255);

    for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i) {
        draw_uncolored_space_traingle_batch(&ctx.uncolored_mesh_batches[i].value,
                                            ctx.uncolored_mesh_batches[i].key);
    }
}


void render(void) {
    textures_update_atlas(&ctx.texture_cache);

    /* fit rendering context onto the resizable screen */
    if (ctx.window_size_has_changed) {
        if ((float)ctx.window_w / (float)ctx.window_h > RENDER_BASE_RATIO) {
            float ratio = (float)ctx.window_h / (float)RENDER_BASE_HEIGHT;
            int w = (int)((float)RENDER_BASE_WIDTH * ratio);
            glViewport(
                ctx.window_w / 2 - w / 2,
                0,
                w,
                ctx.window_h
            );
        } else {
            float ratio = (float)ctx.window_w / (float)RENDER_BASE_WIDTH;
            int h = (int)((float)RENDER_BASE_HEIGHT * ratio);
            glViewport(
                0,
                ctx.window_h / 2 - h / 2,
                ctx.window_w,
                h
            );
        }
    }

    glClearColor((1.0f / 255) * 230,
                 (1.0f / 255) * 230,
                 (1.0f / 255) * 230, 1);

    glClear(GL_COLOR_BUFFER_BIT |
            GL_DEPTH_BUFFER_BIT |
            GL_STENCIL_BUFFER_BIT);

    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
    glShadeModel(GL_SMOOTH);

    if (GLAD_GL_ARB_depth_clamp)
        glDisable(GL_DEPTH_CLAMP);

    {
        glMatrixMode(GL_PROJECTION);
        glLoadMatrixf(&camera_projection_matrix.row[0].x);

        glMatrixMode(GL_MODELVIEW);
        glLoadMatrixf(&camera_look_at_matrix.row[0].x);

        render_space();
    }

    /* TODO: only do it when transition between spaces is needed */
    glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glFlush();

    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST);
    glShadeModel(GL_FLAT);

    /* removes near/far plane comparison and discard */
    if (GLAD_GL_ARB_depth_clamp)
        glDisable(GL_DEPTH_CLAMP);

    {
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT, 0, -1, 1);

        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

        render_2d();
    }

    SDL_GL_SwapWindow(ctx.window);
}

void set_camera(const t_camera *const camera) {
    /* TODO: skip recaulculating if it's the same? */
    camera_projection_matrix = camera_perspective(camera);
    camera_look_at_matrix = camera_look_at(camera);
}
