//
// Created by Vicente Ferrari Smith on 26.02.26.
//

#define NS_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#define MTK_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION

#include <Foundation/Foundation.hpp>
#include <Metal/Metal.hpp>
#include <QuartzCore/QuartzCore.hpp>

#include "metal.h"
#include "../graphics.h"
#include "../texture.h"
#include <print>
#include <iostream>
#include <fstream>
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_COCOA
#include <GLFW/glfw3native.h>
#include <objc/message.h>
#include <objc/objc.h>

extern int32_t window_width;
extern int32_t window_height;

Device                    metal_device{};
MTL::CommandQueue        *queue{};
CA::MetalLayer           *metal_layer{};
CA::MetalDrawable        *metal_drawable{};

Renderer renderer;

void upload_texture(
    const int w,
    const int h,
    const void *pixels,
    Texture *texture)
{
    texture->p_texture = new PlatformTexture{};

    MTL::TextureDescriptor *td = MTL::TextureDescriptor::alloc()->init();
    td->setPixelFormat(MTL::PixelFormatRGBA8Unorm);
    td->setWidth(w);
    td->setHeight(h);
    td->setStorageMode(MTL::StorageModeShared);
    td->setUsage(MTL::TextureUsageShaderRead);

    texture->p_texture->texture = metal_device.device->newTexture(td);

    MTL::Region region = MTL::Region(0, 0, 0, w, h, 1);
    NS::UInteger bytesPerRow = 4 * w;

    texture->p_texture->texture->replaceRegion(region, 0, pixels, bytesPerRow);

    td->release();
}

void create_metal_layer(GLFWwindow *window) {
    void *ns_window = glfwGetCocoaWindow(window);
    if (!ns_window) {
        throw std::runtime_error("Failed to get Cocoa window from GLFWwindow");
    }

    SEL contentViewSel = sel_registerName("contentView");
    id content_view = ((id (*)(id, SEL))objc_msgSend)((id)ns_window, contentViewSel);

    SEL setWantsLayerSel = sel_registerName("setWantsLayer:");
    ((void (*)(id, SEL, bool))objc_msgSend)(content_view, setWantsLayerSel, true);

    metal_layer = CA::MetalLayer::layer()->retain();

    SEL setLayerSel = sel_registerName("setLayer:");
    ((void (*)(id, SEL, id))objc_msgSend)(content_view, setLayerSel, (id)metal_layer);

    metal_layer->setDevice(metal_device.device);
    metal_layer->setPixelFormat(MTL::PixelFormatRGBA16Float);
    metal_layer->setFramebufferOnly(true);
    int fb_w, fb_h;
    glfwGetFramebufferSize(window, &fb_w, &fb_h);
    metal_layer->setDrawableSize(CGSizeMake(fb_w, fb_h));
    CGColorSpaceRef p3Space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
    metal_layer->setColorspace(p3Space);
    CGColorSpaceRelease(p3Space);
}

void load_metal_shader(const std::string &shader_path,
                       const std::string &vertex_fn_name,
                       const std::string &fragment_fn_name,
                       MTL::Function **vertex_shader,
                       MTL::Function **fragment_shader)
{
    NS::Error *error = nullptr;
    MTL::Library *library = nullptr;

    auto ends_with = [](const std::string& s, const char* suf) -> bool {
        const size_t n = std::strlen(suf);
        return s.size() >= n && s.compare(s.size() - n, n, suf) == 0;
    };

    if (ends_with(shader_path, ".metal")) {
        // Compile from source at runtime
        std::ifstream file(shader_path, std::ios::in | std::ios::binary);
        if (!file.is_open()) {
            throw std::runtime_error("Failed to open .metal source file");
        }
        std::string src;
        file.seekg(0, std::ios::end);
        src.resize(static_cast<size_t>(file.tellg()));
        file.seekg(0, std::ios::beg);
        file.read(src.data(), static_cast<std::streamsize>(src.size()));
        file.close();

        NS::String* source = NS::String::string(src.c_str(), NS::UTF8StringEncoding);
        MTL::CompileOptions* opts = MTL::CompileOptions::alloc()->init();
        library = metal_device.device->newLibrary(source, opts, &error);
        opts->release();
    } else {
        // Load a precompiled metallib from file path
        NS::String *nsPath = NS::String::string(shader_path.c_str(), NS::UTF8StringEncoding);
        library = metal_device.device->newLibrary(nsPath, &error);
    }

    if (error || library == nullptr) {
        if (error) {
            // Extract the actual compiler error message
            const char* errorMessage = error->localizedDescription()->utf8String();
            std::string detailedError = "Metal Library Error: ";
            detailedError += errorMessage;

            // It is good practice to release the error object if it exists
            error->release();

            throw std::runtime_error(detailedError);
        }
        throw std::runtime_error("Failed to create Metal library (Unknown error)");
    }
    NS::String *vname = NS::String::string(vertex_fn_name.c_str(), NS::UTF8StringEncoding);
    NS::String *fname = NS::String::string(fragment_fn_name.c_str(), NS::UTF8StringEncoding);
    *vertex_shader = library->newFunction(vname);
    *fragment_shader = library->newFunction(fname);

    if (*vertex_shader == nullptr || *fragment_shader == nullptr) {
        throw std::runtime_error("Failed to create Metal shader functions");
    }

    library->release();
}

void create_command_queue() {
    queue = metal_device.device->newCommandQueue();
}

void create_device() {
    metal_device.device = MTL::CreateSystemDefaultDevice();
}

void platform_graphics_init(GLFWwindow *window) {
    std::println("wow, we are on macos!! crazy!!");

    auto slangTargets{ std::to_array<slang::TargetDesc>({ {
        .format = SLANG_METAL,
        .profile = slangGlobalSession->findProfile("metallib_2_4"),
    } })};
    // auto slangOptions{ std::to_array<slang::CompilerOptionEntry>({ {
    //     slang::CompilerOptionName::EmitSpirvDirectly,
    //     {slang::CompilerOptionValueKind::Int, 1}
    // } })};
    const char *search_paths[] = {"shaders"};
    slang::SessionDesc slangSessionDesc{
        .targets = slangTargets.data(),
        .targetCount = SlangInt(slangTargets.size()),
        .searchPaths = search_paths,
        .searchPathCount = 1,
        // .defaultMatrixLayoutMode = SLANG_MATRIX_LAYOUT_COLUMN_MAJOR,
    };

    slangGlobalSession->createSession(slangSessionDesc, slangSession.writeRef());

    create_device();
    create_metal_layer(window);
    create_command_queue();

    renderer = Renderer(window);
}

void graphics_deinit() {
    queue->release();
    metal_layer->release();
    metal_device.device->release();
}

void begin_frame() {
    renderer.begin_frame();
}

void end_frame(GLFWwindow *window) {
    auto pPool = NS::AutoreleasePool::alloc()->init();
    metal_drawable = metal_layer->nextDrawable();

    renderer.end_frame(window);

    pPool->release();
}

void submit_quad(glm::vec2 pos, glm::vec2 scale) {
    renderer.submit_quad({pos.x, pos.y}, {scale.x, scale.y});
}

void submit_sprite(glm::vec2 pos, const sprite_t &sprite) {
    renderer.submit_sprite({pos.x, pos.y}, sprite);
}
