#include "rendering/sdl3gpu/sdl3gpu_shader_backend.hpp"

#include <iostream>

#include "rendering/sdl3gpu/shader_factory.hpp"

namespace Rendering {

    namespace {

        void logInfo(const std::string& msg) { std::cout << "[INFO] " << msg << '\n'; }
        void logError(const std::string& msg) { std::cerr << "[ERROR] " << msg << '\n'; }

#ifdef __APPLE__
        constexpr SDL_GPUShaderFormat SHADER_FORMAT = SDL_GPU_SHADERFORMAT_MSL;
        constexpr const char* VERTEX_ENTRY = "passthrough_vs";
        constexpr const char* FRAGMENT_ENTRY = "test_fs";  // overridden per-shader (see loadShader)
        constexpr const char* VERTEX_SUFFIX = ".vert.msl";
        constexpr const char* FRAGMENT_SUFFIX = ".frag.msl";
#else
        constexpr SDL_GPUShaderFormat SHADER_FORMAT = SDL_GPU_SHADERFORMAT_SPIRV;
        constexpr const char* VERTEX_ENTRY = "main";
        constexpr const char* FRAGMENT_ENTRY = "main";
        constexpr const char* VERTEX_SUFFIX = ".vert.spv";
        constexpr const char* FRAGMENT_SUFFIX = ".frag.spv";
#endif

    }  // namespace

    Sdl3GpuShaderBackend::~Sdl3GpuShaderBackend() { cleanup(); }

    auto Sdl3GpuShaderBackend::init(SDL_Window* window) -> bool {
        window_ = window;
        return createDevice();
    }

    auto Sdl3GpuShaderBackend::createDevice() -> bool {
        device_ = SDL_CreateGPUDevice(SHADER_FORMAT, false, nullptr);
        if (device_ == nullptr) {
            logError(std::string("SDL_CreateGPUDevice failed: ") + SDL_GetError());
            return false;
        }
        if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
            logError(std::string("SDL_ClaimWindowForGPUDevice failed: ") + SDL_GetError());
            SDL_DestroyGPUDevice(device_);
            device_ = nullptr;
            return false;
        }
        SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode());

        const char* name = SDL_GetGPUDeviceDriver(device_);
        const std::string raw = (name != nullptr) ? name : "GPU";
        if (raw == "vulkan") { driver_name_ = "Vulkan"; }
        else if (raw == "metal") { driver_name_ = "Metal"; }
        else if (raw == "d3d12") { driver_name_ = "D3D12"; }
        else if (!raw.empty()) {
            driver_name_ = raw;
            driver_name_[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(driver_name_[0])));
        } else {
            driver_name_ = "GPU";
        }
        logInfo("GPU driver: " + driver_name_);
        return true;
    }

    auto Sdl3GpuShaderBackend::loadVertexShaderFor(const ShaderProgramSpec& spec) -> bool {
        if (vertex_shader_ != nullptr) { return true; }

        const std::filesystem::path common_dir = spec.folder.parent_path() / "_common";
        const std::filesystem::path vertex_path = common_dir / (std::string("passthrough") + VERTEX_SUFFIX);

        vertex_shader_ = Sdl3Gpu::loadShaderFromFile(device_, vertex_path, SHADER_FORMAT,
            VERTEX_ENTRY, SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
        if (vertex_shader_ == nullptr) {
            logError("Failed to load shared vertex shader: " + vertex_path.string() + " (" + SDL_GetError() + ")");
            return false;
        }
        logInfo("Loaded shared vertex shader: " + vertex_path.filename().string());
        return true;
    }

    auto Sdl3GpuShaderBackend::buildPipeline(SDL_GPUShader* fragment) -> SDL_GPUGraphicsPipeline* {
        const SDL_GPUTextureFormat SWAPCHAIN_FORMAT = SDL_GetGPUSwapchainTextureFormat(device_, window_);

        SDL_GPUColorTargetBlendState no_blend{};
        SDL_GPUColorTargetDescription color_target{};
        color_target.format = SWAPCHAIN_FORMAT;
        color_target.blend_state = no_blend;

        SDL_GPUGraphicsPipelineCreateInfo info{};
        info.vertex_shader = vertex_shader_;
        info.fragment_shader = fragment;
        info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
        info.target_info.num_color_targets = 1;
        info.target_info.color_target_descriptions = &color_target;

        SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device_, &info);
        if (pipeline == nullptr) {
            logError(std::string("SDL_CreateGPUGraphicsPipeline failed: ") + SDL_GetError());
        }
        return pipeline;
    }

    auto Sdl3GpuShaderBackend::loadShader(const ShaderProgramSpec& spec) -> bool {
        if (device_ == nullptr) { return false; }

        if (!loadVertexShaderFor(spec)) { return false; }

        const std::filesystem::path frag_path = spec.folder / (spec.base_name + FRAGMENT_SUFFIX);

#ifdef __APPLE__
        const std::string entry = spec.base_name + "_fs";
        const char* fragment_entry = entry.c_str();
#else
        const char* fragment_entry = FRAGMENT_ENTRY;
#endif

        SDL_GPUShader* new_fragment = Sdl3Gpu::loadShaderFromFile(device_, frag_path, SHADER_FORMAT,
            fragment_entry, SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 1);
        if (new_fragment == nullptr) {
            logError("Failed to load fragment shader: " + frag_path.string() + " (" + SDL_GetError() + ")");
            return false;
        }

        SDL_GPUGraphicsPipeline* new_pipeline = buildPipeline(new_fragment);
        if (new_pipeline == nullptr) {
            SDL_ReleaseGPUShader(device_, new_fragment);
            return false;
        }

        SDL_WaitForGPUIdle(device_);
        if (pipeline_ != nullptr) { SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_); }
        if (fragment_shader_ != nullptr) { SDL_ReleaseGPUShader(device_, fragment_shader_); }

        pipeline_ = new_pipeline;
        fragment_shader_ = new_fragment;
        logInfo("Shader loaded successfully: " + spec.base_name);
        return true;
    }

    void Sdl3GpuShaderBackend::render(const ShaderUniforms& uniforms) {
        if (device_ == nullptr || pipeline_ == nullptr) { return; }

        SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
        if (cmd == nullptr) { return; }

        SDL_GPUTexture* swapchain = nullptr;
        Uint32 sw = 0;
        Uint32 sh = 0;
        if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh) || swapchain == nullptr) {
            SDL_SubmitGPUCommandBuffer(cmd);
            return;
        }

        SDL_GPUColorTargetInfo color_target{};
        color_target.texture = swapchain;
        color_target.load_op = SDL_GPU_LOADOP_CLEAR;
        color_target.store_op = SDL_GPU_STOREOP_STORE;
        color_target.clear_color = {.r = 0.0f, .g = 0.0f, .b = 0.0f, .a = 1.0f};

        SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
        if (pass != nullptr) {
            SDL_GPUViewport vp{};
            vp.x = 0.0f;
            vp.y = 0.0f;
            vp.w = static_cast<float>(sw);
            vp.h = static_cast<float>(sh);
            vp.min_depth = 0.0f;
            vp.max_depth = 1.0f;
            SDL_SetGPUViewport(pass, &vp);

            SDL_BindGPUGraphicsPipeline(pass, pipeline_);

            UniformsStd140 ubo{};
            ubo.iTime = uniforms.iTime;
            ubo.iResolutionX = uniforms.iResolutionX;
            ubo.iResolutionY = uniforms.iResolutionY;
            SDL_PushGPUFragmentUniformData(cmd, 0, &ubo, sizeof(ubo));

            SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
            SDL_EndGPURenderPass(pass);
        }

        SDL_SubmitGPUCommandBuffer(cmd);
    }

    void Sdl3GpuShaderBackend::setVSync(bool vsync) {
        vsync_ = vsync;
        if (device_ != nullptr && window_ != nullptr) {
            SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode());
            logInfo(vsync ? "VSync enabled" : "VSync disabled");
        }
    }

    auto Sdl3GpuShaderBackend::bestPresentMode() const -> SDL_GPUPresentMode {
        if (vsync_) { return SDL_GPU_PRESENTMODE_VSYNC; }
        if (device_ != nullptr && window_ != nullptr) {
            if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_IMMEDIATE)) {
                return SDL_GPU_PRESENTMODE_IMMEDIATE;
            }
            if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_MAILBOX)) {
                return SDL_GPU_PRESENTMODE_MAILBOX;
            }
        }
        return SDL_GPU_PRESENTMODE_VSYNC;
    }

    void Sdl3GpuShaderBackend::cleanup() {
        if (device_ == nullptr) { return; }

        SDL_WaitForGPUIdle(device_);

        if (pipeline_ != nullptr) {
            SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
            pipeline_ = nullptr;
        }
        if (fragment_shader_ != nullptr) {
            SDL_ReleaseGPUShader(device_, fragment_shader_);
            fragment_shader_ = nullptr;
        }
        if (vertex_shader_ != nullptr) {
            SDL_ReleaseGPUShader(device_, vertex_shader_);
            vertex_shader_ = nullptr;
        }

        if (window_ != nullptr) {
            SDL_ReleaseWindowFromGPUDevice(device_, window_);
        }
        SDL_DestroyGPUDevice(device_);
        device_ = nullptr;
        window_ = nullptr;
    }

    auto makeSdl3GpuBackend() -> std::unique_ptr<IShaderBackend> {
        return std::make_unique<Sdl3GpuShaderBackend>();
    }

}  // namespace Rendering
