#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_ttf/SDL_ttf.h>

// Shaders
#include "testgputext/shaders/shader.vert.spv.h"
#include "testgputext/shaders/shader.frag.spv.h"
#include "testgputext/shaders/shader-solid.frag.spv.h"
#include "testgputext/shaders/shader-sdf.frag.spv.h"
#include "testgputext/shaders/shader.vert.dxil.h"
#include "testgputext/shaders/shader.frag.dxil.h"
#include "testgputext/shaders/shader-solid.frag.dxil.h"
#include "testgputext/shaders/shader-sdf.frag.dxil.h"
#include "testgputext/shaders/shader.vert.msl.h"
#include "testgputext/shaders/shader.frag.msl.h"
#include "testgputext/shaders/shader-solid.frag.msl.h"
#include "testgputext/shaders/shader-sdf.frag.msl.h"

#define SDL_MATH_3D_IMPLEMENTATION
#include "testgputext/SDL_math3d.h"

#define MAX_VERTEX_COUNT 4000
#define MAX_INDEX_COUNT  6000
#define SUPPORTED_SHADER_FORMATS (SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL)

typedef enum
{
    VertexShader,
    PixelShader,
    PixelShader_Solid,
    PixelShader_SDF,
} Shader;

typedef SDL_FPoint Vec2;

typedef struct Vec3
{
    float x, y, z;
} Vec3;

typedef struct Vertex
{
    Vec3 pos;
    SDL_FColor colour;
    Vec2 uv;
} Vertex;

typedef struct Context
{
    SDL_GPUDevice *device;
    SDL_Window *window;
    SDL_GPUGraphicsPipeline *pipeline;
    SDL_GPUGraphicsPipeline *solid_pipeline;
    SDL_GPUBuffer *vertex_buffer;
    SDL_GPUBuffer *index_buffer;
    SDL_GPUTransferBuffer *transfer_buffer;
    SDL_GPUSampler *sampler;
    SDL_GPUCommandBuffer *cmd_buf;
} Context;

typedef struct GeometryData
{
    Vertex *vertices;
    int vertex_count;
    int *indices;
    int index_count;
} GeometryData;

void check_error_bool(const bool res)
{
    if (!res) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", SDL_GetError());
    }
}

void *check_error_ptr(void *ptr)
{
    if (!ptr) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", SDL_GetError());
    }

    return ptr;
}

SDL_GPUShader *load_shader(
    SDL_GPUDevice *device,
    Shader shader,
    Uint32 sampler_count,
    Uint32 uniform_buffer_count,
    Uint32 storage_buffer_count,
    Uint32 storage_texture_count)
{
    SDL_GPUShaderCreateInfo createinfo;
    createinfo.num_samplers = sampler_count;
    createinfo.num_storage_buffers = storage_buffer_count;
    createinfo.num_storage_textures = storage_texture_count;
    createinfo.num_uniform_buffers = uniform_buffer_count;
    createinfo.props = 0;

    SDL_GPUShaderFormat format = SDL_GetGPUShaderFormats(device);
    if (format & SDL_GPU_SHADERFORMAT_DXIL) {
        createinfo.format = SDL_GPU_SHADERFORMAT_DXIL;
        switch (shader) {
        case VertexShader:
            createinfo.code = shader_vert_dxil;
            createinfo.code_size = shader_vert_dxil_len;
            createinfo.entrypoint = "VSMain";
            break;
        case PixelShader:
            createinfo.code = shader_frag_dxil;
            createinfo.code_size = shader_frag_dxil_len;
            createinfo.entrypoint = "PSMain";
            break;
        case PixelShader_Solid:
            createinfo.code = shader_solid_frag_dxil;
            createinfo.code_size = shader_solid_frag_dxil_len;
            createinfo.entrypoint = "PSMain";
            break;
        case PixelShader_SDF:
            createinfo.code = shader_sdf_frag_dxil;
            createinfo.code_size = shader_sdf_frag_dxil_len;
            createinfo.entrypoint = "PSMain";
            break;
        }
    } else if (format & SDL_GPU_SHADERFORMAT_MSL) {
        createinfo.format = SDL_GPU_SHADERFORMAT_MSL;
        switch (shader) {
        case VertexShader:
            createinfo.code = shader_vert_msl;
            createinfo.code_size = shader_vert_msl_len;
            createinfo.entrypoint = "main0";
            break;
        case PixelShader:
            createinfo.code = shader_frag_msl;
            createinfo.code_size = shader_frag_msl_len;
            createinfo.entrypoint = "main0";
            break;
        case PixelShader_Solid:
            createinfo.code = shader_solid_frag_msl;
            createinfo.code_size = shader_solid_frag_msl_len;
            createinfo.entrypoint = "main0";
            break;
        case PixelShader_SDF:
            createinfo.code = shader_sdf_frag_msl;
            createinfo.code_size = shader_sdf_frag_msl_len;
            createinfo.entrypoint = "main0";
            break;
        }
    } else {
        createinfo.format = SDL_GPU_SHADERFORMAT_SPIRV;
        switch (shader) {
        case VertexShader:
            createinfo.code = shader_vert_spv;
            createinfo.code_size = shader_vert_spv_len;
            createinfo.entrypoint = "main";
            break;
        case PixelShader:
            createinfo.code = shader_frag_spv;
            createinfo.code_size = shader_frag_spv_len;
            createinfo.entrypoint = "main";
            break;
        case PixelShader_Solid:
            createinfo.code = shader_solid_frag_spv;
            createinfo.code_size = shader_solid_frag_spv_len;
            createinfo.entrypoint = "main";
            break;
        case PixelShader_SDF:
            createinfo.code = shader_sdf_frag_spv;
            createinfo.code_size = shader_sdf_frag_spv_len;
            createinfo.entrypoint = "main";
            break;
        }
    }

    if (shader == VertexShader) {
        createinfo.stage = SDL_GPU_SHADERSTAGE_VERTEX;
    } else {
        createinfo.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
    }
    return SDL_CreateGPUShader(device, &createinfo);
}

void queue_text_sequence(GeometryData *geometry_data, TTF_GPUAtlasDrawSequence *sequence, SDL_FColor *colour)
{
    for (int i = 0; i < sequence->num_vertices; i++) {
        Vertex *vert = &geometry_data->vertices[geometry_data->vertex_count + i];
        const SDL_FPoint *pos = &sequence->xy[i];
        vert->pos.x = pos->x;
        vert->pos.y = pos->y;
        vert->colour = *colour;
        if (sequence->uv) {
            vert->uv = sequence->uv[i];
        }
    }

    SDL_memcpy(geometry_data->indices + geometry_data->index_count, sequence->indices, sequence->num_indices * sizeof(int));

    geometry_data->vertex_count += sequence->num_vertices;
    geometry_data->index_count += sequence->num_indices;
}

void queue_text(GeometryData *geometry_data, TTF_GPUAtlasDrawSequence *sequence, SDL_FColor *colour)
{
    for ( ; sequence; sequence = sequence->next) {
        queue_text_sequence(geometry_data, sequence, colour);
    }
}

void set_geometry_data(Context *context, GeometryData *geometry_data)
{
    Vertex *transfer_data = SDL_MapGPUTransferBuffer(context->device, context->transfer_buffer, false);

    SDL_memcpy(transfer_data, geometry_data->vertices, sizeof(Vertex) * geometry_data->vertex_count);
    SDL_memcpy(transfer_data + MAX_VERTEX_COUNT, geometry_data->indices, sizeof(int) * geometry_data->index_count);

    SDL_UnmapGPUTransferBuffer(context->device, context->transfer_buffer);
}

void transfer_data(Context *context, GeometryData *geometry_data)
{
    SDL_GPUCopyPass *copy_pass = check_error_ptr(SDL_BeginGPUCopyPass(context->cmd_buf));
    SDL_UploadToGPUBuffer(
        copy_pass,
        &(SDL_GPUTransferBufferLocation){
            .transfer_buffer = context->transfer_buffer,
            .offset = 0 },
        &(SDL_GPUBufferRegion){
            .buffer = context->vertex_buffer,
            .offset = 0,
            .size = sizeof(Vertex) * geometry_data->vertex_count },
        false);
    SDL_UploadToGPUBuffer(
        copy_pass,
        &(SDL_GPUTransferBufferLocation){
            .transfer_buffer = context->transfer_buffer,
            .offset = sizeof(Vertex) * MAX_VERTEX_COUNT },
        &(SDL_GPUBufferRegion){
            .buffer = context->index_buffer,
            .offset = 0,
            .size = sizeof(int) * geometry_data->index_count },
        false);
    SDL_EndGPUCopyPass(copy_pass);
}

void draw(Context *context, SDL_Mat4X4 *matrices, int num_matrices, TTF_GPUAtlasDrawSequence *draw_sequence)
{
    SDL_GPUTexture *swapchain_texture;
    check_error_bool(SDL_WaitAndAcquireGPUSwapchainTexture(context->cmd_buf, context->window, &swapchain_texture, NULL, NULL));

    if (swapchain_texture != NULL) {
        SDL_GPUColorTargetInfo colour_target_info = { 0 };
        colour_target_info.texture = swapchain_texture;
        colour_target_info.clear_color = (SDL_FColor){ 0.3f, 0.4f, 0.5f, 1.0f };
        colour_target_info.load_op = SDL_GPU_LOADOP_CLEAR;
        colour_target_info.store_op = SDL_GPU_STOREOP_STORE;

        SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(context->cmd_buf, &colour_target_info, 1, NULL);

        bool solid_pipeline = false;
        SDL_BindGPUGraphicsPipeline(render_pass, context->pipeline);
        SDL_BindGPUVertexBuffers(
            render_pass, 0,
            &(SDL_GPUBufferBinding){
                .buffer = context->vertex_buffer, .offset = 0 },
            1);
        SDL_BindGPUIndexBuffer(
            render_pass,
            &(SDL_GPUBufferBinding){
                .buffer = context->index_buffer, .offset = 0 },
            SDL_GPU_INDEXELEMENTSIZE_32BIT);
        SDL_PushGPUVertexUniformData(context->cmd_buf, 0, matrices, sizeof(SDL_Mat4X4) * num_matrices);

        int index_offset = 0, vertex_offset = 0;
        for (TTF_GPUAtlasDrawSequence *seq = draw_sequence; seq != NULL; seq = seq->next) {
            if (seq->atlas_texture) {
                if (solid_pipeline) {
                    SDL_BindGPUGraphicsPipeline(render_pass, context->pipeline);
                    solid_pipeline = false;
                }

                SDL_BindGPUFragmentSamplers(
                    render_pass, 0,
                    &(SDL_GPUTextureSamplerBinding){
                        .texture = seq->atlas_texture, .sampler = context->sampler },
                    1);
            } else {
                if (!solid_pipeline) {
                    SDL_BindGPUGraphicsPipeline(render_pass, context->solid_pipeline);
                    solid_pipeline = true;
                }
            }

            SDL_DrawGPUIndexedPrimitives(render_pass, seq->num_indices, 1, index_offset, vertex_offset, 0);

            index_offset += seq->num_indices;
            vertex_offset += seq->num_vertices;
        }
        SDL_EndGPURenderPass(render_pass);
    }
}

void free_context(Context *context)
{
    SDL_ReleaseGPUTransferBuffer(context->device, context->transfer_buffer);
    SDL_ReleaseGPUSampler(context->device, context->sampler);
    SDL_ReleaseGPUBuffer(context->device, context->vertex_buffer);
    SDL_ReleaseGPUBuffer(context->device, context->index_buffer);
    SDL_ReleaseGPUGraphicsPipeline(context->device, context->pipeline);
    SDL_ReleaseGPUGraphicsPipeline(context->device, context->solid_pipeline);
    SDL_ReleaseWindowFromGPUDevice(context->device, context->window);
    SDL_DestroyGPUDevice(context->device);
    SDL_DestroyWindow(context->window);
}

int main(int argc, char *argv[])
{
    const char *font_filename = NULL;
    bool use_SDF = false;
    int style;

    (void)argc;
    for (int i = 1; argv[i]; ++i) {
        if (SDL_strcasecmp(argv[i], "--sdf") == 0) {
            use_SDF = true;
        } else if (*argv[i] == '-') {
            break;
        } else {
            font_filename = argv[i];
            break;
        }
    }
    if (!font_filename) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Usage: testgputext [--sdf] FONT_FILENAME");
        return 2;
    }

    check_error_bool(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS));

    bool running = true;
    Context context = { 0 };

    context.window = check_error_ptr(SDL_CreateWindow("GPU text test", 800, 600, 0));

    context.device = check_error_ptr(SDL_CreateGPUDevice(SUPPORTED_SHADER_FORMATS, true, NULL));
    check_error_bool(SDL_ClaimWindowForGPUDevice(context.device, context.window));

    SDL_GPUShader *vertex_shader = check_error_ptr(load_shader(context.device, VertexShader, 0, 1, 0, 0));
    SDL_GPUShader *fragment_shader = check_error_ptr(load_shader(context.device, use_SDF ? PixelShader_SDF : PixelShader, 1, 0, 0, 0));
    SDL_GPUShader *solid_shader = check_error_ptr(load_shader(context.device, PixelShader_Solid, 0, 0, 0, 0));

    SDL_GPUGraphicsPipelineCreateInfo pipeline_create_info = {
        .target_info = {
            .num_color_targets = 1,
            .color_target_descriptions = (SDL_GPUColorTargetDescription[]){{
                .format = SDL_GetGPUSwapchainTextureFormat(context.device, context.window),
                .blend_state = (SDL_GPUColorTargetBlendState){
                    .enable_blend = true,
                    .alpha_blend_op = SDL_GPU_BLENDOP_ADD,
                    .color_blend_op = SDL_GPU_BLENDOP_ADD,
                    .color_write_mask = 0xF,
                    .src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA,
                    .dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_DST_ALPHA,
                    .src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA,
                    .dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA
                }
            }},
            .has_depth_stencil_target = false,
            .depth_stencil_format = SDL_GPU_TEXTUREFORMAT_INVALID /* Need to set this to avoid missing initializer for field error */
        },
        .vertex_input_state = (SDL_GPUVertexInputState){
            .num_vertex_buffers = 1,
            .vertex_buffer_descriptions = (SDL_GPUVertexBufferDescription[]){{
                .slot = 0,
                .input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX,
                .instance_step_rate = 0,
                .pitch = sizeof(Vertex)
            }},
            .num_vertex_attributes = 3,
            .vertex_attributes = (SDL_GPUVertexAttribute[]){{
                .buffer_slot = 0,
                .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3,
                .location = 0,
                .offset = 0
            }, {
                .buffer_slot = 0,
                .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4,
                .location = 1,
                .offset = sizeof(float) * 3
            }, {
                .buffer_slot = 0,
                .format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2,
                .location = 2,
                .offset = sizeof(float) * 7
            }}
        },
        .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST,
        .vertex_shader = vertex_shader,
        .fragment_shader = fragment_shader
    };
    context.pipeline = check_error_ptr(SDL_CreateGPUGraphicsPipeline(context.device, &pipeline_create_info));
    pipeline_create_info.fragment_shader = solid_shader;
    context.solid_pipeline = check_error_ptr(SDL_CreateGPUGraphicsPipeline(context.device, &pipeline_create_info));

    SDL_ReleaseGPUShader(context.device, vertex_shader);
    SDL_ReleaseGPUShader(context.device, fragment_shader);
    SDL_ReleaseGPUShader(context.device, solid_shader);

    SDL_GPUBufferCreateInfo vbf_info = {
        .usage = SDL_GPU_BUFFERUSAGE_VERTEX,
        .size = sizeof(Vertex) * MAX_VERTEX_COUNT
    };
    context.vertex_buffer = check_error_ptr(SDL_CreateGPUBuffer(context.device, &vbf_info));

    SDL_GPUBufferCreateInfo ibf_info = {
        .usage = SDL_GPU_BUFFERUSAGE_INDEX,
        .size = sizeof(int) * MAX_INDEX_COUNT
    };
    context.index_buffer = check_error_ptr(SDL_CreateGPUBuffer(context.device, &ibf_info));

    SDL_GPUTransferBufferCreateInfo tbf_info = {
        .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
        .size = (sizeof(Vertex) * MAX_VERTEX_COUNT) + (sizeof(int) * MAX_INDEX_COUNT)
    };
    context.transfer_buffer = check_error_ptr(SDL_CreateGPUTransferBuffer(context.device, &tbf_info));

    SDL_GPUSamplerCreateInfo sampler_info = {
        .min_filter = SDL_GPU_FILTER_LINEAR,
        .mag_filter = SDL_GPU_FILTER_LINEAR,
        .mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR,
        .address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
        .address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
        .address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE
    };
    context.sampler = check_error_ptr(SDL_CreateGPUSampler(context.device, &sampler_info));

    GeometryData geometry_data = { 0 };
    geometry_data.vertices = SDL_calloc(MAX_VERTEX_COUNT, sizeof(Vertex));
    geometry_data.indices = SDL_calloc(MAX_INDEX_COUNT, sizeof(int));

    check_error_bool(TTF_Init());
    TTF_Font *font = check_error_ptr(TTF_OpenFont(font_filename, 50)); /* Preferably use a Monospaced font */
    if (!font) {
        running = false;
    }
    SDL_Log("SDF %s", use_SDF ? "enabled" : "disabled");
    TTF_SetFontSDF(font, use_SDF);
    TTF_SetFontWrapAlignment(font, TTF_HORIZONTAL_ALIGN_CENTER);
    TTF_TextEngine *engine = check_error_ptr(TTF_CreateGPUTextEngine(context.device));

    char str[] = "     \nSDL is cool";
    TTF_Text *text = check_error_ptr(TTF_CreateText(engine, font, str, 0));

    SDL_Mat4X4 *matrices = (SDL_Mat4X4[]){
        SDL_MatrixPerspective(SDL_PI_F / 2.0f, 800.0f / 600.0f, 0.1f, 100.0f),
        SDL_MatrixIdentity()
    };

    float rot_angle = 0;
    SDL_FColor colour = {1.0f, 1.0f, 0.0f, 1.0f};

    while (running) {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
            case SDL_EVENT_KEY_UP:
                switch (event.key.key) {
                case SDLK_S:
                    // Toggle strike-through style
                    style = TTF_GetFontStyle(font);
                    if (style & TTF_STYLE_STRIKETHROUGH) {
                        style &= ~TTF_STYLE_STRIKETHROUGH;
                    } else {
                        style |= TTF_STYLE_STRIKETHROUGH;
                    }
                    TTF_SetFontStyle(font, style);
                    break;

                case SDLK_U:
                    // Toggle underline style
                    style = TTF_GetFontStyle(font);
                    if (style & TTF_STYLE_UNDERLINE) {
                        style &= ~TTF_STYLE_UNDERLINE;
                    } else {
                        style |= TTF_STYLE_UNDERLINE;
                    }
                    TTF_SetFontStyle(font, style);
                    break;

                case SDLK_ESCAPE:
                    running = false;
                    break;

                default:
                    break;
                }
                break;
            case SDL_EVENT_QUIT:
                running = false;
                break;
            }
        }

        for (int i = 0; i < 5; i++) {
            str[i] = 65 + SDL_rand(26);
        }
        TTF_SetTextString(text, str, 0);

        int tw, th;
        check_error_bool(TTF_GetTextSize(text, &tw, &th));

        rot_angle = SDL_fmodf(rot_angle + 0.01, 2 * SDL_PI_F);

        // Create a model matrix to make the text rotate
        SDL_Mat4X4 model;
        model = SDL_MatrixIdentity();
        model = SDL_MatrixMultiply(model, SDL_MatrixTranslation((SDL_Vec3){ 0.0f, 0.0f, -80.0f }));
        model = SDL_MatrixMultiply(model, SDL_MatrixScaling((SDL_Vec3){ 0.3f, 0.3f, 0.3f}));
        model = SDL_MatrixMultiply(model, SDL_MatrixRotationY(rot_angle));
        model = SDL_MatrixMultiply(model, SDL_MatrixTranslation((SDL_Vec3){ -tw / 2.0f, th / 2.0f, 0.0f }));
        matrices[1] = model;

        // Get the text data and queue the text in a buffer for drawing later
        TTF_GPUAtlasDrawSequence *sequence = TTF_GetGPUTextDrawData(text);
        queue_text(&geometry_data, sequence, &colour);

        set_geometry_data(&context, &geometry_data);

        context.cmd_buf = check_error_ptr(SDL_AcquireGPUCommandBuffer(context.device));
        transfer_data(&context, &geometry_data);
        draw(&context, matrices, 2, sequence);
        SDL_SubmitGPUCommandBuffer(context.cmd_buf);

        geometry_data.vertex_count = 0;
        geometry_data.index_count = 0;
    }

    SDL_free(geometry_data.vertices);
    SDL_free(geometry_data.indices);
    TTF_DestroyText(text);
    TTF_DestroyGPUTextEngine(engine);
    TTF_CloseFont(font);
    TTF_Quit();
    free_context(&context);
    SDL_Quit();

    return 0;
}
