#include "screen.h"
#include <SDL2/SDL_events.h> // Para SDL_DISABLE, SDL_ENABLE
#include <SDL2/SDL_mouse.h>  // Para SDL_ShowCursor
#include <SDL2/SDL_pixels.h> // Para SDL_PIXELFORMAT_RGBA8888
#include <SDL2/SDL_timer.h>  // Para SDL_GetTicks
#include <algorithm>         // Para clamp, max, min
#include <fstream>           // Para basic_ifstream, ifstream
#include <iterator>          // Para istreambuf_iterator, operator==
#include <string>            // Para allocator, operator+, to_string, char_t...
#include <vector>            // Para vector
#include "asset.h"           // Para Asset
#include "dbgtxt.h"          // Para dbg_print
#include "global_inputs.h"   // Para service_pressed_counter
#include "input.h"           // Para Input, InputType, INPUT_DO_NOT_ALLOW_RE...
#include "notifier.h"        // Para Notifier
#include "on_screen_help.h"  // Para OnScreenHelp
#include "options.h"         // Para Options, OptionsVideo, options, Options...

#ifndef NO_SHADERS
#include "jail_shader.h" // para init, render
#endif

// [SINGLETON] Hay que definir las variables estáticas, desde el .h sólo la hemos declarado
Screen *Screen::screen_ = nullptr;

// [SINGLETON] Crearemos el objeto screen con esta función estática
void Screen::init(SDL_Window *window, SDL_Renderer *renderer)
{
    Screen::screen_ = new Screen(window, renderer);
}

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

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

// Constructor
Screen::Screen(SDL_Window *window, SDL_Renderer *renderer)
    : window_(window),
      renderer_(renderer),
      game_canvas_(SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height)),
      shader_canvas_(SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height)),
      src_rect_({0, 0, param.game.width, param.game.height}),
      dst_rect_({0, 0, param.game.width, param.game.height})
{
    // Inicializa variables
    SDL_DisplayMode DM;
    SDL_GetCurrentDisplayMode(0, &DM);
    info_resolution_ = std::to_string(DM.w) + " X " + std::to_string(DM.h) + " AT " + std::to_string(DM.refresh_rate) + " HZ";

    // Establece el modo de video
    setVideoMode(options.video.mode);

    // Muestra la ventana
    SDL_ShowWindow(window_);
}

// Destructor
Screen::~Screen()
{
    SDL_DestroyTexture(game_canvas_);
    SDL_DestroyTexture(shader_canvas_);
}

// 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::blit()
{
    // Actualiza el contador de FPS
    fps_counter_++;

    // Actualiza y dibuja el efecto de flash en la pantalla
    doFlash();

    // Atenua la pantalla
    doAttenuate();

    // Muestra la ayuda por pantalla
    OnScreenHelp::get()->render();

    // Muestra información por pantalla
    renderInfo();

    // Muestra las notificaciones
    Notifier::get()->render();

#ifdef NO_SHADERS
    // Vuelve a dejar el renderizador en modo normal
    SDL_SetRenderTarget(renderer_, nullptr);

    // Borra el contenido previo
    SDL_SetRenderDrawColor(renderer_, border_color_.r, border_color_.g, border_color_.b, 0xFF);
    SDL_RenderClear(renderer_);

    // Copia la textura de juego en el renderizador en la posición adecuada
    if (shake_effect_.enabled)
        SDL_RenderCopy(renderer_, game_canvas_, nullptr, nullptr);
    SDL_RenderCopy(renderer_, game_canvas_, &src_rect_, &dst_rect_);

    // Muestra por pantalla el renderizador
    SDL_RenderPresent(renderer_);
#else
    if (options.video.shaders)
    {
        SDL_SetRenderTarget(renderer_, shader_canvas_);
        SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
        SDL_RenderClear(renderer_);
        if (shake_effect_.enabled)
            SDL_RenderCopy(renderer_, game_canvas_, nullptr, nullptr);
        SDL_RenderCopy(renderer_, game_canvas_, &src_rect_, &dst_rect_);
        SDL_SetRenderTarget(renderer_, nullptr);
        shader::render();
    }
    else
    {
        // Vuelve a dejar el renderizador en modo normal
        SDL_SetRenderTarget(renderer_, nullptr);

        // Borra el render
        SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
        SDL_RenderClear(renderer_);

        // Copia la textura de juego en el renderizador en la posición adecuada
        if (shake_effect_.enabled)
            SDL_RenderCopy(renderer_, game_canvas_, nullptr, nullptr); // Esta copia es para evitar que se vea negro por los laterales
        SDL_RenderCopy(renderer_, game_canvas_, &src_rect_, &dst_rect_);

        // Muestra por pantalla el renderizador
        SDL_RenderPresent(renderer_);
    }
#endif
}

// Establece el modo de video
void Screen::setVideoMode(ScreenVideoMode videoMode)
{
#ifdef ARCADE
    options.video.mode = ScreenVideoMode::WINDOW;
#else
    options.video.mode = videoMode;
#endif

    switch (options.video.mode)
    {
    case ScreenVideoMode::WINDOW:
    {
        // Cambia a modo de ventana
        SDL_SetWindowFullscreen(window_, 0);

#ifdef ARCADE
        // Oculta el puntero
        SDL_ShowCursor(SDL_DISABLE);
#else
        // Muestra el puntero
        SDL_ShowCursor(SDL_ENABLE);
#endif
        // Modifica el tamaño de la ventana
        SDL_Point pos = getNewPosition();
        SDL_SetWindowSize(window_, param.game.width * options.video.window.size, param.game.height * options.video.window.size);
        SDL_SetWindowPosition(window_, pos.x, pos.y);

        break;
    }

    // Si está activo el modo de pantalla completa añade el borde
    case ScreenVideoMode::FULLSCREEN:
    {
        // Aplica el modo de video
        SDL_SetWindowFullscreen(window_, SDL_WINDOW_FULLSCREEN_DESKTOP);

        // Oculta el puntero
        SDL_ShowCursor(SDL_DISABLE);

        break;
    }

    default:
        break;
    }

    // Reinicia los shaders
    if (options.video.shaders)
    {
#ifndef NO_SHADERS
        std::ifstream f(Asset::get()->get("crtpi.glsl").c_str());
        std::string source((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());

        shader::init(window_, shader_canvas_, source.c_str());
#endif
    }
}

// Camibia entre pantalla completa y ventana
void Screen::toggleVideoMode()
{
    options.video.mode = options.video.mode == ScreenVideoMode::WINDOW ? ScreenVideoMode::FULLSCREEN : ScreenVideoMode::WINDOW;
    setVideoMode(options.video.mode);
}

// Cambia el tamaño de la ventana
void Screen::setWindowSize(int size)
{
    options.video.window.size = size;
    setVideoMode(ScreenVideoMode::WINDOW);
}

// Reduce el tamaño de la ventana
void Screen::decWindowSize()
{
    --options.video.window.size;
    options.video.window.size = std::max(options.video.window.size, 1);
    setVideoMode(ScreenVideoMode::WINDOW);
}

// Aumenta el tamaño de la ventana
void Screen::incWindowSize()
{
    ++options.video.window.size;
    options.video.window.size = std::min(options.video.window.size, 4);
    setVideoMode(ScreenVideoMode::WINDOW);
}

// Cambia el color del borde
void Screen::setBorderColor(Color color)
{
    border_color_ = color;
}

// Cambia el tipo de mezcla
void Screen::setBlendMode(SDL_BlendMode blendMode)
{
    SDL_SetRenderDrawBlendMode(renderer_, blendMode);
}

// Actualiza la lógica de la clase
void Screen::update()
{
    updateShakeEffect();
    Notifier::get()->update();
    updateFPS();
    OnScreenHelp::get()->update();
}

// Agita la pantalla
void Screen::shake()
{
    // Si no hay un shake effect activo, se guarda una copia de los valores actuales antes de modificarlos
    if (!shake_effect_.enabled)
    {
        shake_effect_.enabled = true;
        shake_effect_.originalPos = src_rect_.x;
        shake_effect_.originalWidth = src_rect_.w;
        src_rect_.w -= shake_effect_.desp;
        dst_rect_.w = src_rect_.w;
    }

    // Si ya hay un shake effect en marcha no se pilla el origen, solo se renuevan los contadores
    shake_effect_.remaining = shake_effect_.lenght;
    shake_effect_.counter = shake_effect_.delay;
}

// Actualiza la logica para agitar la pantalla
void Screen::updateShakeEffect()
{
    if (shake_effect_.enabled)
    {
        if (shake_effect_.counter > 0)
        {
            shake_effect_.counter--;
        }
        else
        {
            shake_effect_.counter = shake_effect_.delay;
            const auto srcdesp = shake_effect_.remaining % 2 == 0 ? 0 : shake_effect_.desp;
            const auto dstdesp = shake_effect_.remaining % 2 == 1 ? 0 : shake_effect_.desp;
            src_rect_.x = shake_effect_.originalPos + srcdesp;
            dst_rect_.x = shake_effect_.originalPos + dstdesp;
            shake_effect_.remaining--;
            shake_effect_.enabled = shake_effect_.remaining == -1 ? false : true;
            if (!shake_effect_.enabled)
            {
                src_rect_.x = shake_effect_.originalPos;
                src_rect_.w = shake_effect_.originalWidth;
                dst_rect_ = src_rect_;
            }
        }
    }
}

// Pone la pantalla de color
void Screen::flash(Color color, int lenght)
{
    flash_effect_ = FlashEffect(true, lenght, color);
}

// Actualiza y dibuja el efecto de flash en la pantalla
void Screen::doFlash()
{
    if (flash_effect_.enabled)
    {
        // Dibuja el color del flash en la textura
        SDL_SetRenderDrawColor(renderer_, flash_effect_.color.r, flash_effect_.color.g, flash_effect_.color.b, 0xFF);
        SDL_RenderClear(renderer_);

        // Actualiza la lógica del efecto
        flash_effect_.counter > 0 ? flash_effect_.counter-- : flash_effect_.enabled = false;
    }
}

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

// Activa / desactiva los shaders
void Screen::toggleShaders()
{
    options.video.shaders = !options.video.shaders;
    setVideoMode(options.video.mode);
    const std::string value = options.video.shaders ? "on" : "off";
    Notifier::get()->showText({"Shaders " + value});
}

// Activa / desactiva la información de debug
void Screen::toggleDebugInfo()
{
    show_info_ = !show_info_;
}

// Atenua la pantalla
void Screen::attenuate(bool value)
{
    attenuate_effect_ = value;
}

// Obtiene el puntero al renderizador
SDL_Renderer *Screen::getRenderer()
{
    return renderer_;
}

// Calcula los frames por segundo
void Screen::updateFPS()
{
    if (SDL_GetTicks() - fps_ticks_ > 1000)
    {
        fps_ticks_ = SDL_GetTicks();
        fps_ = fps_counter_;
        fps_counter_ = 0;
    }
}

// Muestra información por pantalla
void Screen::renderInfo()
{
    if (show_info_)
    {
        // FPS
        const std::string fpstext = std::to_string(fps_) + " FPS";
        dbg_print(param.game.width - fpstext.length() * 8, 0, fpstext.c_str(), 255, 255, 0);

        // Resolution
        dbg_print(0, 0, info_resolution_.c_str(), 255, 255, 0);

        // Contador de service_pressed_counter
        if (const int counter = globalInputs::service_pressed_counter[0]; counter > 0)
            dbg_print(0, 8, std::to_string(counter).c_str(), 255, 0, 255);
    }
}

// Calcula la nueva posición de la ventana a partir de la antigua al cambiarla de tamaño
SDL_Point Screen::getNewPosition()
{
    // Obtiene la posición actual de la ventana
    SDL_Point current_position;
    SDL_GetWindowPosition(window_, &current_position.x, &current_position.y);

    // Obtiene las dimensiones actuales de la ventana
    int current_width, current_height;
    SDL_GetWindowSize(window_, &current_width, &current_height);

    // Obtiene las dimesiones que tendrá la ventana
    const int new_width = param.game.width * options.video.window.size;
    const int new_height = param.game.height * options.video.window.size;

    // Obtiene el centro de la ventana actual
    SDL_Point center;
    center.x = current_position.x + current_width / 2;
    center.y = current_position.y + current_height / 2;

    // Calcula la nueva posición a partir del centro y las nuevas diemsiones
    SDL_Point new_pos;
    new_pos.x = center.x - new_width / 2;
    new_pos.y = center.y - new_height / 2;

    // Obtiene las dimensiones del escritorio
    SDL_DisplayMode DM;
    SDL_GetCurrentDisplayMode(0, &DM);

    // Evita que la ventana quede fuera del escritorio
    new_pos.x = std::clamp(new_pos.x, 30, DM.w - new_width);
    new_pos.y = std::clamp(new_pos.y, 30, DM.h - new_height);

    return new_pos;
}