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

#include <stb_ds.h>

#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif


void setup_viewport(int x, int y, int width, int height) {
    glViewport(x, y, width, height);
}


VertexBuffer create_vertex_buffer(void) {
    GLuint result;
    glGenBuffers(1, &result);
    return result;
}


void delete_vertex_buffer(VertexBuffer buffer) {
    glDeleteBuffers(1, &buffer);
}


void specify_vertex_buffer(VertexBuffer buffer, void const *data, size_t bytes) {
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, bytes, data, GL_STREAM_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}


VertexBuffer get_quad_element_buffer(void) {
    static VertexBuffer buffer = 0;

    /* it's only generated once at runtime */
    /* TODO: use builder interface, not direct calls (glMapBuffer isn't portable) */
    if (buffer == 0) {
        buffer = create_vertex_buffer();
        VertexBufferBuilder builder = build_vertex_buffer(buffer, sizeof (GLshort) * QUAD_ELEMENT_BUFFER_LENGTH * 6 );

        for (size_t i = 0; i < QUAD_ELEMENT_BUFFER_LENGTH; ++i) {
            ((GLshort *)builder.base)[i * 6 + 0] = (GLshort)(i * 4 + 0);
            ((GLshort *)builder.base)[i * 6 + 1] = (GLshort)(i * 4 + 1);
            ((GLshort *)builder.base)[i * 6 + 2] = (GLshort)(i * 4 + 2);
            ((GLshort *)builder.base)[i * 6 + 3] = (GLshort)(i * 4 + 2);
            ((GLshort *)builder.base)[i * 6 + 4] = (GLshort)(i * 4 + 3);
            ((GLshort *)builder.base)[i * 6 + 5] = (GLshort)(i * 4 + 0);
        }

        finish_vertex_builder(&builder);
    }

    SDL_assert_always(buffer);

    return buffer;
}


VertexBuffer get_circle_element_buffer(void) {
    static VertexBuffer buffer = 0;

    if (buffer == 0) {
        buffer = create_vertex_buffer();
        VertexBufferBuilder builder = build_vertex_buffer(buffer, sizeof (GLshort) * (CIRCLE_VERTICES_MAX - 2) * 3);

        for (size_t i = 1; i < CIRCLE_VERTICES_MAX - 1; ++i) {
            /* first one is center point index, always zero */
            ((GLshort *)builder.base)[(i - 1) * 3 + 0] = 0;

            /* generated point index */
            ((GLshort *)builder.base)[(i - 1) * 3 + 1] = (GLshort)i;
            ((GLshort *)builder.base)[(i - 1) * 3 + 2] = (GLshort)i + 1;
        }

        finish_vertex_builder(&builder);
    }

    SDL_assert_always(buffer);

    return buffer;
}


/* potentially double buffered array of vertex array handles */
/* we assume they will be refilled fully each frame */
static size_t scratch_va_front_used, scratch_va_back_used;
static GLuint *front_scratch_vertex_arrays, *back_scratch_vertex_arrays;
static GLuint **current_scratch_vertex_array = &front_scratch_vertex_arrays;

void restart_scratch_vertex_arrays(void) {
    scratch_va_front_used = 0;
    scratch_va_back_used = 0;

    if (ctx.render_double_buffered) {
        current_scratch_vertex_array = current_scratch_vertex_array == &front_scratch_vertex_arrays ?
                &back_scratch_vertex_arrays : &front_scratch_vertex_arrays;
    }
}


GLuint get_scratch_vertex_array(void) {
    size_t *used = current_scratch_vertex_array == &front_scratch_vertex_arrays ?
            &scratch_va_front_used : &scratch_va_back_used;

    if (arrlenu(*current_scratch_vertex_array) <= *used) {
        GLuint handle;
        glGenBuffers(1, &handle);
        arrpush(*current_scratch_vertex_array, handle);
    }

    (*used)++;
    return (*current_scratch_vertex_array)[*used - 1];
}


GPUTexture create_gpu_texture(TextureFilter filter, bool generate_mipmaps) {
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

#if !defined(EMSCRIPTEN)
    glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, (GLboolean)generate_mipmaps);
#else
    if (generate_mipmaps)
        glGenerateMipmap(GL_TEXTURE_2D);
#endif

    if (filter == TEXTURE_FILTER_NEAREAST) {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    } else if (filter == TEXTURE_FILTER_LINEAR) {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    }

    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

#if !defined(EMSCRIPTEN)
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
#endif

    glBindTexture(GL_TEXTURE_2D, 0);
    return texture;
}


void delete_gpu_texture(GPUTexture texture) {
    glDeleteTextures(1, &texture);
}


void upload_gpu_texture(GPUTexture texture, void *pixels, int channels, int width, int height) {
    glBindTexture(GL_TEXTURE_2D, texture);

    int format_internal, format;
    if (channels == 4) {
        #ifdef EMSCRIPTEN
        format_internal = GL_RGBA;
        #else
        format_internal = GL_RGBA8;
        #endif
        format = GL_RGBA;
    } else if (channels == 3) {
        #ifdef EMSCRIPTEN
        format_internal = GL_RGBA;
        #else
        format_internal = GL_RGBA8;
        #endif
        format = GL_RGB;
    } else if (channels == 1) {
        format_internal = GL_ALPHA;
        format = GL_ALPHA;
    } else {
        CRY("upload_gpu_texture", "Unsupported channel count");
        return;
    }

    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 format_internal,
                 width,
                 height,
                 0,
                 format,
                 GL_UNSIGNED_BYTE,
                 pixels);

    glBindTexture(GL_TEXTURE_2D, 0);
}


void bind_gpu_texture(GPUTexture texture) {
    glBindTexture(GL_TEXTURE_2D, texture);
}
