/* a rendering.c mixin */
#ifndef SPRITES_H
#define SPRITES_H

#include "../rendering.h"
#include "townengine/context.h"
#include "../util.h"
#include "../textures/internal_api.h"
#include "quad_element_buffer.h"
#include "internal_api.h"

#include <stb_ds.h>

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

/* interleaved vertex array data */
/* TODO: use int16_t for uvs */
/* TODO: use packed types? */
/* TODO: int16_t could be used for positioning, but we would need to have more CPU calcs */
struct sprite_primitive_payload {
    /* upper-left */
    t_fvec2 v0;
    t_fvec2 uv0;
    t_color c0;
    /* bottom-left */
    t_fvec2 v1;
    t_fvec2 uv1;
    t_color c1;
    /* bottom-right */
    t_fvec2 v2;
    t_fvec2 uv2;
    t_color c2;
    /* upper-right */
    t_fvec2 v3;
    t_fvec2 uv3;
    t_color c3;
};


struct sprite_primitive_payload_without_color {
    /* upper-left */
    t_fvec2 v0;
    t_fvec2 uv0;
    /* bottom-left */
    t_fvec2 v1;
    t_fvec2 uv1;
    /* bottom-right */
    t_fvec2 v2;
    t_fvec2 uv2;
    /* upper-right */
    t_fvec2 v3;
    t_fvec2 uv3;
};

/*
 * 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
 */
/* TODO: it might make sense to infer alpha channel presence / meaningfulness for textures in atlas */
/*          so that they are rendered with no blend / batched in a way to reduce overdraw automatically */
void push_sprite(const t_push_sprite_args args) {
    struct sprite_primitive sprite = {
        .rect = args.rect,
        .color = m_or(args, color, ((t_color) { 255, 255, 255, 255 })),
        .rotation = m_or(args, rotation, 0.0f),
        .texture_key = textures_get_key(&ctx.texture_cache, args.path),
        .flip_x = m_or(args, flip_x, false),
        .flip_y = m_or(args, flip_y, false),
        .repeat = !m_or(args, stretch, true),
        m_opt_from(texture_origin, args, texture_origin)
    };

    struct primitive_2d primitive = {
        .type = PRIMITIVE_2D_SPRITE,
        .sprite = sprite,
    };

    arrput(ctx.render_queue_2d, primitive);
}


static struct sprite_batch {
    size_t size;             /* how many primitives are in current batch */
    enum texture_mode mode;
    bool constant_colored;   /* whether colored batch is uniformly colored */
    bool repeat;             /* whether repeat is needed */
} collect_sprite_batch(const struct primitive_2d *primitives, size_t len) {
    /* assumes that first primitive is already a sprite */
    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 sprite_batch batch = {
        .mode = textures_get_mode(&ctx.texture_cache, primitives[0].sprite.texture_key),
        .constant_colored = true,
        .repeat = primitives[0].sprite.repeat,
    };

    const uint32_t uniform_color = *(const uint32_t *)&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 struct primitive_2d *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 enum texture_mode 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 *)&current->sprite.color != uniform_color)
            batch.constant_colored = false;

        ++batch.size;
    }

    return batch;
}


/* assumes that orthogonal matrix setup is done already */
static void render_sprites(const struct primitive_2d primitives[],
                           const struct sprite_batch batch)
{
    /* single vertex array is used for every batch with NULL glBufferData() trick at the end */
    static GLuint vertex_array = 0;
    if (vertex_array == 0)
        glGenBuffers(1, &vertex_array);

    if (batch.mode == TEXTURE_MODE_GHOSTLY) {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glDepthFunc(GL_ALWAYS);
        glDepthMask(GL_FALSE);
        glDisable(GL_ALPHA_TEST);
    } else if (batch.mode == TEXTURE_MODE_SEETHROUGH) {
        glDisable(GL_BLEND);
        glDepthFunc(GL_LEQUAL);
        glDepthMask(GL_TRUE);
        glEnable(GL_ALPHA_TEST);
        glAlphaFunc(GL_EQUAL, 1.0f);
    } else {
        glDisable(GL_BLEND);
        glDepthFunc(GL_LESS);
        glDepthMask(GL_TRUE);
        glDisable(GL_ALPHA_TEST);
    }

    size_t payload_size;
    if (!batch.constant_colored)
        payload_size = sizeof (struct sprite_primitive_payload);
    else
        payload_size = sizeof (struct sprite_primitive_payload_without_color);

    glBindBuffer(GL_ARRAY_BUFFER, vertex_array);
    glBufferData(GL_ARRAY_BUFFER,
                 payload_size * batch.size,
                 NULL,
                 GL_STREAM_DRAW);

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

    /* vertex population over a mapped buffer */
    {
        /* TODO: check errors, ensure alignment ? */
        void *const payload = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

        for (size_t i = 0; i < batch.size; ++i) {
            /* render opaques front to back */
            const size_t cur = batch.mode == TEXTURE_MODE_GHOSTLY ? i : batch.size - i - 1;
            const struct sprite_primitive sprite = primitives[cur].sprite;

            const t_frect srcrect =
                textures_get_srcrect(&ctx.texture_cache, primitives[cur].sprite.texture_key);

            t_fvec2 uv0, uv1, uv2, uv3;

            if (!sprite.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;

                uv0 = (t_fvec2){ xr + wr *  sprite.flip_x, yr + hr *  sprite.flip_y };
                uv1 = (t_fvec2){ xr + wr *  sprite.flip_x, yr + hr * !sprite.flip_y };
                uv2 = (t_fvec2){ xr + wr * !sprite.flip_x, yr + hr * !sprite.flip_y };
                uv3 = (t_fvec2){ xr + wr * !sprite.flip_x, yr + hr *  sprite.flip_y };

                /* TODO: texture_origin support */

            } 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 = (t_fvec2){ rx *  sprite.flip_x, ry *  sprite.flip_y };
                uv1 = (t_fvec2){ rx *  sprite.flip_x, ry * !sprite.flip_y };
                uv2 = (t_fvec2){ rx * !sprite.flip_x, ry * !sprite.flip_y };
                uv3 = (t_fvec2){ rx * !sprite.flip_x, ry *  sprite.flip_y };

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

                    const float ax = sprite.texture_origin_opt.x / srcrect.w;
                    const float ay = sprite.texture_origin_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;
                }
            }

            t_fvec2 v0, v1, v2, v3;

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

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

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

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

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

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

                const t_fvec2 c = frect_center(sprite.rect);
                const t_fvec2 t = fast_cossine(sprite.rotation);

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

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

            if (!batch.constant_colored)
                ((struct sprite_primitive_payload *)payload)[i] = (struct sprite_primitive_payload) {
                    .v0 = v0,
                    .v1 = v1,
                    .v2 = v2,
                    .v3 = v3,

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

                    /* equal for all (flat shaded) */
                    .c0 = sprite.color,
                    .c1 = sprite.color,
                    .c2 = sprite.color,
                    .c3 = sprite.color,
                };
            else
                ((struct sprite_primitive_payload_without_color *)payload)[i] = (struct sprite_primitive_payload_without_color) {
                    .v0 = v0,
                    .v1 = v1,
                    .v2 = v2,
                    .v3 = v3,

                    .uv0 = uv0,
                    .uv1 = uv1,
                    .uv2 = uv2,
                    .uv3 = uv3,
                };
        }

        glUnmapBuffer(GL_ARRAY_BUFFER);
    }

    GLsizei off;
    GLsizei voff;
    GLsizei uvoff;

    if (!batch.constant_colored) {
        off   = offsetof(struct sprite_primitive_payload, v1);
        voff  = offsetof(struct sprite_primitive_payload, v0);
        uvoff = offsetof(struct sprite_primitive_payload, uv0);
    } else {
        off   = offsetof(struct sprite_primitive_payload_without_color, v1);
        voff  = offsetof(struct sprite_primitive_payload_without_color, v0);
        uvoff = offsetof(struct sprite_primitive_payload_without_color, uv0);
    }

    /* vertex specification */
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2,
                    GL_FLOAT,
                    off,
                    (void *)(size_t)voff);

    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glClientActiveTexture(GL_TEXTURE0);
    glTexCoordPointer(2,
                      GL_FLOAT,
                      off,
                      (void *)(size_t)uvoff);

    if (!batch.constant_colored) {
        glEnableClientState(GL_COLOR_ARRAY);
        glColorPointer(4,
                       GL_UNSIGNED_BYTE,
                       off,
                       (void *)offsetof(struct sprite_primitive_payload, c0));
    } else
        glColor4ub(primitives[0].sprite.color.r,
                   primitives[0].sprite.color.g,
                   primitives[0].sprite.color.b,
                   primitives[0].sprite.color.a);

    if (!batch.repeat)
        textures_bind(&ctx.texture_cache, primitives->sprite.texture_key, GL_TEXTURE_2D);
    else
        textures_bind_repeating(&ctx.texture_cache, primitives->sprite.texture_key, GL_TEXTURE_2D);

    bind_quad_element_buffer();

    glDrawElements(GL_TRIANGLES, 6 * (GLsizei)batch.size, GL_UNSIGNED_SHORT, NULL);

    /* clear the state */
    glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

    glBindTexture(GL_TEXTURE_2D, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

#endif
