#include "twn_draw_c.h"
#include "twn_draw.h"
#include "twn_engine_context_c.h"
#include "twn_camera_c.h"
#include "twn_types.h"
#include "twn_util_c.h"
#include "twn_vec.h"
#include "twn_deferred_commands.h"

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

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


DeferredCommand *deferred_commands;

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


void render_queue_clear(void) {
    text_cache_reset_arena(&ctx.text_cache);
    
    /* 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);

    /* TODO: free memory if it isn't used for a while */
    for (size_t i = 0; i < hmlenu(ctx.uncolored_mesh_batches); ++i)
        arrsetlen(ctx.uncolored_mesh_batches[i].value.primitives, 0);

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


void draw_nine_slice(const char *texture, Vec2 corners, Rect rect, float border_thickness, Color color) {
    const float bt = border_thickness;
    const float bt2 = bt * 2; /* combined size of the two borders in an axis */


    Rect top_left = {
        .x = rect.x,
        .y = rect.y,
        .w = bt,
        .h = bt,
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, top_left),
        m_opt(texture_region, ((Rect) { 0, 0, bt, bt })),
        m_opt(color, color),
    );


    Rect top_center = {
        .x = rect.x + bt,
        .y = rect.y,
        .w = rect.w - bt2, /* here bt2 represents the top left and right corners */
        .h = bt,
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, top_center),
        m_opt(texture_region, ((Rect) { bt, 0, corners.x - bt2, bt })),
        m_opt(color, color),
    );


    Rect top_right = {
        .x = rect.x + (rect.w - bt),
        .y = rect.y,
        .w = bt,
        .h = bt,
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, top_right),
        m_opt(texture_region, ((Rect) { corners.x - bt, 0, bt, bt })),
        m_opt(color, color),
    );


    Rect center_left = {
        .x = rect.x,
        .y = rect.y + bt,
        .w = bt,
        .h = rect.h - bt2, /* here bt2 represents the top and bottom left corners */
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, center_left),
        m_opt(texture_region, ((Rect) { 0, bt, bt, corners.y - bt2 })),
        m_opt(color, color),
    );


    Rect center_right = {
        .x = rect.x + (rect.w - bt),
        .y = rect.y + bt,
        .w = bt,
        .h = rect.h - bt2, /* here bt2 represents the top and bottom right corners */
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, center_right),
        m_opt(texture_region, ((Rect) { corners.x - bt, bt, bt, corners.y - bt2 })),
        m_opt(color, color),
    );


    Rect bottom_left = {
        .x = rect.x,
        .y = rect.y + (rect.h - bt),
        .w = bt,
        .h = bt,
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, bottom_left),
        m_opt(texture_region, ((Rect) { 0, corners.y - bt, bt, bt })),
        m_opt(color, color),
    );


    Rect bottom_center = {
        .x = rect.x + bt,
        .y = rect.y + (rect.h - bt),
        .w = rect.w - bt2, /* here bt2 represents the bottom left and right corners */
        .h = bt,
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, bottom_center),
        m_opt(texture_region, ((Rect) { bt, corners.y - bt, corners.x - bt2, bt })),
        m_opt(color, color),
    );


    Rect bottom_right = {
        .x = rect.x + (rect.w - bt),
        .y = rect.y + (rect.h - bt),
        .w = bt,
        .h = bt,
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, bottom_right),
        m_opt(texture_region, ((Rect) { corners.x - bt, corners.y - bt, bt, bt })),
        m_opt(color, color),
    );


    Rect center = {
        .x = rect.x + bt,
        .y = rect.y + bt,
        .w = rect.w - bt2,
        .h = rect.h - bt2,
    };

    m_sprite(
        m_set(texture, texture),
        m_set(rect, center),
        m_opt(texture_region, ((Rect) { bt, bt, corners.x - bt2, corners.y - bt2 })),
        m_opt(color, color),
    );
}


static void render_2d(void) {
    use_2d_pipeline();

    const size_t render_queue_len = arrlenu(ctx.render_queue_2d);

    struct Render2DInvocation {
        Primitive2D const *primitive;
        double layer;
        union {
            struct QuadBatch quad_batch;
        };
    };

    /* first, collect all invocations, while merging into batches where applicable */
    /* we separate into opaque and transparent ones, as it presents optimization opportunities */
    struct Render2DInvocation *opaque_invocations = NULL;
    struct Render2DInvocation *ghostly_invocations = NULL;

    arrsetcap(opaque_invocations, render_queue_len);
    arrsetcap(ghostly_invocations, render_queue_len);

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

        // TODO: https://gamedev.stackexchange.com/questions/101136/using-full-resolution-of-depth-buffer-for-2d-rendering
        double const layer = ((double)((render_queue_len + 1) - i) / (double)(render_queue_len + 1)) * 0.75;

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

                struct Render2DInvocation const invocation = {
                    .primitive = current,
                    .quad_batch = batch,
                    .layer = layer,
                };

                if (batch.mode == TEXTURE_MODE_GHOSTLY)
                    arrput(ghostly_invocations, invocation);
                else
                    arrput(opaque_invocations, invocation);

                i += batch.size - 1;
                break;
            }

            case PRIMITIVE_2D_RECT: {
                const struct QuadBatch batch =
                    collect_rect_batch(current, render_queue_len - i);

                struct Render2DInvocation const invocation = {
                    .primitive = current,
                    .quad_batch = batch,
                    .layer = layer,
                };

                if (batch.mode == TEXTURE_MODE_GHOSTLY)
                    arrput(ghostly_invocations, invocation);
                else
                    arrput(opaque_invocations, invocation);

                i += batch.size - 1;
                break;
            }

            case PRIMITIVE_2D_CIRCLE: {
                struct Render2DInvocation const invocation = {
                    .primitive = current,
                    .layer = layer,
                };

                if (current->circle.color.a != 255)
                    arrput(ghostly_invocations, invocation);
                else
                    arrput(opaque_invocations, invocation);
                break;
            }

            case PRIMITIVE_2D_TEXT: {
                struct Render2DInvocation const invocation = {
                    .primitive = current,
                    .layer = layer,
                };
                arrput(ghostly_invocations, invocation);
                break;
            }

            default:
                SDL_assert(false);
        }
    }

    /* first issue all opaque primitives, front-to-back */
    for (size_t i = 0; i < arrlenu(opaque_invocations); ++i) {
        struct Render2DInvocation const invocation = opaque_invocations[arrlenu(opaque_invocations) - 1 - i];

        /* idea here is to set constant z write that moves further and further along */
        /* with that every batch can early z reject against the previous */
        /* additionally, it will also apply for future transparent passes, sandwitching in-between */
        set_depth_range(invocation.layer, 1.0);

        switch (invocation.primitive->type) {
            case PRIMITIVE_2D_SPRITE: {
                render_sprite_batch(invocation.primitive, invocation.quad_batch);
                break;
            }
            case PRIMITIVE_2D_RECT: {
                render_rect_batch(invocation.primitive, invocation.quad_batch);
                break;
            }
            /* TODO: circle batching */
            case PRIMITIVE_2D_CIRCLE:
                render_circle(&invocation.primitive->circle);
                break;
            case PRIMITIVE_2D_TEXT:
            default:
                SDL_assert(false);
        }
    }

    /* then issue all transparent primitives, back-to-front */
    for (size_t i = 0; i < arrlenu(ghostly_invocations); ++i) {
        struct Render2DInvocation const invocation = ghostly_invocations[i];

        /* now we use it not for writing layers, but inferring ordering */
        set_depth_range(invocation.layer, 1.0);

        switch (invocation.primitive->type) {
            case PRIMITIVE_2D_SPRITE: {
                render_sprite_batch(invocation.primitive, invocation.quad_batch);
                break;
            }
            case PRIMITIVE_2D_RECT: {
                render_rect_batch(invocation.primitive, invocation.quad_batch);
                break;
            }
            /* TODO: circle batching */
            case PRIMITIVE_2D_CIRCLE:
                render_circle(&invocation.primitive->circle);
                break;
            case PRIMITIVE_2D_TEXT:
                render_text(&invocation.primitive->text);
                break;
            default:
                SDL_assert(false);
        }
    }

    arrfree(opaque_invocations);
    arrfree(ghostly_invocations);
}


static void render_space(void) {
    /* nothing to do, abort */
    /* as space pipeline isn't used we can have fewer changes and initialization costs */
    if (hmlenu(ctx.uncolored_mesh_batches) != 0 || hmlenu(ctx.billboard_batches) != 0) {
        use_space_pipeline();
        apply_fog();

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

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

        pop_fog();
    }

    render_skybox(); /* after everything else, as to use depth buffer for early z rejection */
}


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

    /* fit rendering context onto the resizable screen */
    if (ctx.window_size_has_changed) {
        setup_viewport((int)ctx.viewport_rect.x, (int)ctx.viewport_rect.y, (int)ctx.viewport_rect.w, (int)ctx.viewport_rect.h);
    }

    start_render_frame(); {
        render_space();
        render_2d();
    } end_render_frame();
}


void draw_camera(Vec3 position, float fov, Vec3 up, Vec3 direction) {
    Camera const camera = {
        .fov = fov,
        .pos = position,
        .target = direction,
        .up = up,
    };

    camera_projection_matrix = camera_perspective(&camera);
    camera_look_at_matrix    = camera_look_at(&camera);
}


/* TODO: https://stackoverflow.com/questions/62493770/how-to-add-roll-in-camera-class */
DrawCameraFromPrincipalAxesResult draw_camera_from_principal_axes(Vec3 position, float fov, float roll, float pitch, float yaw) {
    (void)roll;

    float yawc, yaws, pitchc, pitchs;
    sincosf(yaw, &yaws, &yawc);
    sincosf(pitch, &pitchs, &pitchc);

    Camera const camera = {
        .fov = fov,
        .pos = position,
        .target = m_vec_norm(((Vec3){
            yawc * pitchc,
            pitchs,
            yaws * pitchc,
        })),
        .up = (Vec3){0, 1, 0},
    };

    camera_projection_matrix = camera_perspective(&camera);
    camera_look_at_matrix    = camera_look_at(&camera);

    return (DrawCameraFromPrincipalAxesResult) {
        .direction = camera.target,
        .up = camera.up,
    };
}


void set_depth_range(double low, double high) {
    DeferredCommand const command = {
        .type = DEFERRED_COMMAND_TYPE_DEPTH_RANGE,
        .depth_range = {
            .low = low,
            .high = high
        }
    };

    arrpush(deferred_commands, command);
}


void clear_draw_buffer(void) {
    /* TODO: we can optimize a rectangle drawn over whole window to a clear color call*/

    DeferredCommand command = {
        .type = DEFERRED_COMMAND_TYPE_CLEAR,
        .clear = (DeferredCommandClear) {
            .clear_color = true,
            .clear_depth = true,
            .clear_stencil = true,
            .color = (Color) { 230, 230, 230, 1 }
        }
    };

    arrpush(deferred_commands, command);
}


void use_texture_mode(TextureMode mode) {
    DeferredCommand const command = {
        .type = DEFERRED_COMMAND_TYPE_USE_TEXTURE_MODE,
        .use_texture_mode = { mode }
    };

    arrpush(deferred_commands, command);
}


void use_2d_pipeline(void) {
    DeferredCommand const command = {
        .type = DEFERRED_COMMAND_TYPE_USE_PIPIELINE,
        .use_pipeline = { PIPELINE_2D }
    };

    arrpush(deferred_commands, command);
}


void use_space_pipeline(void) {
    DeferredCommand const command = {
        .type = DEFERRED_COMMAND_TYPE_USE_PIPIELINE,
        .use_pipeline = { PIPELINE_SPACE }
    };

    arrpush(deferred_commands, command);
}


void issue_deferred_draw_commands(void) {
    for (size_t i = 0; i < arrlenu(deferred_commands); ++i) {
        switch (deferred_commands[i].type) {
            case DEFERRED_COMMAND_TYPE_DEPTH_RANGE: {
                finally_set_depth_range(deferred_commands[i].depth_range);
                break;
            }

            case DEFERRED_COMMAND_TYPE_CLEAR: {
                finally_clear_draw_buffer(deferred_commands[i].clear);
                break;
            }

            case DEFERRED_COMMAND_TYPE_DRAW: {
                finally_draw_command(deferred_commands[i].draw);
                break;
            }

            case DEFERRED_COMMAND_TYPE_DRAW_SKYBOX: {
                finally_render_skybox(deferred_commands[i].draw_skybox);
                break;
            }

            case DEFERRED_COMMAND_TYPE_USE_PIPIELINE: {
                switch (deferred_commands[i].use_pipeline.pipeline) {
                    case PIPELINE_2D:
                        finally_use_2d_pipeline();
                        break;
                    case PIPELINE_SPACE:
                        finally_use_space_pipeline();
                        break;
                    case PIPELINE_NO:
                    default:
                        SDL_assert(false);
                }

                break;
            }

            case DEFERRED_COMMAND_TYPE_USE_TEXTURE_MODE: {
                finally_use_texture_mode(deferred_commands[i].use_texture_mode.mode);
                break;
            }

            case DEFERRED_COMMAND_TYPE_APPLY_FOG: {
                finally_apply_fog(deferred_commands[i].apply_fog);
                break;
            }

            case DEFERRED_COMMAND_TYPE_POP_FOG: {
                finally_pop_fog();
                break;
            }

            default:
                SDL_assert(false);
        }
    }
}
