#include "screen.hpp"

#include <SDL3/SDL.h>  // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WINDOW_OPENGL, SDL_WindowFlags

#include <algorithm>  // Para min, max
#include <memory>     // Para allocator, shared_ptr, unique_ptr, __shared_ptr_access, make_shared, make_unique
#include <string>     // Para basic_string, operator+, char_traits, to_string, string
#include <vector>     // Para vector

#include "asset.hpp"                           // Para Asset
#include "mouse.hpp"                           // Para updateCursorVisibility
#include "options.hpp"                         // Para Video, video, Window, window
#include "param.hpp"                           // Para Param, param, ParamGame, ParamDebug
#include "rendering/opengl/opengl_shader.hpp"  // Para OpenGLShader
#include "shader_backend.hpp"                  // Para ShaderBackend
#include "text.hpp"                            // Para Text
#include "texture.hpp"                         // Para Texture
#include "ui/logger.hpp"                       // Para info
#include "ui/notifier.hpp"                     // Para Notifier
#include "ui/service_menu.hpp"                 // Para ServiceMenu

// Singleton
Screen* Screen::instance = nullptr;

// Inicializa la instancia única del singleton
void Screen::init() { Screen::instance = new Screen(); }

// Libera la instancia
void Screen::destroy() { delete Screen::instance; }

// Obtiene la instancia
auto Screen::get() -> Screen* { return Screen::instance; }

// Constructor
Screen::Screen()
    : window_(nullptr),
      renderer_(nullptr),
      game_canvas_(nullptr),
      service_menu_(nullptr),
      notifier_(nullptr),
      src_rect_(SDL_FRect{0, 0, param.game.width, param.game.height}),
      dst_rect_(SDL_FRect{0, 0, param.game.width, param.game.height}) {
    // Arranca SDL VIDEO, crea la ventana y el renderizador
    initSDLVideo();

    // Crea la textura de destino
    game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
    SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST);

    // Crea el objeto de texto
    createText();

#ifdef _DEBUG
    debug_info_.text = text_;
    setDebugInfoEnabled(true);
#endif

    // Renderizar una vez la textura vacía para que tenga contenido válido antes de inicializar los shaders (evita pantalla negra)
    SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);

    // Limpiar renderer
    SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
    SDL_RenderClear(renderer_);
    SDL_RenderPresent(renderer_);

    // Ahora sí inicializar los shaders
    initShaders();
}

// Destructor
Screen::~Screen() {
    SDL_DestroyTexture(game_canvas_);
    SDL_DestroyRenderer(renderer_);
    SDL_DestroyWindow(window_);
}

// Limpia la pantalla
void Screen::clean(Color color) {
    SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
    SDL_RenderClear(renderer_);
}

// Prepara para empezar a dibujar en la textura de juego
void Screen::start() { SDL_SetRenderTarget(renderer_, game_canvas_); }

// Vuelca el contenido del renderizador en pantalla
void Screen::render() {
    fps_.increment();
    renderOverlays();  // Renderiza todos los overlays y efectos
    renderPresent();   // Renderiza el contenido del game_canvas_
}

// Vuelca el contenido del renderizador en pantalla exceptuando ciertas partes
void Screen::coreRender() {
    /*fps_.increment();
#ifdef _DEBUG
    renderInfo();
#endif*/
    renderPresent();  // Renderiza el contenido del game_canvas_
}

// Renderiza el contenido del game_canvas_
void Screen::renderPresent() {
    SDL_SetRenderTarget(renderer_, nullptr);
    clean();

    if (Options::video.shaders && shader_backend_) {
        shader_backend_->render();
    } else {
        SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
        SDL_RenderPresent(renderer_);
    }
}

// Establece el modo de video
void Screen::setFullscreenMode() {
    SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
}

// Camibia entre pantalla completa y ventana
void Screen::toggleFullscreen() {
    Options::video.fullscreen = !Options::video.fullscreen;
    setFullscreenMode();
}

// Cambia el tamaño de la ventana
void Screen::setWindowZoom(int zoom) {
    Options::window.zoom = zoom;
    adjustWindowSize();
}

// Reduce el tamaño de la ventana
auto Screen::decWindowSize() -> bool {
    if (!Options::video.fullscreen) {
        const int PREVIOUS_ZOOM = Options::window.zoom;
        --Options::window.zoom;
        Options::window.zoom = std::max(Options::window.zoom, 1);

        if (Options::window.zoom != PREVIOUS_ZOOM) {
            adjustWindowSize();
            return true;
        }
    }

    return false;
}

// Aumenta el tamaño de la ventana
auto Screen::incWindowSize() -> bool {
    if (!Options::video.fullscreen) {
        const int PREVIOUS_ZOOM = Options::window.zoom;
        ++Options::window.zoom;
        Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);

        if (Options::window.zoom != PREVIOUS_ZOOM) {
            adjustWindowSize();
            return true;
        }
    }

    return false;
}

// Recibe deltaTime de las secciones y actualiza la lógica
void Screen::update(float delta_time) {
    fps_.calculate(SDL_GetTicks());
    shake_effect_.update(src_rect_, dst_rect_, delta_time);
    flash_effect_.update(delta_time);
    if (service_menu_ != nullptr) {
        service_menu_->update(delta_time);
    }
    if (notifier_ != nullptr) {
        notifier_->update(delta_time);
    }
    Mouse::updateCursorVisibility();
}

// Actualiza los elementos mínimos
void Screen::coreUpdate() {
    fps_.calculate(SDL_GetTicks());
    Mouse::updateCursorVisibility();
}

// Actualiza y dibuja el efecto de flash en la pantalla
void Screen::renderFlash() {
    if (flash_effect_.isRendarable()) {
        SDL_SetRenderDrawColor(renderer_, flash_effect_.color.r, flash_effect_.color.g, flash_effect_.color.b, 0xFF);
        SDL_RenderClear(renderer_);
    }
}

// Aplica el efecto de agitar la pantalla
void Screen::renderShake() {
    if (shake_effect_.enabled) {
        // Guarda el renderizador actual para dejarlo despues como estaba
        auto* current_target = SDL_GetRenderTarget(renderer_);

        // Crea una textura temporal
        auto* temp_texture = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);

        // Vuelca game_canvas_ a la textura temporal
        SDL_SetRenderTarget(renderer_, temp_texture);
        SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);

        // Vuelca textura temporal a game_canvas_
        SDL_SetRenderTarget(renderer_, game_canvas_);
        SDL_RenderTexture(renderer_, temp_texture, &src_rect_, &dst_rect_);

        // Elimina la textura temporal
        SDL_DestroyTexture(temp_texture);

        // Restaura el renderizador de destino original
        SDL_SetRenderTarget(renderer_, current_target);
    }
}
#ifdef _DEBUG
// Muestra información por pantalla
void Screen::renderInfo() {
    if (debug_info_.show) {
        // Resolution
        debug_info_.text->writeDX(Text::COLOR | Text::STROKE, param.game.width - debug_info_.text->length(Options::video.info) - 2, 1, Options::video.info, 1, param.debug.color, 1, param.debug.color.DARKEN(150));

        // FPS
        const std::string FPS_TEXT = std::to_string(fps_.last_value) + " FPS";
        debug_info_.text->writeDX(Text::COLOR | Text::STROKE, param.game.width - debug_info_.text->length(FPS_TEXT) - 2, 1 + debug_info_.text->getCharacterSize(), FPS_TEXT, 1, param.debug.color, 1, param.debug.color.DARKEN(150));
#ifdef RECORDING
        // RECORDING
        debug_info_.text->writeDX(Text::COLOR | Text::STROKE, param.game.width - debug_info_.text->length("RECORDING"), 2 * (1 + debug_info_.text->getCharacterSize()), "RECORDING", 1, param.debug.color, 1, param.debug.color.DARKEN(150));
#endif
    }
}
#endif
// Carga el contenido de los archivos GLSL
void Screen::loadShaders() {
    if (vertex_shader_source_.empty()) {
        // Detectar si necesitamos OpenGL ES (Raspberry Pi)
        // Intentar cargar versión ES primero si existe
        std::string vertex_file = "crtpi_vertex_es.glsl";
        auto data = Asset::get()->loadData(vertex_file);

        if (data.empty()) {
            // Si no existe versión ES, usar versión Desktop
            vertex_file = "crtpi_vertex.glsl";
            data = Asset::get()->loadData(vertex_file);
            // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Usando shaders OpenGL Desktop 3.3");
            Logger::info("Usando shaders OpenGL Desktop 3.3");
        } else {
            // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Usando shaders OpenGL ES 3.0 (Raspberry Pi)");
            Logger::info("Usando shaders OpenGL ES 3.0 (Raspberry Pi)");
        }

        if (!data.empty()) {
            vertex_shader_source_ = std::string(data.begin(), data.end());
        }
    }
    if (fragment_shader_source_.empty()) {
        // Intentar cargar versión ES primero si existe
        std::string fragment_file = "crtpi_fragment_es.glsl";
        auto data = Asset::get()->loadData(fragment_file);

        if (data.empty()) {
            // Si no existe versión ES, usar versión Desktop
            fragment_file = "crtpi_fragment.glsl";
            data = Asset::get()->loadData(fragment_file);
        }

        if (!data.empty()) {
            fragment_shader_source_ = std::string(data.begin(), data.end());
        }
    }
}

// Inicializa los shaders
void Screen::initShaders() {
#ifndef __APPLE__
    if (Options::video.shaders) {
        loadShaders();
        if (!shader_backend_) {
            shader_backend_ = std::make_unique<Rendering::OpenGLShader>();
        }
        shader_backend_->init(window_, game_canvas_, vertex_shader_source_, fragment_shader_source_);
    }
#else
    // En macOS, OpenGL está deprecated y rinde mal
    // TODO: Implementar backend de Metal para shaders en macOS
    Logger::info("WARNING: Shaders no disponibles en macOS (OpenGL deprecated). Usa Metal backend.", Logger::YELLOW);
#endif
}

// Calcula el tamaño de la ventana
void Screen::adjustWindowSize() {
    if (!Options::video.fullscreen) {
        // Establece el nuevo tamaño
        const int WIDTH = param.game.width * Options::window.zoom;
        const int HEIGHT = param.game.height * Options::window.zoom;

        int old_width;
        int old_height;
        SDL_GetWindowSize(window_, &old_width, &old_height);

        int old_pos_x;
        int old_pos_y;
        SDL_GetWindowPosition(window_, &old_pos_x, &old_pos_y);

        const int NEW_POS_X = old_pos_x + ((old_width - WIDTH) / 2);
        const int NEW_POS_Y = old_pos_y + ((old_height - HEIGHT) / 2);

        SDL_SetWindowPosition(window_, std::max(NEW_POS_X, WINDOWS_DECORATIONS), std::max(NEW_POS_Y, 0));
        SDL_SetWindowSize(window_, WIDTH, HEIGHT);
    }
}

// Renderiza todos los overlays y efectos
void Screen::renderOverlays() {
    // Dibuja efectos y elementos sobre el game_canvas_
    renderShake();
    renderFlash();
    renderAttenuate();
    service_menu_->render();
    notifier_->render();
#ifdef _DEBUG
    renderInfo();
#endif
}

// Atenua la pantalla
void Screen::renderAttenuate() {
    if (attenuate_effect_) {
        SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 64);
        SDL_RenderFillRect(renderer_, nullptr);
    }
}

// Arranca SDL VIDEO y crea la ventana
auto Screen::initSDLVideo() -> bool {
    // Inicializar SDL
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "FATAL: Failed to initialize SDL_VIDEO! SDL Error: %s",
            SDL_GetError());
        return false;
    }

    // Obtener información de la pantalla
    getDisplayInfo();

    // Configurar hint para renderizado
#ifdef __APPLE__
    if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
            "Warning: Failed to set Metal hint!");
    }
#else
    // Configurar hint de render driver
    if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl")) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
            "Warning: Failed to set OpenGL hint!");
    }

#ifdef _WIN32
    // Windows: Pedir explícitamente OpenGL 3.3 Core Profile
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Solicitando OpenGL 3.3 Core Profile");
    Logger::info("Solicitando OpenGL 3.3 Core Profile");
#else
    // Linux: Dejar que SDL elija (Desktop 3.3 en PC, ES 3.0 en RPi automáticamente)
    // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Usando OpenGL por defecto del sistema");
    Logger::info("Usando OpenGL por defecto del sistema");
#endif
#endif

    // Crear ventana
#ifdef __APPLE__
    SDL_WindowFlags window_flags = SDL_WINDOW_METAL;
#else
    SDL_WindowFlags window_flags = SDL_WINDOW_OPENGL;
#endif
    if (Options::video.fullscreen) {
        window_flags |= SDL_WINDOW_FULLSCREEN;
    }
    window_flags |= SDL_WINDOW_HIDDEN;
    window_ = SDL_CreateWindow(
        Options::window.caption.c_str(),
        param.game.width * Options::window.zoom,
        param.game.height * Options::window.zoom,
        window_flags);

    if (window_ == nullptr) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "FATAL: Failed to create window! SDL Error: %s",
            SDL_GetError());
        SDL_Quit();
        return false;
    }

    // Crear renderer
    renderer_ = SDL_CreateRenderer(window_, nullptr);
    if (renderer_ == nullptr) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
            "FATAL: Failed to create renderer! SDL Error: %s",
            SDL_GetError());
        SDL_DestroyWindow(window_);
        window_ = nullptr;
        SDL_Quit();
        return false;
    }

    // Configurar renderer
    SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
    SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
    SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);

    Logger::info("Video system initialized successfully");
    return true;
}

// Obtiene información sobre la pantalla
void Screen::getDisplayInfo() {
    int i;
    int num_displays = 0;
    SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
    if (displays != nullptr) {
        for (i = 0; i < num_displays; ++i) {
            SDL_DisplayID instance_id = displays[i];
            const char* name = SDL_GetDisplayName(instance_id);

            // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Display %" SDL_PRIu32 ": %s", instance_id, (name != nullptr) ? name : "Unknown");
            Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown"));
        }

        const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);

        // Guarda información del monitor en display_monitor_
        const char* first_display_name = SDL_GetDisplayName(displays[0]);
        display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
        display_monitor_.width = static_cast<int>(dm->w);
        display_monitor_.height = static_cast<int>(dm->h);
        display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);

        // Calcula el máximo factor de zoom que se puede aplicar a la pantalla
        Options::window.max_zoom = std::min(dm->w / param.game.width, dm->h / param.game.height);
        Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);

        // Obtiene la cadena con la información sobre la resolución y el refresco
        Options::video.info = std::to_string(dm->w) + "x" +
            std::to_string(dm->h) + " @ " +
            std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";

        // Muestra información sobre el tamaño de la pantalla y de la ventana de juego
        // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Current display mode: %dx%d @ %dHz", static_cast<int>(dm->w), static_cast<int>(dm->h), static_cast<int>(dm->refresh_rate));
        Logger::info("Current display mode: " + Options::video.info);

        // SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Window resolution: %dx%d x%d", static_cast<int>(param.game.width), static_cast<int>(param.game.height), Options::window.zoom);
        Logger::info("Window resolution: " + std::to_string(static_cast<int>(param.game.width)) + "x" + std::to_string(static_cast<int>(param.game.height)) + "x" + std::to_string(Options::window.zoom));

        // Calcula el máximo factor de zoom que se puede aplicar a la pantalla
        const int MAX_ZOOM = std::min(dm->w / param.game.width, (dm->h - WINDOWS_DECORATIONS) / param.game.height);

        // Normaliza los valores de zoom
        Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);

        SDL_free(displays);
    }
}

// Alterna entre activar y desactivar los shaders
void Screen::toggleShaders() {
    Options::video.shaders = !Options::video.shaders;
    initShaders();
}

// Alterna entre activar y desactivar el escalado entero
void Screen::toggleIntegerScale() {
    Options::video.integer_scale = !Options::video.integer_scale;
    SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
}

// Alterna entre activar y desactivar el V-Sync
void Screen::toggleVSync() {
    Options::video.vsync = !Options::video.vsync;
    SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
}

// Establece el estado del V-Sync
void Screen::setVSync(bool enabled) {
    Options::video.vsync = enabled;
    SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
}

// Obtiene los punteros a los singletones
void Screen::getSingletons() {
    service_menu_ = ServiceMenu::get();
    notifier_ = Notifier::get();
}

// Aplica los valores de las opciones
void Screen::applySettings() {
    SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
    SDL_SetRenderLogicalPresentation(Screen::get()->getRenderer(), param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
    setFullscreenMode();
    adjustWindowSize();
}

// Crea el objeto de texto
void Screen::createText() {
    auto texture = std::make_shared<Texture>(getRenderer(), Asset::get()->get("aseprite.png"));
    text_ = std::make_shared<Text>(texture, Asset::get()->get("aseprite.txt"));
}