#include "twn_draw_c.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_engine_context_c.h"
#include "twn_types.h"
#include "twn_deferred_commands.h"
#include "twn_gl_any_rendering_c.h"

#include <glad/glad.h>
#include <stb_ds.h>


static TextureMode texture_mode_last_used = TEXTURE_MODE_UNKNOWN;
static Pipeline pipeline_last_used = PIPELINE_NO;


void start_render_frame(void) {
    clear_draw_buffer();
}


void end_render_frame(void) {
    if (!ctx.render_double_buffered || ctx.game.frame_number == 1) {
        issue_deferred_draw_commands();
        SDL_GL_SwapWindow(ctx.window);
        arrsetlen(deferred_commands, 0);
        restart_scratch_vertex_arrays();
    } else {
        /* instead of waiting for frame to finish for the swap, we continue */
        /* while issuing new state for the next call, but deferring any fragment emitting calls for later  */
        /* actual swap will happen when next frame is fully finished, introducing a delay */
        SDL_GL_SwapWindow(ctx.window);
        issue_deferred_draw_commands();
        restart_scratch_vertex_arrays();
        glFlush();
        arrsetlen(deferred_commands, 0);
    }
}


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

    texture_mode_last_used = -1;
    pipeline_last_used = PIPELINE_SPACE;
}


void finally_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)
                glEnable(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, 0, 1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    texture_mode_last_used = -1;
    pipeline_last_used = PIPELINE_2D;
}


void finally_use_texture_mode(TextureMode mode) {
    if (texture_mode_last_used == mode)
        return;

    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_LESS);
            glDepthMask(GL_FALSE);
            glDisable(GL_ALPHA_TEST);
        } glEndList();

        /* seethrough */
        glNewList(lists + 1, GL_COMPILE); {
            glDisable(GL_BLEND);
            glDepthFunc(GL_LESS);
            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);
    }

    texture_mode_last_used = mode;
}


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 const *bytes, size_t size) {
    if (builder->bytes_left == 0)
        return false;

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


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(DeferredCommandDrawSkybox command) {
    static GLuint cubemap = 0;
    static char *paths_cache = NULL;

    bool loading_needed = false;

    /* drop it */
    if (!paths_cache || (SDL_strcmp(paths_cache, command.paths) != 0)) {
        if (cubemap)
            glDeleteTextures(1, &cubemap);
        glGenTextures(1, &cubemap);
        if (paths_cache)
            SDL_free(paths_cache);
        paths_cache = command.paths;
        loading_needed = true;
    } else
        SDL_free(command.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(command.paths, "up");
        load_cubemap_side(expanded, GL_TEXTURE_CUBE_MAP_POSITIVE_Y);
        SDL_free(expanded);

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

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

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

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

        expanded = expand_asterisk(command.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);

            /* removes near/far plane comparison and discard */
            if (GLAD_GL_ARB_depth_clamp)
                glEnable(GL_DEPTH_CLAMP);

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

            if (GLAD_GL_ARB_depth_clamp)
                glDisable(GL_DEPTH_CLAMP);

            glDepthMask(GL_TRUE);
            glDisable(GL_TEXTURE_CUBE_MAP);

        } glEndList();
    }

    glCallList(list);
}


void finally_apply_fog(DeferredCommandApplyFog command) {
    if (command.density < 0.0f || command.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, command.density);
    glFogf(GL_FOG_START, command.start);
    glFogf(GL_FOG_END, command.end);

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

    glFogfv(GL_FOG_COLOR, color_conv);
}


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


void finally_set_depth_range(DeferredCommandDepthRange command) {
    glDepthRange(command.low, command.high);
}


void finally_clear_draw_buffer(DeferredCommandClear command) {
    glClearColor((1.0f / 255) * command.color.r,
                 (1.0f / 255) * command.color.g,
                 (1.0f / 255) * command.color.b,
                 (1.0f / 255) * command.color.a);

    /* needed as we might mess with it */
    glDepthRange(0.0, 1.0);
    glDepthMask(GL_TRUE);

    glClear((command.clear_color   ? GL_COLOR_BUFFER_BIT   : 0) |
            (command.clear_depth   ? GL_DEPTH_BUFFER_BIT   : 0) |
            (command.clear_stencil ? GL_STENCIL_BUFFER_BIT : 0) );
}


void finally_draw_command(DeferredCommandDraw command) {
    /* TODO: don't assume a single vertex array ? */
    SDL_assert(command.vertices.arity != 0);
    SDL_assert(command.vertices.buffer);
    SDL_assert((command.element_buffer && command.element_count != 0) || command.primitive_count != 0);

    glBindBuffer(GL_ARRAY_BUFFER, command.vertices.buffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, command.element_buffer);

    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(command.vertices.arity,
                    command.vertices.type,
                    command.vertices.stride,
                    (void *)command.vertices.offset);

    if (command.texcoords.arity != 0) {
        SDL_assert(command.texcoords.buffer == command.vertices.buffer);

        glEnableClientState(GL_TEXTURE_COORD_ARRAY);
        glClientActiveTexture(GL_TEXTURE0);
        glTexCoordPointer(command.texcoords.arity,
                          command.texcoords.type,
                          command.texcoords.stride,
                          (void *)command.texcoords.offset);
    }

    if (command.colors.arity != 0) {
        SDL_assert(command.colors.buffer == command.vertices.buffer);

        glEnableClientState(GL_COLOR_ARRAY);
        glColorPointer(command.colors.arity,
                       command.colors.type,
                       command.colors.stride,
                       (void *)command.colors.offset);

    } else if (command.constant_colored)
        glColor4ub(command.color.r,
                   command.color.g,
                   command.color.b,
                   command.color.a);

    if (command.textured) {
        if (command.uses_gpu_key)
            glBindTexture(GL_TEXTURE_2D, command.gpu_texture);
        else if (command.texture_repeat)
            textures_bind_repeating(&ctx.texture_cache, command.texture_key);
        else
            textures_bind(&ctx.texture_cache, command.texture_key);
    }

    if (command.element_buffer) {
        SDL_assert(command.element_count != 0);
        if (command.range_start == command.range_end)
            glDrawElements(GL_TRIANGLES, command.element_count, GL_UNSIGNED_SHORT, NULL);
        else
            glDrawRangeElements(GL_TRIANGLES,
                                command.range_start,
                                command.range_end,
                                command.element_count,
                                GL_UNSIGNED_SHORT,
                                NULL);
    } else {
        SDL_assert(command.primitive_count != 0);
        glDrawArrays(GL_TRIANGLES, 0, command.primitive_count);
    }

    /* state clearing */

    if (command.textured)
        glBindTexture(GL_TEXTURE_2D, 0);

    if (command.colors.arity != 0)
        glDisableClientState(GL_COLOR_ARRAY);

    if (command.texcoords.arity != 0)
        glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glDisableClientState(GL_VERTEX_ARRAY);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
