// sdl_manager.cpp - Implementació del gestor SDL3
// © 2026 JailDesigner

#include "sdl_manager.hpp"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <format>
#include <iostream>

#include "core/config/postfx_config.hpp"
#include "core/defaults/game.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/window.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/coordinate_transform.hpp"
#include "core/system/notifier.hpp"
#include "project.h"

namespace {
    auto initWindowAndGpu(SDL_Window** out_window,
        Rendering::Renderer& gpu_renderer,
        int width,
        int height,
        bool fullscreen,
        int initial_vsync,
        int render_width,
        int render_height) -> bool {
        // Título estático estilo CCAE. El FPS y el estado de VSync los muestra
        // el DebugOverlay (toggle F11), no la barra de título.
        const std::string TITLE = std::format("© 2026 {} — JailDesigner",
            Project::LONG_NAME);

        SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE;
        if (fullscreen) {
            flags = static_cast<SDL_WindowFlags>(flags | SDL_WINDOW_FULLSCREEN);
        }

        SDL_Window* window = SDL_CreateWindow(TITLE.c_str(), width, height, flags);
        if (window == nullptr) {
            std::cerr << "Error creant finestra: " << SDL_GetError() << '\n';
            return false;
        }

        if (!fullscreen) {
            SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
        }

        // Inicializar el FrameRenderer (claim del window + pipeline de líneas).
        // logical_*: espacio de coordenadas del juego (fijo 1280×720).
        // render_*:  resolución física del offscreen (configurable vía YAML).
        if (!gpu_renderer.init(window,
                static_cast<float>(Defaults::Game::WIDTH),
                static_cast<float>(Defaults::Game::HEIGHT),
                static_cast<float>(render_width),
                static_cast<float>(render_height))) {
            std::cerr << "Error inicialitzant GpuFrameRenderer\n";
            SDL_DestroyWindow(window);
            return false;
        }

        gpu_renderer.setVSync(initial_vsync != 0);

        // Cargar parámetros del postpro desde el resource pack. Si el YAML falta
        // o falla, el loader devuelve los defaults built-in (bloom suave + flicker
        // sutil + background verde tenue).
        gpu_renderer.setPostFx(Config::PostFx::load("config/postfx.yaml"));

        *out_window = window;
        return true;
    }
}  // namespace

SDLManager::SDLManager(int width, int height, bool fullscreen, Config::EngineConfig& cfg, std::function<void()> on_persist)
    : cfg_(&cfg),
      on_persist_(std::move(on_persist)),
      current_width_(width),
      current_height_(height),
      is_fullscreen_(fullscreen),
      zoom_factor_(static_cast<float>(width) / Defaults::Window::WIDTH),
      windowed_width_(width),
      windowed_height_(height) {
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        std::cerr << "Error inicialitzant SDL3: " << SDL_GetError() << '\n';
        return;
    }

    calculateMaxWindowSize();

    // Validar la resolució de render del config: si no és un preset 16:9
    // conegut, fer fallback a 1280×720 i avisar. Això protegeix d'edicions
    // manuals invàlides al YAML.
    int effective_render_w = cfg_->rendering.render_width;
    int effective_render_h = cfg_->rendering.render_height;
    if (!Defaults::Rendering::isValidRenderResolution(effective_render_w, effective_render_h)) {
        std::cerr << "Resolució de render invàlida (" << effective_render_w << "x"
                  << effective_render_h << "), fent fallback a "
                  << Defaults::Rendering::RENDER_WIDTH_DEFAULT << "x"
                  << Defaults::Rendering::RENDER_HEIGHT_DEFAULT << '\n';
        effective_render_w = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
        effective_render_h = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
        cfg_->rendering.render_width = effective_render_w;
        cfg_->rendering.render_height = effective_render_h;
    }

    if (!initWindowAndGpu(&finestra_,
            gpu_renderer_,
            current_width_,
            current_height_,
            is_fullscreen_,
            cfg_->rendering.vsync,
            effective_render_w,
            effective_render_h)) {
        SDL_Quit();
        return;
    }

    // Aplica l'estat inicial d'antialias des de la config (per defecte ON).
    gpu_renderer_.setAntialias(cfg_->rendering.antialias != 0);

    updateViewport();

    // En fullscreen: forzar ocultació permanent del cursor.
    if (is_fullscreen_) {
        Mouse::setForceHidden(true);
    }

    std::cout << "SDL3 inicialitzat: " << current_width_ << "x" << current_height_
              << " (logic: " << Defaults::Game::WIDTH << "x"
              << Defaults::Game::HEIGHT
              << ", render: " << effective_render_w << "x" << effective_render_h
              << ")";
    if (is_fullscreen_) {
        std::cout << " [FULLSCREEN]";
    }
    std::cout << '\n';
}

SDLManager::~SDLManager() {
    gpu_renderer_.destroy();

    if (finestra_ != nullptr) {
        SDL_DestroyWindow(finestra_);
        finestra_ = nullptr;
    }

    SDL_Quit();
    std::cout << "SDL3 netejat correctament" << '\n';
}

void SDLManager::calculateMaxWindowSize() {
    SDL_DisplayID display = SDL_GetPrimaryDisplay();
    const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);

    if (mode != nullptr) {
        // Deixar marge de 100px para decoracions de l'OS
        max_width_ = mode->w - 100;
        max_height_ = mode->h - 100;
        std::cout << "Display detectat: " << mode->w << "x" << mode->h
                  << " (max finestra: " << max_width_ << "x" << max_height_ << ")"
                  << '\n';
    } else {
        max_width_ = 1920;
        max_height_ = 1080;
        std::cerr << "No s'ha pogut detectar el display, usant fallback: "
                  << max_width_ << "x" << max_height_ << '\n';
    }

    calculateMaxZoom();
}

void SDLManager::calculateMaxZoom() {
    float max_zoom_width = static_cast<float>(max_width_) / Defaults::Window::WIDTH;
    float max_zoom_height = static_cast<float>(max_height_) / Defaults::Window::HEIGHT;
    float max_zoom_unrounded = std::min(max_zoom_width, max_zoom_height);
    max_zoom_ = std::floor(max_zoom_unrounded / Defaults::Window::ZOOM_INCREMENT) * Defaults::Window::ZOOM_INCREMENT;
    max_zoom_ = std::max(max_zoom_, Defaults::Window::MIN_ZOOM);

    std::cout << "Max zoom: " << max_zoom_ << "x (display: "
              << max_width_ << "x" << max_height_ << ")" << '\n';
}

void SDLManager::applyZoom(float new_zoom) {
    new_zoom = std::max(Defaults::Window::MIN_ZOOM,
        std::min(new_zoom, max_zoom_));
    new_zoom = std::round(new_zoom / Defaults::Window::ZOOM_INCREMENT) * Defaults::Window::ZOOM_INCREMENT;

    if (std::abs(new_zoom - zoom_factor_) < 0.01F) {
        return;
    }

    zoom_factor_ = new_zoom;

    int new_width = static_cast<int>(std::round(Defaults::Window::WIDTH * zoom_factor_));
    int new_height = static_cast<int>(std::round(Defaults::Window::HEIGHT * zoom_factor_));

    applyWindowSize(new_width, new_height);
    updateViewport();

    windowed_width_ = new_width;
    windowed_height_ = new_height;

    cfg_->window.width = new_width;
    cfg_->window.height = new_height;
    cfg_->window.zoom_factor = zoom_factor_;

    std::cout << "Zoom: " << zoom_factor_ << "x ("
              << new_width << "x" << new_height << ")" << '\n';
}

void SDLManager::updateViewport() {
    // Càlcul de letterbox: el joc es renderitza a 1280×720 lògics, però la
    // swapchain té la mida física de la finestra. Apliquem un viewport
    // centrat amb aspect-fit (omple un eix, lletrabox a l'altre).
    //
    // IMPORTANT: l'escala del viewport es deriva de la mida física actual,
    // NO del zoom_factor_. El zoom_factor_ només dimensiona la finestra en
    // mode windowed (F1/F2). Si l'enllacéssim, en fullscreen el viewport
    // quedaria capat per max_zoom_ (display-100px) i no ompliria la pantalla.
    float scale_w = static_cast<float>(current_width_) / Defaults::Game::WIDTH;
    float scale_h = static_cast<float>(current_height_) / Defaults::Game::HEIGHT;
    float scale = std::min(scale_w, scale_h);
    int scaled_width = static_cast<int>(std::round(Defaults::Game::WIDTH * scale));
    int scaled_height = static_cast<int>(std::round(Defaults::Game::HEIGHT * scale));

    int offset_x = (current_width_ - scaled_width) / 2;
    int offset_y = (current_height_ - scaled_height) / 2;
    offset_x = std::max(offset_x, 0);
    offset_y = std::max(offset_y, 0);

    gpu_renderer_.setViewport(static_cast<float>(offset_x),
        static_cast<float>(offset_y),
        static_cast<float>(scaled_width),
        static_cast<float>(scaled_height));

    std::cout << "Viewport: " << scaled_width << "x" << scaled_height
              << " @ (" << offset_x << "," << offset_y << ") [scale=" << scale << "]"
              << '\n';
}

void SDLManager::updateRenderingContext() const {
    Rendering::g_current_scale_factor = zoom_factor_;
}

void SDLManager::increaseWindowSize() {
    if (is_fullscreen_) {
        return;
    }
    float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
    applyZoom(new_zoom);
    if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
        notifier->notifyInfo(localeSubstitute(
            Locale::get().text("notification.zoom"),
            "{z}",
            std::format("{:.1f}", zoom_factor_)));
    }
}

void SDLManager::decreaseWindowSize() {
    if (is_fullscreen_) {
        return;
    }
    float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
    applyZoom(new_zoom);
    if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
        notifier->notifyInfo(localeSubstitute(
            Locale::get().text("notification.zoom"),
            "{z}",
            std::format("{:.1f}", zoom_factor_)));
    }
}

void SDLManager::applyWindowSize(int new_width, int new_height) {
    int old_x;
    int old_y;
    SDL_GetWindowPosition(finestra_, &old_x, &old_y);

    int old_width = current_width_;
    int old_height = current_height_;

    SDL_SetWindowSize(finestra_, new_width, new_height);
    current_width_ = new_width;
    current_height_ = new_height;

    int delta_width = old_width - new_width;
    int delta_height = old_height - new_height;
    int new_x = old_x + (delta_width / 2);
    int new_y = old_y + (delta_height / 2);

    constexpr int TITLEBAR_HEIGHT = 35;
    new_x = std::max(new_x, 0);
    new_y = std::max(new_y, TITLEBAR_HEIGHT);

    SDL_SetWindowPosition(finestra_, new_x, new_y);
    updateViewport();
}

void SDLManager::toggleFullscreen() {
    if (!is_fullscreen_) {
        windowed_width_ = current_width_;
        windowed_height_ = current_height_;
        is_fullscreen_ = true;
        // SDL3: cal seleccionar explícitament el mode "borderless desktop"
        // (mode=nullptr) abans d'activar el fullscreen. Sense això, el
        // comportament depèn del mode que tingués la finestra anteriorment.
        SDL_SetWindowFullscreenMode(finestra_, nullptr);
        SDL_SetWindowFullscreen(finestra_, true);
    } else {
        is_fullscreen_ = false;
        SDL_SetWindowFullscreen(finestra_, false);
        applyWindowSize(windowed_width_, windowed_height_);
    }

    cfg_->window.fullscreen = is_fullscreen_;
    Mouse::setForceHidden(is_fullscreen_);

    if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
        notifier->notifyInfo(Locale::get().text(is_fullscreen_ ? "notification.fullscreen_on" : "notification.fullscreen_off"));
    }
}

auto SDLManager::handleWindowEvent(const SDL_Event& event) -> bool {
    if (event.type == SDL_EVENT_WINDOW_RESIZED) {
        SDL_GetWindowSize(finestra_, &current_width_, &current_height_);

        // En fullscreen el zoom_factor_ no participa del viewport (aspect-fit
        // sobre la mida física), així que el preservem amb el valor de
        // windowed per no perdre'l en tornar a windowed.
        if (!is_fullscreen_) {
            float new_zoom = static_cast<float>(current_width_) / Defaults::Window::WIDTH;
            zoom_factor_ = std::max(Defaults::Window::MIN_ZOOM,
                std::min(new_zoom, max_zoom_));
            windowed_width_ = current_width_;
            windowed_height_ = current_height_;
        }

        updateViewport();

        std::cout << "Finestra redimensionada: " << current_width_
                  << "x" << current_height_ << " (zoom ≈" << zoom_factor_ << "x)"
                  << '\n';
        return true;
    }
    return false;
}

auto SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) -> bool {
    // El fondo lo dibuja ahora el shader de postpro (background pulse). El
    // offscreen se limpia en negro dentro de beginFrame. Los argumentos r/g/b
    // se mantienen por compatibilidad de API.
    (void)r;
    (void)g;
    (void)b;
    // beginFrame devuelve false si la swapchain no está disponible (ventana
    // minimizada, por ejemplo). Propagamos el bool al caller para que pueda
    // saltarse draw+present ese frame; si no, los vértices se acumulan en
    // el batch interno sin que nadie los consuma.
    return gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
}

void SDLManager::present() {
    gpu_renderer_.endFrame();
}

void SDLManager::toggleVSync() {
    cfg_->rendering.vsync = (cfg_->rendering.vsync == 1) ? 0 : 1;
    gpu_renderer_.setVSync(cfg_->rendering.vsync != 0);
    if (on_persist_) {
        on_persist_();
    }
    if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
        notifier->notifyInfo(Locale::get().text(cfg_->rendering.vsync != 0 ? "notification.vsync_on" : "notification.vsync_off"));
    }
}

void SDLManager::toggleAntialias() {
    cfg_->rendering.antialias = (cfg_->rendering.antialias == 1) ? 0 : 1;
    gpu_renderer_.setAntialias(cfg_->rendering.antialias != 0);
    // No persistim: l'AA és toggleable runtime però el seu estat no es
    // guarda al YAML de moment (decisió volgudament conservadora).
    if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
        notifier->notifyInfo(Locale::get().text(cfg_->rendering.antialias != 0 ? "notification.antialias_on" : "notification.antialias_off"));
    }
}

void SDLManager::setRenderResolution(int w, int h) {
    if (!Defaults::Rendering::isValidRenderResolution(w, h)) {
        std::cerr << "[SDLManager] Resolucio no valida (" << w << "x" << h
                  << "), ignorant.\n";
        return;
    }
    if (w == cfg_->rendering.render_width && h == cfg_->rendering.render_height) {
        return;  // ja era l'actual
    }
    if (!gpu_renderer_.resizeRenderTarget(static_cast<float>(w), static_cast<float>(h))) {
        std::cerr << "[SDLManager] resizeRenderTarget ha fallat.\n";
        return;
    }
    cfg_->rendering.render_width = w;
    cfg_->rendering.render_height = h;
    if (on_persist_) {
        on_persist_();
    }
}

void SDLManager::togglePostFx() {
    const bool NEW_STATE = !gpu_renderer_.isPostFxEnabled();
    gpu_renderer_.setPostFxEnabled(NEW_STATE);
    // No persistim: el toggle és per A/B testing visual, l'estat per defecte
    // del joc continua sent "postfx ON" segons defaults/YAML.
    if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
        notifier->notifyInfo(Locale::get().text(NEW_STATE ? "notification.postfx_on" : "notification.postfx_off"));
    }
}
