#include "twn_draw.h"
#include "twn_draw_c.h"
#include "twn_engine_context_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_textures_c.h"
#include "twn_option.h"

#include <stb_ds.h>

#include <stdbool.h>
#include <stddef.h>

/*
 * an implementation note:
 * try to avoid doing expensive work in the push functions,
 * because they will be called multiple times in the main loop
 * before anything is really rendered
 */
void draw_sprite(char const *path,
                 Rect        rect,
                 Rect const *texture_region,    /* optional, default: NULL */
                 Color       color,             /* optional, default: all 255 */
                 float       rotation,          /* optional, default: 0 */
                 bool        flip_x,            /* optional, default: false */
                 bool        flip_y,            /* optional, default: false */
                 bool        stretch)
{
    SpritePrimitive sprite = {
        .rect = rect,
        .color = color,
        .rotation = rotation,
        .texture_key = textures_get_key(&ctx.texture_cache, path),
        .flip_x = flip_x,
        .flip_y = flip_y,
        .repeat = !stretch,
        .texture_region_opt_set = texture_region != NULL,
    };

    if (texture_region)
        sprite.texture_region_opt = *texture_region;

    Primitive2D primitive = {
        .type = PRIMITIVE_2D_SPRITE,
        .sprite = sprite,
    };

    arrput(ctx.render_queue_2d, primitive);
}

void draw_sprite_args(const DrawSpriteArgs args) {
    Color const color = m_or(args, color, ((Color) { 255, 255, 255, 255 }));
    float const rotation = m_or(args, rotation, 0.0f);
    bool const flip_x = m_or(args, flip_x, false);
    bool const flip_y = m_or(args, flip_y, false);
    bool const stretch = m_or(args, stretch, false);
    Rect const *texture_region = m_is_set(args, texture_region) ? &args.texture_region_opt : NULL;

    draw_sprite(args.texture, args.rect, texture_region, color, rotation, flip_x, flip_y, stretch);
}


struct QuadBatch collect_sprite_batch(const Primitive2D primitives[], size_t len) {
    SDL_assert(primitives[0].type == PRIMITIVE_2D_SPRITE);
    SDL_assert(primitives && len != 0);

    const uint16_t texture_key_id = primitives[0].sprite.texture_key.id;
    const int atlas_id = textures_get_atlas_id(&ctx.texture_cache, primitives[0].sprite.texture_key);

    struct QuadBatch batch = {
        .mode = textures_get_mode(&ctx.texture_cache, primitives[0].sprite.texture_key),
        .texture_key = primitives[0].sprite.texture_key,
        .constant_colored = true,
        .repeat = primitives[0].sprite.repeat,
        .textured = true,
    };

    const uint32_t uniform_color = *(const uint32_t *)(void const*)&primitives[0].sprite.color;

    /* batch size is clamped so that reallocated short indices could be used */
    if (len >= QUAD_ELEMENT_BUFFER_LENGTH)
        len = QUAD_ELEMENT_BUFFER_LENGTH;

    for (size_t i = 0; i < len; ++i) {
        const Primitive2D *const current = &primitives[i];

        /* don't touch things other than sprites */
        if (current->type != PRIMITIVE_2D_SPRITE)
            break;

        /* only collect the same blend modes */
        const TextureMode mode = textures_get_mode(&ctx.texture_cache, current->sprite.texture_key);
        if (mode != batch.mode)
            break;

        /* only collect the same texture atlases */
        if (textures_get_atlas_id(&ctx.texture_cache, current->sprite.texture_key) != atlas_id)
            break;

        /* repeated textures require separate handling */
        if (batch.repeat) {
            /* all must be repeated */
            if (!current->sprite.repeat)
                break;

            /* all must be of same texture id, not just atlas id */
            if (current->sprite.texture_key.id != texture_key_id)
                break;
        }

        /* if all are modulated the same we can skip sending the color data */
        if (*(const uint32_t *)(void const *)&current->sprite.color != uniform_color)
            batch.constant_colored = false;

        ++batch.size;
    }

    return batch;
}


/* assumes that orthogonal matrix setup is done already */
void render_sprite_batch(const Primitive2D primitives[],
                         const struct QuadBatch batch)
{
    SDL_assert(primitives && batch.size != 0);
    SDL_assert(primitives[0].type == PRIMITIVE_2D_SPRITE);

    VertexBuffer const vertex_array = get_scratch_vertex_array();

    const Rect dims =
        textures_get_dims(&ctx.texture_cache, primitives->sprite.texture_key);

    /* cached srcrect */
    Rect cached_srcrect = {0};
    TextureKey cached_srcrect_key = TEXTURE_KEY_INVALID;

    /* vertex population over a vertex buffer builder interface */
    {
        VertexBufferBuilder payload = build_vertex_buffer(vertex_array, get_quad_payload_size(batch) * batch.size);

        for (size_t i = 0; i < batch.size; ++i) {
            /* render opaques front to back, to gain benefit of an early z rejection */
            const size_t cur = batch.mode == TEXTURE_MODE_GHOSTLY ? i : batch.size - i - 1;
            const SpritePrimitive sprite = primitives[cur].sprite;

            if (primitives[cur].sprite.texture_key.id != cached_srcrect_key.id) {
                cached_srcrect = textures_get_srcrect(&ctx.texture_cache, primitives[cur].sprite.texture_key);
                cached_srcrect_key = primitives[cur].sprite.texture_key;
            }

            Rect const srcrect = cached_srcrect;

            Vec2 uv0, uv1, uv2, uv3;

            if (!batch.repeat) {
                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;

                if (!m_is_set(sprite, texture_region)) {
                    uv0 = (Vec2){ xr + wr *  sprite.flip_x, yr + hr *  sprite.flip_y };
                    uv1 = (Vec2){ xr + wr *  sprite.flip_x, yr + hr * !sprite.flip_y };
                    uv2 = (Vec2){ xr + wr * !sprite.flip_x, yr + hr * !sprite.flip_y };
                    uv3 = (Vec2){ xr + wr * !sprite.flip_x, yr + hr *  sprite.flip_y };
                } else {
                    /* TODO: support for flipping */
                    uv0 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x) / dims.w,
                                  (srcrect.y + sprite.texture_region_opt.y) / dims.h };
                    uv1 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x) / dims.w,
                                  (srcrect.y + sprite.texture_region_opt.y + sprite.texture_region_opt.h) / dims.h };
                    uv2 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x + sprite.texture_region_opt.w) / dims.w,
                                  (srcrect.y + sprite.texture_region_opt.y + sprite.texture_region_opt.h) / dims.h };
                    uv3 = (Vec2){ (srcrect.x + sprite.texture_region_opt.x + sprite.texture_region_opt.w) / dims.w,
                                  (srcrect.y + sprite.texture_region_opt.y) / dims.h };
                }
            } else {
                /* try fitting texture into supplied destination rectangle */

                const float rx = sprite.rect.w / srcrect.w;
                const float ry = sprite.rect.h / srcrect.h;

                uv0 = (Vec2){ rx *  sprite.flip_x, ry *  sprite.flip_y };
                uv1 = (Vec2){ rx *  sprite.flip_x, ry * !sprite.flip_y };
                uv2 = (Vec2){ rx * !sprite.flip_x, ry * !sprite.flip_y };
                uv3 = (Vec2){ rx * !sprite.flip_x, ry *  sprite.flip_y };

                if (m_is_set(sprite, texture_region)) {
                    /* displace origin */

                    const float ax = sprite.texture_region_opt.x / srcrect.w;
                    const float ay = sprite.texture_region_opt.y / srcrect.h;

                    uv0.x += ax; uv1.x += ax; uv2.x += ax; uv3.x += ax;
                    uv0.y += ay; uv1.y += ay; uv2.y += ay; uv3.y += ay;
                }
            }

            Vec2 v0, v1, v2, v3;

            /* todo: fast PI/2 degree divisible rotations? */
            if (sprite.rotation == 0.0f) {
                /* non-rotated case */

                v0 = (Vec2){ sprite.rect.x,                 sprite.rect.y };
                v1 = (Vec2){ sprite.rect.x,                 sprite.rect.y + sprite.rect.h };
                v2 = (Vec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y + sprite.rect.h };
                v3 = (Vec2){ sprite.rect.x + sprite.rect.w, sprite.rect.y };

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wfloat-equal"

            } else if (sprite.rect.w == sprite.rect.h) {
                /* rotated square case */

#pragma GCC diagnostic pop

                const Vec2 c = rect_center(sprite.rect);
                const Vec2 t = fast_cossine(sprite.rotation + (float)M_PI_4);
                const Vec2 d = {
                    .x = t.x * sprite.rect.w * (float)M_SQRT1_2,
                    .y = t.y * sprite.rect.h * (float)M_SQRT1_2,
                };

                v0 = (Vec2){ c.x - d.x, c.y - d.y };
                v1 = (Vec2){ c.x - d.y, c.y + d.x };
                v2 = (Vec2){ c.x + d.x, c.y + d.y };
                v3 = (Vec2){ c.x + d.y, c.y - d.x };

            } else {
                /* rotated non-square case*/

                const Vec2 c = rect_center(sprite.rect);
                const Vec2 t = fast_cossine(sprite.rotation);

                const Vec2 h = { sprite.rect.w / 2, sprite.rect.h / 2 };

                v0 = (Vec2){ c.x + t.x * -h.x - t.y * -h.y, c.y + t.y * -h.x + t.x * -h.y };
                v1 = (Vec2){ c.x + t.x * -h.x - t.y * +h.y, c.y + t.y * -h.x + t.x * +h.y };
                v2 = (Vec2){ c.x + t.x * +h.x - t.y * +h.y, c.y + t.y * +h.x + t.x * +h.y };
                v3 = (Vec2){ c.x + t.x * +h.x - t.y * -h.y, c.y + t.y * +h.x + t.x * -h.y };
            }

            push_quad_payload_to_vertex_buffer_builder(batch, &payload, v0, v1, v2, v3, uv0, uv1, uv2, uv3, sprite.color);
        }
    }

    finally_render_quads(primitives, batch, vertex_array);
}
