#include "screen.h"
#include <SDL2/SDL_events.h> // for SDL_DISABLE, SDL_ENABLE
#include <SDL2/SDL_mouse.h>  // for SDL_ShowCursor
#include <SDL2/SDL_pixels.h> // for SDL_PIXELFORMAT_RGBA8888
#include <SDL2/SDL_timer.h>  // for SDL_GetTicks
#include <algorithm>         // for max, min
#include <fstream>           // for basic_ifstream, ifstream
#include <iterator>          // for istreambuf_iterator, operator!=
#include <string>            // for basic_string, operator+, to_string, cha...
#include <vector>            // for vector
#include "asset.h"           // for Asset
#include "dbgtxt.h"          // for dbg_print
#include "global_inputs.h"   // for servicePressedCounter
#include "input.h"           // for Input, inputs_e, INPUT_DO_NOT_ALLOW_REPEAT
#include "notify.h"          // for Notify
#include "on_screen_help.h"  // for OnScreenHelp
#include "options.h"         // for options
#include "param.h"           // for param

#ifndef NO_SHADERS
#include "jail_shader.h" // for 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)
{
    // Copia punteros
    input = Input::get();
    asset = Asset::get();

    // Inicializa variables
    srcrect = {0, 0, param.game.width, param.game.height};
    dstrect = {0, 0, param.game.width, param.game.height};
    borderColor = {0, 0, 0};
    flashEffect.enabled = false;
    flashEffect.counter = 0;
    flashEffect.lenght = 0;
    flashEffect.color = {0xFF, 0xFF, 0xFF};
    shakeEffect.enabled = false;
    shakeEffect.desp = 2;
    shakeEffect.delay = 3;
    shakeEffect.counter = 0;
    shakeEffect.lenght = 8;
    shakeEffect.remaining = 0;
    shakeEffect.originalPos = 0;
    shakeEffect.originalWidth = param.game.width;
    attenuateEffect = false;
    fpsTicks = 0;
    fpsCounter = 0;
    fps = 0;
#ifdef DEBUG
    showInfo = true;
#else
    showInfo = false;
#endif
    SDL_DisplayMode DM;
    SDL_GetCurrentDisplayMode(0, &DM);
    infoResolution = std::to_string(DM.w) + " X " + std::to_string(DM.h) + " AT " + std::to_string(DM.refresh_rate) + " HZ";

    // Crea los objetos
    notify = std::make_unique<Notify>(renderer, "", asset->get("8bithud.png"), asset->get("8bithud.txt"), asset->get("notify.wav"));

    // Define el color del borde para el modo de pantalla completa
    borderColor = {0x00, 0x00, 0x00};

    // Crea las textura donde se dibujan los graficos del juego
    gameCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
    shaderCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);

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

    // Muestra la ventana
    SDL_ShowWindow(window);
}

// Destructor
Screen::~Screen()
{
    SDL_DestroyTexture(gameCanvas);
    SDL_DestroyTexture(shaderCanvas);
}

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

// Vuelca el contenido del renderizador en pantalla
void Screen::blit()
{
    // Actualiza el contador de FPS
    fpsCounter++;

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

    // Muestra las notificaciones
    notify->render();

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

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

    // Copia la textura de juego en el renderizador en la posición adecuada
    if (shakeEffect.enabled)
        SDL_RenderCopy(renderer, gameCanvas, nullptr, nullptr);
    SDL_RenderCopy(renderer, gameCanvas, &srcrect, &dstrect);

    // Muestra por pantalla el renderizador
    SDL_RenderPresent(renderer);
#else
    if (options.video.shaders)
    {
        SDL_SetRenderTarget(renderer, shaderCanvas);
        SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
        SDL_RenderClear(renderer);
        if (shakeEffect.enabled)
            SDL_RenderCopy(renderer, gameCanvas, nullptr, nullptr);
        SDL_RenderCopy(renderer, gameCanvas, &srcrect, &dstrect);
        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 (shakeEffect.enabled)
            SDL_RenderCopy(renderer, gameCanvas, nullptr, nullptr);
        SDL_RenderCopy(renderer, gameCanvas, &srcrect, &dstrect);

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

// Establece el modo de video
void Screen::setVideoMode(ScreenVideoMode videoMode)
{
    options.video.mode = videoMode;
#ifdef ARCADE
    options.video.mode = ScreenVideoMode::WINDOW;
#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_SetWindowSize(window, param.game.width * options.video.window.size, param.game.height * options.video.window.size);
        SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);

        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("crtpi.glsl").c_str());
        std::string source((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());

        shader::init(window, shaderCanvas, source.c_str());
#endif
    }
}

// Camibia entre pantalla completa y ventana
void Screen::switchVideoMode()
{
    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)
{
    borderColor = 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()
{
    updateShake();
    notify->update();
    updateFPS();
    OnScreenHelp::get()->update();
}

// Comprueba las entradas
void Screen::checkInput()
{
#ifndef ARCADE
    // Comprueba el teclado para cambiar entre pantalla completa y ventana
    if (input->checkInput(input_window_fullscreen, INPUT_DO_NOT_ALLOW_REPEAT, INPUT_USE_KEYBOARD))
    {
        switchVideoMode();
        const std::string mode = options.video.mode == ScreenVideoMode::WINDOW ? "Window" : "Fullscreen";
        showNotification(mode + " mode");
        return;
    }

    // Comprueba el teclado para decrementar el tamaño de la ventana
    if (input->checkInput(input_window_dec_size, INPUT_DO_NOT_ALLOW_REPEAT, INPUT_USE_KEYBOARD))
    {
        decWindowSize();
        const std::string size = std::to_string(options.video.window.size);
        showNotification("Window size x" + size);
        return;
    }

    // Comprueba el teclado para incrementar el tamaño de la ventana
    if (input->checkInput(input_window_inc_size, INPUT_DO_NOT_ALLOW_REPEAT, INPUT_USE_KEYBOARD))
    {
        incWindowSize();
        const std::string size = std::to_string(options.video.window.size);
        showNotification("Window size x" + size);
        return;
    }
#endif

    // Comprueba el teclado para activar o desactivar los shaders
    if (input->checkInput(input_video_shaders, INPUT_DO_NOT_ALLOW_REPEAT, INPUT_USE_KEYBOARD))
    {
        switchShaders();
        return;
    }

#ifdef DEBUG
    // Comprueba el teclado para mostrar la información de debug
    if (input->checkInput(input_showinfo, INPUT_DO_NOT_ALLOW_REPEAT, INPUT_USE_KEYBOARD))
    {
        showInfo = !showInfo;
        return;
    }
#endif

    for (int i = 0; i < input->getNumControllers(); ++i)
    {
        // Comprueba los mandos para activar o desactivar los shaders
        if (input->checkModInput(input_service, input_video_shaders, INPUT_DO_NOT_ALLOW_REPEAT, INPUT_USE_GAMECONTROLLER, i))
        {
            switchShaders();
            return;
        }

        // Comprueba los mandos para mostrar la información de debug
        if (input->checkModInput(input_service, input_showinfo, INPUT_DO_NOT_ALLOW_REPEAT, INPUT_USE_GAMECONTROLLER, i))
        {
            showInfo = !showInfo;
            return;
        }
    }
}

// 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 (!shakeEffect.enabled)
    {
        shakeEffect.enabled = true;
        shakeEffect.originalPos = srcrect.x;
        shakeEffect.originalWidth = srcrect.w;
        srcrect.w -= shakeEffect.desp;
        dstrect.w = srcrect.w;
    }

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

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

// Pone la pantalla de color
void Screen::flash(Color color, int lenght)
{
    flashEffect.enabled = true;
    flashEffect.counter = 0;
    flashEffect.lenght = lenght;
    flashEffect.color = color;
}

// Actualiza y dibuja el efecto de flash en la pantalla
void Screen::doFlash()
{
    if (flashEffect.enabled)
    {
        // Dibuja el color del flash en la textura
        SDL_Texture *temp = SDL_GetRenderTarget(renderer);
        SDL_SetRenderTarget(renderer, gameCanvas);
        SDL_SetRenderDrawColor(renderer, flashEffect.color.r, flashEffect.color.g, flashEffect.color.b, 0xFF);
        SDL_RenderClear(renderer);
        SDL_SetRenderTarget(renderer, temp);

        // Actualiza la lógica del efecto
        flashEffect.counter < flashEffect.lenght ? flashEffect.counter++ : flashEffect.enabled = false;
    }
}

// Atenua la pantalla
void Screen::doAttenuate()
{
    if (attenuateEffect)
    {
        SDL_Texture *temp = SDL_GetRenderTarget(renderer);
        SDL_SetRenderTarget(renderer, gameCanvas);
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 64);
        SDL_RenderFillRect(renderer, nullptr);
        SDL_SetRenderTarget(renderer, temp);
    }
}

// Activa/desactiva los shaders
void Screen::switchShaders()
{
    options.video.shaders = !options.video.shaders;
    setVideoMode(options.video.mode);
    const std::string value = options.video.shaders ? "on" : "off";
    showNotification("Shaders " + value);
}

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

// Muestra una notificación de texto por pantalla;
void Screen::showNotification(std::string text1, std::string text2, int icon)
{
    notify->showText(text1, text2, icon);
}

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

// Calcula los frames por segundo
void Screen::updateFPS()
{
    if (SDL_GetTicks() - fpsTicks > 1000)
    {
        fpsTicks = SDL_GetTicks();
        fps = fpsCounter;
        fpsCounter = 0;
    }
}

// Muestra información por pantalla
void Screen::displayInfo()
{
    if (showInfo)
    {
        // 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, infoResolution.c_str(), 255, 255, 0);

        dbg_print(0, 8, std::to_string(globalInputs::servicePressedCounter[0]).c_str(), 255, 255, 0);
    }
}

// Indica si hay alguna notificación activa en pantalla
bool Screen::notificationsAreActive()
{
    return notify->active();
}