#include "screen.h"
#include <SDL3/SDL_blendmode.h> // Para SDL_BLENDMODE_BLEND
#include <SDL3/SDL_error.h>     // Para SDL_GetError
#include <SDL3/SDL_hints.h>     // Para SDL_SetHint, SDL_HINT_RENDER_DRIVER
#include <SDL3/SDL_init.h>      // Para SDL_Init, SDL_INIT_VIDEO
#include <SDL3/SDL_log.h>       // Para SDL_LogCategory, SDL_LogError, SDL_L...
#include <SDL3/SDL_pixels.h>    // Para SDL_PixelFormat
#include <SDL3/SDL_surface.h>   // Para SDL_ScaleMode
#include <SDL3/SDL_timer.h>     // Para SDL_GetTicks
#include <algorithm>            // Para min, max
#include <fstream>              // Para basic_ifstream, ifstream
#include <iterator>             // Para istreambuf_iterator, operator==
#include <memory>               // Para __shared_ptr_access, shared_ptr
#include <string>               // Para allocator, char_traits, operator+
#include "asset.h"              // Para Asset
#include "jail_shader.h"        // Para init, render
#include "mouse.h"              // Para updateCursorVisibility
#include "notifier.h"           // Para Notifier
#include "options.h"            // Para Options, options, WindowOptions, Vid...
#include "resource.h"           // Para Resource
#include "text.h"               // Para Text

// [SINGLETON]
Screen *Screen::screen_ = nullptr;

// [SINGLETON] Crearemos el objeto con esta función estática
void Screen::init() { Screen::screen_ = new Screen(); }

// [SINGLETON] Destruiremos el objeto con esta función estática
void Screen::destroy() { delete Screen::screen_; }

// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
Screen *Screen::get() { return Screen::screen_; }

// Constructor
Screen::Screen()
    : src_rect_(SDL_FRect{0, 0, static_cast<float>(param.game.width), static_cast<float>(param.game.height)}),
      dst_rect_(SDL_FRect{0, 0, static_cast<float>(param.game.width), static_cast<float>(param.game.height)})
{
    // Arranca SDL VIDEO, crea la ventana y el renderizador
    initSDL();

    // 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);

    // Inicializa variables
    adjustRenderLogicalSize();

    // Muestra la ventana
    show();

    // Inicializa los shaders
    initShaders(); // Se ha de ejecutar con la ventana visible
}

// 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();

    // Renderiza todos los overlays y efectos
    renderOverlays();

    // Renderiza el contenido del game_canvas_
    renderScreen();
}

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

    if (options.video.shaders)
    {
        shader::render();
    }
    else
    {
        SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
        SDL_RenderPresent(renderer_);
    }
}

// Establece el modo de video
void Screen::setFullscreenMode(bool mode)
{
    // Actualiza las opciones
    options.video.fullscreen = mode;

    // Configura el modo de pantalla
    SDL_SetWindowFullscreen(window_, options.video.fullscreen);

    initShaders();
}

// 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
bool Screen::decWindowZoom()
{
    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
bool Screen::incWindowZoom()
{
    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;
}

// Actualiza la lógica de la clase
void Screen::update()
{
    fps_.calculate(SDL_GetTicks());
    shake_effect_.update(src_rect_, dst_rect_);
    flash_effect_.update();
    Notifier::get()->update();
    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)
    {
        // FPS
        const std::string FPS_TEXT = std::to_string(fps_.lastValue) + " FPS";
        debug_info_.text->writeColored(param.game.width - debug_info_.text->lenght(FPS_TEXT), 0, FPS_TEXT, ORANGE_SOFT_COLOR);

        // Resolution
        debug_info_.text->writeColored(0, 0, options.video.info, ORANGE_SOFT_COLOR);
    }
}
#endif
// Carga el contenido del archivo GLSL
void Screen::loadShaders()
{
    const std::string GLSL_FILE = param.game.game_area.rect.h == 256 ? "crtpi_256.glsl" : "crtpi_240.glsl";
    std::ifstream f(Asset::get().get(GLSL_FILE).c_str());
    shader_source_ = std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
}

// Inicializa los shaders
void Screen::initShaders()
{
    if (shader_source_.empty())
    {
        loadShaders();
    }
    shader::init(window_, game_canvas_, shader_source_);
}

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

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

        int old_pos_x, 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);

        initShaders();
    }
}

// Renderiza todos los overlays y efectos
void Screen::renderOverlays()
{
    // Dibuja efectos y elementos sobre el game_canvas_
    renderShake();
    renderFlash();
    renderAttenuate();
    Notifier::get()->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
bool Screen::initSDL()
{
    // Indicador de éxito
    auto success = true;

    // Inicializa SDL
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_VIDEO could not initialize! SDL Error: %s", SDL_GetError());
        success = false;
    }
    else
    {
        SDL_LogInfo(SDL_LOG_CATEGORY_TEST, "\n** SDL_VIDEO: INITIALIZING\n");

        getDisplayInfo();

        if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl"))
        {
            SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: opengl not enabled!");
        }

        // Crea la ventana
        window_ = SDL_CreateWindow(options.window.caption.c_str(), param.game.width * options.window.zoom, param.game.height * options.window.zoom, SDL_WINDOW_OPENGL);
        if (!window_)
        {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Window could not be created! SDL Error: %s", SDL_GetError());
            success = false;
        }
        else
        {
            renderer_ = SDL_CreateRenderer(window_, nullptr);

            if (!renderer_)
            {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Renderer could not be created! SDL Error: %s", SDL_GetError());
                success = false;
            }
            else
            {
                SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
                SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
                SDL_SetWindowFullscreen(window_, options.video.fullscreen);
                SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
                SDL_SetRenderVSync(renderer_, options.video.v_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
            }
        }
    }

    SDL_LogInfo(SDL_LOG_CATEGORY_TEST, "** SDL_VIDEO: INITIALIZATION COMPLETE\n");
    return success;
}

// Obtiene información sobre la pantalla
void Screen::getDisplayInfo()
{
    int i, num_displays = 0;
    SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
    if (displays)
    {
        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 ? name : "Unknown");
        }

        auto DM = SDL_GetCurrentDisplayMode(displays[0]);

        // 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);

        // 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));

        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);

        options.video.info = std::to_string(static_cast<int>(DM->w)) + " X " +
                             std::to_string(static_cast<int>(DM->h)) + " AT " +
                             std::to_string(static_cast<int>(DM->refresh_rate)) + " HZ";

        // 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);
    }
}

// Activa / desactiva el escalado entero
void Screen::toggleIntegerScale()
{
    options.video.integer_scale = !options.video.integer_scale;
    SDL_SetRenderLogicalPresentation(Screen::get()->getRenderer(), param.game.width, param.game.height, options.video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
}

// Activa / desactiva el vsync
void Screen::toggleVSync()
{
    options.video.v_sync = !options.video.v_sync;
    SDL_SetRenderVSync(renderer_, options.video.v_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
}