#include "twn_draw_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_engine_context_c.h"
#include "twn_text_c.h"

#include <glad/glad.h>
#include <stb_ds.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 */
typedef struct ElementIndexedQuad {
    /* upper-left */
    Vec2 v0;
    Vec2 uv0;
    Color c0;
    /* bottom-left */
    Vec2 v1;
    Vec2 uv1;
    Color c1;
    /* bottom-right */
    Vec2 v2;
    Vec2 uv2;
    Color c2;
    /* upper-right */
    Vec2 v3;
    Vec2 uv3;
    Color c3;
} ElementIndexedQuad;


typedef struct ElementIndexedQuadWithoutColor {
    /* upper-left */
    Vec2 v0;
    Vec2 uv0;
    /* bottom-left */
    Vec2 v1;
    Vec2 uv1;
    /* bottom-right */
    Vec2 v2;
    Vec2 uv2;
    /* upper-right */
    Vec2 v3;
    Vec2 uv3;
} ElementIndexedQuadWithoutColor;


typedef enum {
    PIPELINE_NO,
    PIPELINE_SPACE,
    PIPELINE_2D,
} Pipeline;


static Pipeline pipeline_last_used = PIPELINE_NO;


void use_space_pipeline(void) {
    if (pipeline_last_used == PIPELINE_SPACE)
        return;

    static GLuint list = 0;
    if (!list) {
        list = glGenLists(1);

        glNewList(list, GL_COMPILE); {
            glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
            glShadeModel(GL_SMOOTH);

            if (GLAD_GL_ARB_depth_clamp)
                glDisable(GL_DEPTH_CLAMP);

            glEnable(GL_CULL_FACE);
            glDepthRange(0, 1);
            glEnable(GL_TEXTURE_2D);
            glActiveTexture(GL_TEXTURE0);

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

    glCallList(list);

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

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

    pipeline_last_used = PIPELINE_SPACE;
}


void use_2d_pipeline(void) {
    if (pipeline_last_used == PIPELINE_SPACE) {
        glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        glFlush();
    }

    if (pipeline_last_used == PIPELINE_2D)
        return;

    static GLuint list = 0;
    if (!list) {
        list = glGenLists(1);

        glNewList(list, GL_COMPILE); {
            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);

            glEnable(GL_TEXTURE_2D);
            glActiveTexture(GL_TEXTURE0);
            glDisable(GL_CULL_FACE);
            glEnable(GL_DEPTH_TEST);
        } glEndList();
    }

    glCallList(list);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, (double)ctx.base_render_width, (double)ctx.base_render_height, 0, -1, 1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    pipeline_last_used = PIPELINE_2D;
}


void upload_quad_vertices(Rect 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);
}


void render_rectangle(const RectPrimitive *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);
}


void render_circle(const CirclePrimitive *circle) {
    SDL_Vertex *vertices = NULL;
    int *indices = NULL;
    int num_vertices = (int)circle->radius;

    create_circle_geometry(circle->position,
                           circle->color,
                           circle->radius,
                           num_vertices,
                           &vertices,
                           &indices);

    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2,
                    GL_FLOAT,
                    sizeof (SDL_Vertex),
                    &vertices->position);

    glEnableClientState(GL_COLOR_ARRAY);
    glColorPointer(4,
                   GL_UNSIGNED_BYTE,
                   sizeof (SDL_Vertex),
                   &vertices->color);

    glDrawElements(GL_TRIANGLES,
                   num_vertices * 3,
                   GL_UNSIGNED_INT,
                   indices);

    glDisableClientState(GL_COLOR_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);

    SDL_free(vertices);
    SDL_free(indices);
}


void use_texture_mode(TextureMode mode) {
    static GLuint lists = 0;
    if (!lists) {
        lists = glGenLists(3);

        /* ghostly */
        glNewList(lists + 0, GL_COMPILE); {
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            glDepthFunc(GL_ALWAYS);
            glDepthMask(GL_FALSE);
            glDisable(GL_ALPHA_TEST);
        } glEndList();

        /* seethrough */
        glNewList(lists + 1, GL_COMPILE); {
            glDisable(GL_BLEND);
            glDepthFunc(GL_LEQUAL);
            glDepthMask(GL_TRUE);
            glEnable(GL_ALPHA_TEST);
            glAlphaFunc(GL_EQUAL, 1.0f);
        } glEndList();

        /* opaque */
        glNewList(lists + 2, GL_COMPILE); {
            glDisable(GL_BLEND);
            glDepthFunc(GL_LESS);
            glDepthMask(GL_TRUE);
            glDisable(GL_ALPHA_TEST);
        } glEndList();
    }

    if (mode == TEXTURE_MODE_GHOSTLY) {
        glCallList(lists + 0);
    } else if (mode == TEXTURE_MODE_SEETHROUGH) {
        glCallList(lists + 1);
    } else {
        glCallList(lists + 2);
    }
}


VertexBufferBuilder build_vertex_buffer(VertexBuffer buffer, size_t bytes) {
    SDL_assert(bytes != 0);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, bytes, NULL, GL_STREAM_DRAW);
    void *mapping = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
    if (!mapping)
        CRY("build_vertex_buffer", "Error mapping a vertex array buffer");

    return (VertexBufferBuilder) {
        .mapping = mapping,
        .bytes_left = bytes,
    };
}


bool push_to_vertex_buffer_builder(VertexBufferBuilder *builder,
                                   void *bytes, size_t size) {
    if (builder->bytes_left == 0)
        return false;

    SDL_memcpy(builder->mapping, bytes, size);
    builder->bytes_left -= size;

    /* trigger data send */
    if (builder->bytes_left == 0) {
        glUnmapBuffer(GL_ARRAY_BUFFER);
        return false;
    }

    builder->mapping = (void *)((uintptr_t)builder->mapping + size);

    return true;
}


void finally_render_sprites(const Primitive2D primitives[],
                            const struct SpriteBatch batch,
                            const VertexBuffer buffer)
{
    (void)buffer;

    GLsizei off;
    GLsizei voff;
    GLsizei uvoff;

    if (!batch.constant_colored) {
        off   = offsetof(ElementIndexedQuad, v1);
        voff  = offsetof(ElementIndexedQuad, v0);
        uvoff = offsetof(ElementIndexedQuad, uv0);
    } else {
        off   = offsetof(ElementIndexedQuadWithoutColor, v1);
        voff  = offsetof(ElementIndexedQuadWithoutColor, v0);
        uvoff = offsetof(ElementIndexedQuadWithoutColor, 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(ElementIndexedQuad, 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);
    else
        textures_bind_repeating(&ctx.texture_cache, primitives->sprite.texture_key);

    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);

    if (!batch.constant_colored)
        glDisableClientState(GL_COLOR_ARRAY);

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


size_t get_sprite_payload_size(struct SpriteBatch batch) {
    if (batch.constant_colored)
        return sizeof (ElementIndexedQuadWithoutColor);
    else
        return sizeof (ElementIndexedQuad);
}


bool push_sprite_payload_to_vertex_buffer_builder(struct SpriteBatch batch,
                                                  VertexBufferBuilder *builder,
                                                  Vec2 v0, Vec2 v1, Vec2 v2, Vec2 v3,
                                                  Vec2 uv0, Vec2 uv1, Vec2 uv2, Vec2 uv3,
                                                  Color color)
{
    if (!batch.constant_colored) {
        ElementIndexedQuad buffer_element = {
            .v0 = v0,
            .v1 = v1,
            .v2 = v2,
            .v3 = v3,

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

            /* equal for all (flat shaded) */
            .c0 = color,
            .c1 = color,
            .c2 = color,
            .c3 = color,
        };

        return push_to_vertex_buffer_builder(builder, &buffer_element, sizeof buffer_element);

    } else {
        ElementIndexedQuadWithoutColor buffer_element = {
            .v0 = v0,
            .v1 = v1,
            .v2 = v2,
            .v3 = v3,

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

        return push_to_vertex_buffer_builder(builder, &buffer_element, sizeof buffer_element);
    }
}


void finally_draw_uncolored_space_traingle_batch(const MeshBatch *batch,
                                                 const TextureKey texture_key,
                                                 const VertexBuffer buffer)
{
    const size_t primitives_len = arrlenu(batch->primitives);

    textures_bind(&ctx.texture_cache, texture_key);

    use_texture_mode(textures_get_mode(&ctx.texture_cache, texture_key));

    glBindBuffer(GL_ARRAY_BUFFER, buffer);

    /* vertex specification*/
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(3,
                    GL_FLOAT,
                    offsetof(struct UncoloredSpaceTrianglePayload, v1),
                    (void *)offsetof(struct UncoloredSpaceTrianglePayload, v0));

    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glClientActiveTexture(GL_TEXTURE0);
    glTexCoordPointer(2,
                      GL_FLOAT,
                      offsetof(struct UncoloredSpaceTrianglePayload, v1),
                      (void *)offsetof(struct UncoloredSpaceTrianglePayload, uv0));

    /* commit for drawing */
    glDrawArrays(GL_TRIANGLES, 0, 3 * (GLint)primitives_len);

    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_VERTEX_ARRAY);

    /* invalidate the buffer immediately */
    glBufferData(GL_ARRAY_BUFFER, 0, NULL, GL_STREAM_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}


bool push_text_payload_to_vertex_buffer_builder(FontData const *font_data,
                                                VertexBufferBuilder *builder,
                                                stbtt_aligned_quad quad)
{
    (void)font_data;

    ElementIndexedQuadWithoutColor buffer_element = {
        .v0 = (Vec2){ quad.x0, quad.y0 },
        .v1 = (Vec2){ quad.x1, quad.y0 },
        .v2 = (Vec2){ quad.x1, quad.y1 },
        .v3 = (Vec2){ quad.x0, quad.y1 },

        .uv0 = (Vec2){ quad.s0, quad.t0 },
        .uv1 = (Vec2){ quad.s1, quad.t0 },
        .uv2 = (Vec2){ quad.s1, quad.t1 },
        .uv3 = (Vec2){ quad.s0, quad.t1 },
    };

    return push_to_vertex_buffer_builder(builder, &buffer_element, sizeof buffer_element);
}


void finally_draw_text(FontData const *font_data,
                       size_t len,
                       Color color,
                       VertexBuffer buffer)
{
    (void)buffer;

    /* vertex specification */
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2,
                    GL_FLOAT,
                    offsetof(ElementIndexedQuadWithoutColor, v1),
                    (void *)(size_t)offsetof(ElementIndexedQuadWithoutColor, v0));

    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glClientActiveTexture(GL_TEXTURE0);
    glTexCoordPointer(2,
                      GL_FLOAT,
                      offsetof(ElementIndexedQuadWithoutColor, v1),
                      (void *)(size_t)offsetof(ElementIndexedQuadWithoutColor, uv0));

    bind_quad_element_buffer();

    use_texture_mode(TEXTURE_MODE_GHOSTLY);

    glBindTexture(GL_TEXTURE_2D, font_data->texture);

    glColor4ub(color.r, color.g, color.b, color.a);

    glDrawElements(GL_TRIANGLES, 6 * (GLsizei)len, 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);

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

    /* TODO: why doesn't it get restored if not placed here? */
    glDepthMask(GL_TRUE);
}


size_t get_text_payload_size(void) {
    return sizeof (ElementIndexedQuadWithoutColor);
}

static void load_cubemap_side(const char *path, GLenum target) {
    SDL_Surface *surface = textures_load_surface(path);
    /* TODO: sanity check whether all of them have same dimensions? */
    glTexImage2D(target,
                 0,
                 GL_RGBA8,
                 surface->w, surface->h,
                 0,
                 surface->format->BytesPerPixel == 4 ? GL_RGBA : GL_RGB,
                 GL_UNSIGNED_BYTE,
                 surface->pixels);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

    SDL_free(surface->pixels);
    SDL_FreeSurface(surface);
}

void finally_render_skybox(char *paths) {
    static GLuint cubemap = 0;
    static char *paths_cache = NULL;

    bool loading_needed = false;

    /* drop it */
    if (!paths_cache || (SDL_strcmp(paths_cache, paths) != 0)) {
        if (cubemap)
            glDeleteTextures(1, &cubemap);
        glGenTextures(1, &cubemap);
        if (paths_cache)
            SDL_free(paths_cache);
        paths_cache = paths;
        loading_needed = true;
    } else
        SDL_free(paths);

    Matrix4 camera_look_at_matrix_solipsist = camera_look_at_matrix;
    camera_look_at_matrix_solipsist.row[3].x = 0;
    camera_look_at_matrix_solipsist.row[3].y = 0;
    camera_look_at_matrix_solipsist.row[3].z = 0;

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

    glDisable(GL_TEXTURE_2D);
    glEnable(GL_TEXTURE_CUBE_MAP);
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap);

    if (loading_needed) {
        /* load all the sides */
        char *expanded = expand_asterisk(paths, "up");
        load_cubemap_side(expanded,  GL_TEXTURE_CUBE_MAP_POSITIVE_Y);
        SDL_free(expanded);

        expanded = expand_asterisk(paths, "down");
        load_cubemap_side(expanded,  GL_TEXTURE_CUBE_MAP_NEGATIVE_Y);
        SDL_free(expanded);

        expanded = expand_asterisk(paths, "east");
        load_cubemap_side(expanded,  GL_TEXTURE_CUBE_MAP_POSITIVE_X);
        SDL_free(expanded);

        expanded = expand_asterisk(paths, "north");
        load_cubemap_side(expanded,  GL_TEXTURE_CUBE_MAP_POSITIVE_Z);
        SDL_free(expanded);

        expanded = expand_asterisk(paths, "west");
        load_cubemap_side(expanded,  GL_TEXTURE_CUBE_MAP_NEGATIVE_X);
        SDL_free(expanded);

        expanded = expand_asterisk(paths, "south");
        load_cubemap_side(expanded,  GL_TEXTURE_CUBE_MAP_NEGATIVE_Z);
        SDL_free(expanded);
    }

    /* TODO: figure out which coordinates to use to not have issues with far z */
    /* TODO: recalculate the list if far z requirement changes */

    static GLuint list = 0;
    if (!list) {
        list = glGenLists(1);

        glNewList(list, GL_COMPILE); {
            /* note: assumes that space pipeline is applied already */
            glDisable(GL_ALPHA_TEST);
            glDepthMask(GL_FALSE);

            glBegin(GL_QUADS); {
                /* up */
                glTexCoord3f(50.f, 50.f, 50.f);
                glVertex3f(50.f, 50.f, 50.f);
                glTexCoord3f(-50.f, 50.f, 50.f);
                glVertex3f(-50.f, 50.f, 50.f);
                glTexCoord3f(-50.f, 50.f, -50.f);
                glVertex3f(-50.f, 50.f, -50.f);
                glTexCoord3f(50.f, 50.f, -50.f);
                glVertex3f(50.f, 50.f, -50.f);

                /* down */
                glTexCoord3f(50.f, -50.f, 50.f);
                glVertex3f(50.f, -50.f, 50.f);
                glTexCoord3f(50.f, -50.f, -50.f);
                glVertex3f(50.f, -50.f, -50.f);
                glTexCoord3f(-50.f, -50.f, -50.f);
                glVertex3f(-50.f, -50.f, -50.f);
                glTexCoord3f(-50.f, -50.f, 50.f);
                glVertex3f(-50.f, -50.f, 50.f);

                /* east */
                glTexCoord3f(50.f, -50.f, 50.f);
                glVertex3f(50.f, -50.f, 50.f);
                glTexCoord3f(50.f, 50.f, 50.f);
                glVertex3f(50.f, 50.f, 50.f);
                glTexCoord3f(50.f, 50.f, -50.f);
                glVertex3f(50.f, 50.f, -50.f);
                glTexCoord3f(50.f, -50.f, -50.f);
                glVertex3f(50.f, -50.f, -50.f);

                /* west */
                glTexCoord3f(-50.f, -50.f, 50.f);
                glVertex3f(-50.f, -50.f, 50.f);
                glTexCoord3f(-50.f, -50.f, -50.f);
                glVertex3f(-50.f, -50.f, -50.f);
                glTexCoord3f(-50.f, 50.f, -50.f);
                glVertex3f(-50.f, 50.f, -50.f);
                glTexCoord3f(-50.f, 50.f, 50.f);
                glVertex3f(-50.f, 50.f, 50.f);

                /* north */
                glTexCoord3f(-50.f, -50.f, 50.f);
                glVertex3f(-50.f, -50.f, 50.f);
                glTexCoord3f(-50.f, 50.f, 50.f);
                glVertex3f(-50.f, 50.f, 50.f);
                glTexCoord3f(50.f, 50.f, 50.f);
                glVertex3f(50.f, 50.f, 50.f);
                glTexCoord3f(50.f, -50.f, 50.f);
                glVertex3f(50.f, -50.f, 50.f);

                /* south */
                glTexCoord3f(-50.f, -50.f, -50.f);
                glVertex3f(-50.f, -50.f, -50.f);
                glTexCoord3f(50.f, -50.f, -50.f);
                glVertex3f(50.f, -50.f, -50.f);
                glTexCoord3f(50.f, 50.f, -50.f);
                glVertex3f(50.f, 50.f, -50.f);
                glTexCoord3f(-50.f, 50.f, -50.f);
                glVertex3f(-50.f, 50.f, -50.f);
            } glEnd();

            glDepthMask(GL_TRUE);
            glDisable(GL_TEXTURE_CUBE_MAP);

        } glEndList();
    }

    glCallList(list);
}


void finally_apply_fog(float start, float end, float density, Color color) {
    if (density < 0.0f || density > 1.0f)
        log_warn("Invalid fog density given, should be in range [0..1]");

    /* TODO: cache it for constant parameters, which is a common case */

    glEnable(GL_FOG);

    glFogf(GL_FOG_DENSITY, density);
    glFogf(GL_FOG_START, start);
    glFogf(GL_FOG_END, end);

    float color_conv[4];
    color_conv[0] = (float)color.r / UINT8_MAX;
    color_conv[1] = (float)color.g / UINT8_MAX;
    color_conv[2] = (float)color.b / UINT8_MAX;
    color_conv[3] = (float)color.a / UINT8_MAX;

    glFogfv(GL_FOG_COLOR, color_conv);
}


void finally_pop_fog(void) {
    glDisable(GL_FOG);
}
