// gpu_frame_renderer.cpp - Implementación del FrameRenderer con offscreen + postpro

#include "core/rendering/gpu/gpu_frame_renderer.hpp"

#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>

#include <cmath>
#include <cstdint>
#include <cstring>
#include <iostream>

namespace Rendering::GPU {

GpuFrameRenderer::~GpuFrameRenderer() { destroy(); }

auto GpuFrameRenderer::init(SDL_Window* window, float logical_w, float logical_h) -> bool {
    logical_w_ = logical_w;
    logical_h_ = logical_h;

    if (!device_.init(window)) {
        return false;
    }
    // Pipeline de líneas: escribe sobre el offscreen (formato fijo).
    if (!line_pipeline_.init(device_, offscreen_format_)) {
        device_.destroy();
        return false;
    }
    // Pipeline de postpro: escribe sobre swapchain (formato del swapchain).
    if (!postfx_pipeline_.init(device_, device_.swapchainFormat())) {
        line_pipeline_.destroy();
        device_.destroy();
        return false;
    }
    if (!createOffscreen()) {
        postfx_pipeline_.destroy();
        line_pipeline_.destroy();
        device_.destroy();
        return false;
    }
    return true;
}

auto GpuFrameRenderer::createOffscreen() -> bool {
    SDL_GPUDevice* dev = device_.get();
    if (dev == nullptr) {
        return false;
    }

    // Textura offscreen del tamaño lógico del juego, COLOR_TARGET + SAMPLER.
    SDL_GPUTextureCreateInfo tex_info{};
    tex_info.type = SDL_GPU_TEXTURETYPE_2D;
    tex_info.format = offscreen_format_;
    tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
    tex_info.width = static_cast<uint32_t>(logical_w_);
    tex_info.height = static_cast<uint32_t>(logical_h_);
    tex_info.layer_count_or_depth = 1;
    tex_info.num_levels = 1;
    tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
    offscreen_texture_ = SDL_CreateGPUTexture(dev, &tex_info);
    if (offscreen_texture_ == nullptr) {
        std::cerr << "[GpuFrameRenderer] SDL_CreateGPUTexture (offscreen): "
                  << SDL_GetError() << '\n';
        return false;
    }

    // Sampler lineal con clamp-to-edge (evita sangrado en los bordes del bloom).
    SDL_GPUSamplerCreateInfo sampler_info{};
    sampler_info.min_filter = SDL_GPU_FILTER_LINEAR;
    sampler_info.mag_filter = SDL_GPU_FILTER_LINEAR;
    sampler_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR;
    sampler_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
    sampler_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
    sampler_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
    linear_sampler_ = SDL_CreateGPUSampler(dev, &sampler_info);
    if (linear_sampler_ == nullptr) {
        std::cerr << "[GpuFrameRenderer] SDL_CreateGPUSampler: "
                  << SDL_GetError() << '\n';
        return false;
    }
    return true;
}

void GpuFrameRenderer::destroyOffscreen() {
    SDL_GPUDevice* dev = device_.get();
    if (dev == nullptr) {
        offscreen_texture_ = nullptr;
        linear_sampler_ = nullptr;
        return;
    }
    if (offscreen_texture_ != nullptr) {
        SDL_ReleaseGPUTexture(dev, offscreen_texture_);
        offscreen_texture_ = nullptr;
    }
    if (linear_sampler_ != nullptr) {
        SDL_ReleaseGPUSampler(dev, linear_sampler_);
        linear_sampler_ = nullptr;
    }
}

void GpuFrameRenderer::destroy() {
    destroyOffscreen();
    postfx_pipeline_.destroy();
    line_pipeline_.destroy();
    device_.destroy();
    vertices_.clear();
    indices_.clear();
}

auto GpuFrameRenderer::beginFrame(float clear_r, float clear_g, float clear_b) -> bool {
    // Los clear_* se ignoran: el fondo lo pinta el postpro. Mantenemos la
    // firma para no romper el SDLManager.
    (void)clear_r;
    (void)clear_g;
    (void)clear_b;

    SDL_GPUDevice* dev = device_.get();
    if (dev == nullptr) {
        return false;
    }

    cmd_buffer_ = SDL_AcquireGPUCommandBuffer(dev);
    if (cmd_buffer_ == nullptr) {
        std::cerr << "[GpuFrameRenderer] SDL_AcquireGPUCommandBuffer: " << SDL_GetError() << '\n';
        return false;
    }

    if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(),
                                               &swapchain_texture_, nullptr, nullptr)) {
        std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
        SDL_SubmitGPUCommandBuffer(cmd_buffer_);
        cmd_buffer_ = nullptr;
        return false;
    }

    if (swapchain_texture_ == nullptr) {
        // Ventana minimizada o swapchain no disponible: solo submit y salir.
        SDL_SubmitGPUCommandBuffer(cmd_buffer_);
        cmd_buffer_ = nullptr;
        return false;
    }

    // Abrir render pass sobre OFFSCREEN con clear a negro.
    SDL_GPUColorTargetInfo color_target{};
    color_target.texture = offscreen_texture_;
    color_target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
    color_target.load_op = SDL_GPU_LOADOP_CLEAR;
    color_target.store_op = SDL_GPU_STOREOP_STORE;
    color_target.cycle = false;

    render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
    if (render_pass_ == nullptr) {
        std::cerr << "[GpuFrameRenderer] SDL_BeginGPURenderPass (offscreen): "
                  << SDL_GetError() << '\n';
        SDL_SubmitGPUCommandBuffer(cmd_buffer_);
        cmd_buffer_ = nullptr;
        return false;
    }
    // Sin SetGPUViewport: el offscreen se llena entero a tamaño lógico.

    vertices_.clear();
    indices_.clear();
    return true;
}

void GpuFrameRenderer::setViewport(float x, float y, float w, float h) {
    viewport_x_ = x;
    viewport_y_ = y;
    viewport_w_ = w;
    viewport_h_ = h;
    // El viewport solo se aplica en el pase final (composite). Si estamos
    // ya dentro del composite, lo aplicaríamos inmediatamente, pero la API
    // está pensada para llamarse antes de endFrame/al cambiar de ventana.
}

void GpuFrameRenderer::setVSync(bool enabled) {
    SDL_GPUDevice* dev = device_.get();
    if (dev == nullptr || device_.window() == nullptr) {
        return;
    }
    const SDL_GPUPresentMode MODE = enabled
        ? SDL_GPU_PRESENTMODE_VSYNC
        : SDL_GPU_PRESENTMODE_IMMEDIATE;
    if (!SDL_SetGPUSwapchainParameters(dev, device_.window(),
                                       SDL_GPU_SWAPCHAINCOMPOSITION_SDR, MODE)) {
        std::cerr << "[GpuFrameRenderer] SDL_SetGPUSwapchainParameters: " << SDL_GetError() << '\n';
    }
}

void GpuFrameRenderer::applyFinalViewport() {
    if (render_pass_ == nullptr) {
        return;
    }
    if (viewport_w_ <= 0.0F || viewport_h_ <= 0.0F) {
        return;
    }
    SDL_GPUViewport vp{};
    vp.x = viewport_x_;
    vp.y = viewport_y_;
    vp.w = viewport_w_;
    vp.h = viewport_h_;
    vp.min_depth = 0.0F;
    vp.max_depth = 1.0F;
    SDL_SetGPUViewport(render_pass_, &vp);
}

void GpuFrameRenderer::pushLine(float x1, float y1, float x2, float y2, float thickness,
                                float r, float g, float b, float a) {
    const float DX = x2 - x1;
    const float DY = y2 - y1;
    const float LEN = std::sqrt((DX * DX) + (DY * DY));
    if (LEN < 1e-6F) {
        return;
    }

    const float HALF = thickness * 0.5F;
    const float NX = -DY / LEN * HALF;
    const float NY = DX / LEN * HALF;

    const auto BASE_INDEX = static_cast<uint16_t>(vertices_.size());

    vertices_.push_back({x1 + NX, y1 + NY, r, g, b, a});
    vertices_.push_back({x1 - NX, y1 - NY, r, g, b, a});
    vertices_.push_back({x2 + NX, y2 + NY, r, g, b, a});
    vertices_.push_back({x2 - NX, y2 - NY, r, g, b, a});

    indices_.push_back(BASE_INDEX + 0);
    indices_.push_back(BASE_INDEX + 1);
    indices_.push_back(BASE_INDEX + 2);
    indices_.push_back(BASE_INDEX + 1);
    indices_.push_back(BASE_INDEX + 3);
    indices_.push_back(BASE_INDEX + 2);
}

void GpuFrameRenderer::flushBatch() {
    if (vertices_.empty() || indices_.empty()) {
        return;
    }

    SDL_GPUDevice* dev = device_.get();

    const uint32_t VBO_SIZE = static_cast<uint32_t>(vertices_.size() * sizeof(LineVertex));
    const uint32_t IBO_SIZE = static_cast<uint32_t>(indices_.size() * sizeof(uint16_t));

    SDL_GPUBufferCreateInfo vbo_info{};
    vbo_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
    vbo_info.size = VBO_SIZE;
    SDL_GPUBuffer* vbo = SDL_CreateGPUBuffer(dev, &vbo_info);

    SDL_GPUBufferCreateInfo ibo_info{};
    ibo_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
    ibo_info.size = IBO_SIZE;
    SDL_GPUBuffer* ibo = SDL_CreateGPUBuffer(dev, &ibo_info);

    SDL_GPUTransferBufferCreateInfo tbo_info{};
    tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
    tbo_info.size = VBO_SIZE + IBO_SIZE;
    SDL_GPUTransferBuffer* tbo = SDL_CreateGPUTransferBuffer(dev, &tbo_info);

    auto* mapped = static_cast<uint8_t*>(SDL_MapGPUTransferBuffer(dev, tbo, false));
    std::memcpy(mapped, vertices_.data(), VBO_SIZE);
    std::memcpy(mapped + VBO_SIZE, indices_.data(), IBO_SIZE);
    SDL_UnmapGPUTransferBuffer(dev, tbo);

    // Copy pass FUERA del render pass.
    SDL_EndGPURenderPass(render_pass_);
    render_pass_ = nullptr;

    SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_);
    SDL_GPUTransferBufferLocation vbo_src{.transfer_buffer = tbo, .offset = 0};
    SDL_GPUBufferRegion vbo_dst{.buffer = vbo, .offset = 0, .size = VBO_SIZE};
    SDL_UploadToGPUBuffer(copy_pass, &vbo_src, &vbo_dst, false);
    SDL_GPUTransferBufferLocation ibo_src{.transfer_buffer = tbo, .offset = VBO_SIZE};
    SDL_GPUBufferRegion ibo_dst{.buffer = ibo, .offset = 0, .size = IBO_SIZE};
    SDL_UploadToGPUBuffer(copy_pass, &ibo_src, &ibo_dst, false);
    SDL_EndGPUCopyPass(copy_pass);

    // Reabrir render pass sobre OFFSCREEN (load_op=LOAD para preservar el clear).
    SDL_GPUColorTargetInfo color_target{};
    color_target.texture = offscreen_texture_;
    color_target.load_op = SDL_GPU_LOADOP_LOAD;
    color_target.store_op = SDL_GPU_STOREOP_STORE;
    render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);

    SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());

    // UBO de líneas usa el tamaño lógico (también del offscreen).
    LineUniforms ubo{.viewport_width = logical_w_,
                     .viewport_height = logical_h_,
                     .padding_0 = 0.0F,
                     .padding_1 = 0.0F};
    SDL_PushGPUVertexUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));

    SDL_GPUBufferBinding vbo_binding{.buffer = vbo, .offset = 0};
    SDL_BindGPUVertexBuffers(render_pass_, 0, &vbo_binding, 1);

    SDL_GPUBufferBinding ibo_binding{.buffer = ibo, .offset = 0};
    SDL_BindGPUIndexBuffer(render_pass_, &ibo_binding, SDL_GPU_INDEXELEMENTSIZE_16BIT);

    SDL_DrawGPUIndexedPrimitives(render_pass_,
                                 static_cast<uint32_t>(indices_.size()),
                                 1, 0, 0, 0);

    SDL_ReleaseGPUBuffer(dev, vbo);
    SDL_ReleaseGPUBuffer(dev, ibo);
    SDL_ReleaseGPUTransferBuffer(dev, tbo);
}

void GpuFrameRenderer::compositePass() {
    // Cierra el render pass actual (sobre offscreen).
    if (render_pass_ != nullptr) {
        SDL_EndGPURenderPass(render_pass_);
        render_pass_ = nullptr;
    }

    // Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el
    // letterbox del viewport físico).
    SDL_GPUColorTargetInfo target{};
    target.texture = swapchain_texture_;
    target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
    target.load_op = SDL_GPU_LOADOP_CLEAR;
    target.store_op = SDL_GPU_STOREOP_STORE;
    target.cycle = false;
    render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &target, 1, nullptr);
    if (render_pass_ == nullptr) {
        std::cerr << "[GpuFrameRenderer] BeginRenderPass (composite): "
                  << SDL_GetError() << '\n';
        return;
    }
    applyFinalViewport();

    SDL_BindGPUGraphicsPipeline(render_pass_, postfx_pipeline_.get());

    // Bind del sampler (escena offscreen) en slot 0 del fragment shader.
    SDL_GPUTextureSamplerBinding sampler_binding{};
    sampler_binding.texture = offscreen_texture_;
    sampler_binding.sampler = linear_sampler_;
    SDL_BindGPUFragmentSamplers(render_pass_, 0, &sampler_binding, 1);

    // Uniforms del postpro. Si una sección está desactivada, anulamos sus
    // contribuciones (intensidad / amplitud / max=min) en lugar de tener
    // un branch en el shader.
    const float BLOOM_INTENSITY = postfx_params_.bloom_enabled
        ? postfx_params_.bloom_intensity : 0.0F;
    const float FLICKER_AMPLITUDE = postfx_params_.flicker_enabled
        ? postfx_params_.flicker_amplitude : 0.0F;
    const float BG_MIN_R = postfx_params_.background_enabled ? postfx_params_.background_min_r : 0.0F;
    const float BG_MIN_G = postfx_params_.background_enabled ? postfx_params_.background_min_g : 0.0F;
    const float BG_MIN_B = postfx_params_.background_enabled ? postfx_params_.background_min_b : 0.0F;
    const float BG_MAX_R = postfx_params_.background_enabled ? postfx_params_.background_max_r : 0.0F;
    const float BG_MAX_G = postfx_params_.background_enabled ? postfx_params_.background_max_g : 0.0F;
    const float BG_MAX_B = postfx_params_.background_enabled ? postfx_params_.background_max_b : 0.0F;

    // Tiempo en segundos desde el inicio de SDL (wall-clock real, robusto a FPS variables).
    const float TIME_SECONDS = static_cast<float>(SDL_GetTicks()) / 1000.0F;

    PostFxUniforms ubo{};
    ubo.time = TIME_SECONDS;
    ubo.bloom_intensity = BLOOM_INTENSITY;
    ubo.bloom_threshold = postfx_params_.bloom_threshold;
    ubo.bloom_radius_px = postfx_params_.bloom_radius_px;
    ubo.flicker_amplitude = FLICKER_AMPLITUDE;
    ubo.flicker_frequency_hz = postfx_params_.flicker_frequency_hz;
    ubo.background_pulse_freq_hz = postfx_params_.background_pulse_freq_hz;
    ubo.pad_a_ = 0.0F;
    ubo.background_min_r = BG_MIN_R;
    ubo.background_min_g = BG_MIN_G;
    ubo.background_min_b = BG_MIN_B;
    ubo.background_min_a = 1.0F;
    ubo.background_max_r = BG_MAX_R;
    ubo.background_max_g = BG_MAX_G;
    ubo.background_max_b = BG_MAX_B;
    ubo.background_max_a = 1.0F;
    ubo.texel_size_x = 1.0F / logical_w_;
    ubo.texel_size_y = 1.0F / logical_h_;
    ubo.pad_b_ = 0.0F;
    ubo.pad_c_ = 0.0F;

    SDL_PushGPUFragmentUniformData(cmd_buffer_, 0, &ubo, sizeof(ubo));

    // Fullscreen triangle: 3 vértices generados en el shader, sin VBO.
    SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
}

void GpuFrameRenderer::endFrame() {
    if (cmd_buffer_ == nullptr) {
        return;
    }
    flushBatch();
    compositePass();
    if (render_pass_ != nullptr) {
        SDL_EndGPURenderPass(render_pass_);
        render_pass_ = nullptr;
    }
    SDL_SubmitGPUCommandBuffer(cmd_buffer_);
    cmd_buffer_ = nullptr;
    swapchain_texture_ = nullptr;
}

}  // namespace Rendering::GPU
