#include "twn_draw.h"
#include "twn_draw_c.h"
#include "twn_engine_context_c.h"
#include "twn_textures_c.h"
#include "twn_types.h"
#include "twn_util_c.h"
#include "twn_vec.h"

#include <stb_ds.h>


void draw_billboard(const char *texture,
                    Vec3 position,
                    Vec2 size,
                    Color color,
                    bool cylindrical)
{
    // TODO: order drawing by atlas id as well, so that texture rebinding is not as common
    const TextureKey texture_key = textures_get_key(&ctx.texture_cache, texture);

    struct MeshBatchItem *batch_p = hmgetp_null(ctx.billboard_batches, texture_key);
    if (!batch_p) {
        struct MeshBatch item = {0};
        hmput(ctx.billboard_batches, texture_key, item);
        batch_p = &ctx.billboard_batches[hmlenu(ctx.billboard_batches) - 1]; /* TODO: can last index be used? */
    }

    struct SpaceBillboard billboard = {
        .color = color,
        .cylindrical = cylindrical,
        .position = position,
        .size = size,
    };

    struct SpaceBillboard *billboards = (struct SpaceBillboard *)(void *)batch_p->value.primitives;

    arrpush(billboards, billboard);
    batch_p->value.primitives = (uint8_t *)billboards;
}


/* reused for all billboards */
static Vec3 right_plus_up;
static Vec3 right_minus_up;
static Vec3 right_plus_up_cylindrical;
static Vec3 right_minus_up_cylindrical;

/* precalculate (right + up) and (right - up) that are used with all batches */
static void calculate_intermediates(void) {
    Vec3 const right = { camera_look_at_matrix.row[0].x, camera_look_at_matrix.row[1].x, camera_look_at_matrix.row[2].x };
    Vec3 const up = { camera_look_at_matrix.row[0].y, camera_look_at_matrix.row[1].y, camera_look_at_matrix.row[2].y };

    right_plus_up = m_vec_add(right, up);
    right_minus_up = m_vec_sub(right, up);

    Vec3 const up_cylindrical = { 0, 1, 0 };

    right_plus_up_cylindrical = m_vec_add(right, up_cylindrical);
    right_minus_up_cylindrical = m_vec_sub(right, up_cylindrical);
}


/* http://www.lighthouse3d.com/opengl/billboarding/index.php?billCheat2 */
void finally_draw_billboard_batch(struct MeshBatch const *batch,
                                  TextureKey texture_key)
{
    const size_t primitives_len = arrlenu(batch->primitives);

    /* nothing to do */
    if (primitives_len == 0)
        return;

    /* TODO: only do it once per frame */
    calculate_intermediates();

    const Rect srcrect = textures_get_srcrect(&ctx.texture_cache, texture_key);
    const Rect dims    = textures_get_dims(&ctx.texture_cache, texture_key);

    const float wr = srcrect.w / dims.w;
    const float hr = srcrect.h / dims.h;
    const float xr = srcrect.x / dims.w;
    const float yr = srcrect.y / dims.h;

    const Vec2 uv0 = { xr,           yr      };
    const Vec2 uv1 = { xr,           yr + hr };
    const Vec2 uv2 = { xr + wr,      yr + hr };
    const Vec2 uv3 = { xr + wr,      yr      };

    for (size_t batch_n = 0; batch_n <= (primitives_len - 1) / QUAD_ELEMENT_BUFFER_LENGTH; batch_n++) {

        /* emit vertex data */
        VertexBuffer const buffer = get_scratch_vertex_array();
        VertexBufferBuilder builder = build_vertex_buffer(
            buffer,
            sizeof (ElementIndexedBillboard) * MIN(primitives_len - batch_n * QUAD_ELEMENT_BUFFER_LENGTH, QUAD_ELEMENT_BUFFER_LENGTH));

        for (size_t i = 0; i < primitives_len; ++i) {
            struct SpaceBillboard const billboard = ((SpaceBillboard *)(void *)batch->primitives)[batch_n * QUAD_ELEMENT_BUFFER_LENGTH + i];

            /* a = (right + up) * size, b = (right - up) * size*/
            Vec3 a, b;
            if (billboard.cylindrical) {
                a = vec3_mul(right_plus_up_cylindrical, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
                b = vec3_mul(right_minus_up_cylindrical, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
            } else {
                a = vec3_mul(right_plus_up, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
                b = vec3_mul(right_minus_up, ((Vec3){billboard.size.x, billboard.size.y, billboard.size.x }));
            }

            struct ElementIndexedBillboard const payload = {
                /* flat shading is assumed, so we can skip setting the duplicates */
                .c0 = billboard.color,
                // .c1 = billboard.color,
                .c2 = billboard.color,
                // .c3 = billboard.color,

                .uv0 = uv0,
                .uv1 = uv1,
                .uv2 = uv2,
                .uv3 = uv3,

                .v0 = vec3_sub(billboard.position, b),
                .v1 = vec3_sub(billboard.position, a),
                .v2 = vec3_add(billboard.position, b),
                .v3 = vec3_add(billboard.position, a),
            };

            ((struct ElementIndexedBillboard *)builder.base)[i] = payload;
        }

        finish_vertex_builder(&builder);

        /* commit to drawing */
        DeferredCommandDraw command = {0};

        command.vertices = (AttributeArrayPointer) {
            .arity = 3,
            .type = TWN_FLOAT,
            .stride = offsetof(ElementIndexedBillboard, v1),
            .offset = offsetof(ElementIndexedBillboard, v0),
            .buffer = buffer
        };

        command.texcoords = (AttributeArrayPointer) {
            .arity = 2,
            .type = TWN_FLOAT,
            .stride = offsetof(ElementIndexedBillboard, v1),
            .offset = offsetof(ElementIndexedBillboard, uv0),
            .buffer = buffer
        };

        command.colors = (AttributeArrayPointer) {
            .arity = 4,
            .type = TWN_UNSIGNED_BYTE,
            .stride = offsetof(ElementIndexedBillboard, v1),
            .offset = offsetof(ElementIndexedBillboard, c0),
            .buffer = buffer
        };

        command.textured = true;
        command.texture_key = texture_key;

        command.element_buffer = get_quad_element_buffer();
        command.element_count = 6 * (uint32_t)primitives_len;
        command.range_end = 6 * (uint32_t)primitives_len;

        /* TODO: support alpha blended case, with distance sort */
        TextureMode mode = textures_get_mode(&ctx.texture_cache, texture_key);
        if (mode == TEXTURE_MODE_GHOSTLY)
            mode = TEXTURE_MODE_SEETHROUGH;

        command.texture_mode = mode;
        command.pipeline = PIPELINE_SPACE;

        command.depth_range_high = depth_range_high;
        command.depth_range_low = depth_range_low;

        DeferredCommand final_command = {
            .type = DEFERRED_COMMAND_TYPE_DRAW,
            .draw = command
        };

        arrpush(deferred_commands, final_command);
    }
}
