/*

Ejemplo de uso de las unidades incluídas en jail_engine
Código fuente creado por JailDesigner

*/

#include <SDL2/SDL.h>
#include <iostream>
#include "units/jail_audio.h"
#include "units/text.h"
#include "units/utils.h"
#include "units/asset.h"
#include "units/movingsprite.h"
#include "units/texture.h"
#include "units/screen.h"
#include "units/input.h"
#include "units/on_screen_keyboard.h"
#include "quickcg.h"
using namespace QuickCG;

// Punteros
SDL_Event *event;
SDL_Window *window;
SDL_Renderer *renderer;
Asset *asset;
JA_Music_t *music;
JA_Sound_t *sound;
Input *input;
struct options_t *options;
Screen *screen;
Text *text;
Text *debugText;
Texture *texture;
MovingSprite *sprite;
OnScreenKeyboard *osk;

// Variables de uso general
Uint32 ticks = 0;			  // Variable para la frecuencia de actualización de la lógica del programa
const Uint32 ticksSpeed = 15; // Variable para la frecuencia de actualización de la lógica del programa
bool should_exit = false;	  // Variable para saber si ha terminado el progra,a
int counter = 0;			  // Contador para lo que se necesite
string controllerName;		  // Nombre del primer mando detectado
string inputPressed;		  // Texto con el último input que se ha pulsado
int fx = 0;					  // Efecto seleccionado para el fondo
// 0 - Degradado
// 1 - Fuego
int fxTotal = 2; // Cantidad total de efectos disponibles para el fondo

// Variables para el efecto del degradado
int gradColorMin = 64;		 // Minimo color más alto del degradado
int gradColorMax = 192;		 // Minimo color más alto del degradado
int gradCurrentColor = 192;	 // Color actual más alto del degradado
int gradBreathDirection = 0; // Indica si gradCurrentColor crece o decrece

// Variables para el efecto de fuego
const int fireDesp = 3;											// Desplazamiento inferior del efecto de fuego para eliminar las primeras filas
const int fireWidth = 320;										// Ancho del efecto de fuego
const int fireHeight = 240 + fireDesp;							// Alto del efecto de fuego
Uint8 fire[fireHeight][fireWidth];								// Buffer con el fuego
Uint32 palette[256];											// Paleta de color para el fuego
int fireModifier = 28;											// Valor para modificar el efecto de fuego (0-30)
SDL_Texture *fireTexture;										// Textura donde pintar los pixels del fuego
Uint32 *fireBuffer;												// Ubiación donde se guardan los pixeles del fuego
int pitch;														// No se utiliza pero se necesita para dibujar los pixeles en la textura
SDL_Rect fireSource = {0, 0, fireWidth, fireHeight - fireDesp}; // Parte de la textura donde está el efecto de fuego que se copiará a la pantalla

// Inicializa las opciones
void initOptions();

// Inicializa la lista de recursos
void initAsset(char *argv[]);

// Inicializa SDL
void initSDL();

// Inicializa la ventana
void initWindow();

// Inicializa el gestor de eventos
void initEvent();

// Inicializa jail_audio
void initJailAudio();

// Inicializa el objeto screen
void initScreen();

// Inicializa el objeto input
void initInput();

// Inicializa el texto
void initText();

// Inicializa el teclado en pantalla
void initOnScreenKeyboard();

// Inicializa el sprite
void initSprite();

// Inicializa el efecto de fuego
void initFire();

// Inicializa todo
void initAll(char *argv[]);

// Comprueba el teclado y los eventos
void checkEvents();

// Comprueba el objeto input
void checkInput();

// Actualiza el sprite
void updateSprite();

// Actualiza el efecto de degradado
void updateGradient();

// Actualiza el efecto de fuego
void updateFire();

// Actualiza el efecto de fondo
void updateFX();

// Actualiza la lógica del programa
void update();

// Dibuja un degradado de fondo
void renderGradient();

// Dibuja el efecto de fuego
void renderFire();

// Dibuja el efecto de fondo
void renderFX();

// Dibuja el texto
void renderText();

// Dibuja los elementos del programa en pantalla
void render();

// Libera la memoria reservada
void freeAll();

// Inicializa las opciones
void initOptions()
{
	options = new options_t;
	initOptions(options);
	options->screen.nativeWidth = 320;
	options->screen.nativeHeight = 240;
	options->screen.nativeZoom = 2;
	options->screen.windowZoom = 1;
	options->console = true;
}

// Inicializa la lista de recursos
void initAsset(char *argv[])
{
	asset = new Asset(argv[0]);
	asset->add("/data/music.ogg", t_music);
	asset->add("/data/sound.wav", t_sound);
	asset->add("/data/smb2.txt", t_font);
	asset->add("/data/smb2.png", t_bitmap);
	asset->add("/data/debug.txt", t_font);
	asset->add("/data/debug.png", t_bitmap);
	asset->add("/data/z80.png", t_bitmap);
	asset->add("/data/notify.png", t_bitmap);
	asset->add("/data/notify.wav", t_sound);
	asset->add("/data/gamecontrollerdb.txt", t_data);
	asset->setVerbose(options->console);
	if (!asset->check())
	{
		exit(EXIT_FAILURE);
	}
}

// Inicializa SDL
void initSDL()
{
	SDL_Init(SDL_INIT_EVERYTHING);
}

// Inicializa la ventana
void initWindow()
{
	window = SDL_CreateWindow("Jail Engine DEMO", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, options->screen.nativeWidth * options->screen.nativeZoom * options->screen.windowZoom, options->screen.nativeHeight * options->screen.nativeZoom * options->screen.windowZoom, SDL_WINDOW_SHOWN);
	if (window != nullptr)
	{
		renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
		if (renderer != nullptr)
		{
			SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
		}
		else
		{
			exit(EXIT_FAILURE);
		}
	}
	else
	{
		exit(EXIT_FAILURE);
	}
}

// Inicializa el gestor de eventos
void initEvent()
{
	event = new SDL_Event();
}

// Inicializa jail_audio
void initJailAudio()
{
	JA_Init(48000, AUDIO_S16, 2);
	music = JA_LoadMusic(asset->get("music.ogg").c_str());
	sound = JA_LoadSound(asset->get("sound.wav").c_str());
	// JA_PlayMusic(music, true);
}

// Inicializa el objeto screen
void initScreen()
{
	screen = new Screen(window, renderer, options);
	screen->addNotifier(asset->get("notify.png"), asset->get("smb2.png"), asset->get("smb2.txt"), asset->get("notify.wav"));
}

// Inicializa el objeto input
void initInput()
{
	input = new Input(asset->get("gamecontrollerdb.txt"), options->console);
	controllerName = input->getNumControllers() > 0 ? input->getControllerName(0) : "No se ha encontrado ningun mando";
}

// Inicializa el texto
void initText()
{
	text = new Text(asset->get("smb2.txt"), asset->get("smb2.png"), renderer);
	debugText = new Text(asset->get("debug.txt"), asset->get("debug.png"), renderer);
}

// Inicializa el teclado en pantalla
void initOnScreenKeyboard()
{
	osk = new OnScreenKeyboard(renderer, input, asset->get("notify.png"), asset->get("smb2.png"), asset->get("smb2.txt"), options, options->screen.nativeWidth - 30, options->screen.nativeHeight - 80);
	osk->setBgColor({123, 99, 63});
	osk->setCaption("JAILER_ID");
	osk->setChars(USE_UPPER | USE_NUMBER);
}

// Inicializa el sprite
void initSprite()
{
	texture = new Texture(renderer, asset->get("z80.png"));
	sprite = new MovingSprite();
	sprite->setRenderer(renderer);
	sprite->setTexture(texture);
	sprite->setPosX(140);
	sprite->setPosY(100);
	sprite->setWidth(16);
	sprite->setHeight(32);
	sprite->setSpriteClip({0, 0, 16, 32});
	sprite->setVelX(1);
	sprite->setVelY(2);
}

// Inicializa el efecto de fuego
void initFire()
{
	// Inicializa el generador de numeros aleatorios
	srand((unsigned)SDL_GetTicks());

	// Inicializa el buffer de fuego
	for (int y = 0; y < fireHeight; ++y)
		for (int x = 0; x < fireWidth; ++x)
			fire[y][x] = 0;

	fireTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, fireWidth, fireHeight);

	// Inicializa la paleta
	for (int x = 0; x < 256; ++x)
	{
		// HSLtoRGB is used to generate colors:
		// Hue goes from 0 to 85: red to yellow
		// Saturation is always the maximum: 255
		// Lightness is 0..255 for x=0..128, and 255 for x=128..255
		ColorRGB color = HSLtoRGB(ColorHSL(x / 3, 255, std::min(255, x * 2)));

		// Pon en la paleta el color calculado
		palette[x] = (Uint32)16777216 * 255 + 65536 * color.r + 256 * color.g + color.b;
	}

	// Coloca algo de fuego en la textura
	for (int x = 0; x < 100; ++x)
		updateFire();
}

// Inicializa todo
void initAll(char *argv[])
{
	// Inicializa las opciones
	initOptions();

	// Inicializa la lista de recursos
	initAsset(argv);

	// Inicializa SDL
	initSDL();

	// Inicializa la ventana
	initWindow();

	// Inicializa el gestor de eventos
	initEvent();

	// Inicializa jail_audio
	initJailAudio();

	// Inicializa el objeto screen
	initScreen();

	// Inicializa el objeto input
	initInput();

	// Inicializa el texto
	initText();

	// Inicializa el teclado en pantalla
	initOnScreenKeyboard();

	// Inicializa el sprite
	initSprite();

	// Inicializa el efecto de fuego
	initFire();
}

// Comprueba el teclado y los eventos
void checkEvents()
{
	while (SDL_PollEvent(event))
	{
		if (event->type == SDL_QUIT)
		{
			should_exit = true;
			break;
		}
		if (event->type == SDL_KEYDOWN)
		{
			switch (event->key.keysym.scancode)
			{
			case SDL_SCANCODE_ESCAPE:
				should_exit = true;
				break;

			case SDL_SCANCODE_F1:
				screen->decWindowSize();
				break;

			case SDL_SCANCODE_F2:
				screen->incWindowSize();
				break;

			case SDL_SCANCODE_F3:
				--fireModifier;
				fireModifier = std::max(0, fireModifier);
				break;

			case SDL_SCANCODE_F4:
				++fireModifier;
				fireModifier = std::min(30, fireModifier);
				break;

			case SDL_SCANCODE_SPACE:
				++fx %= fxTotal;
				break;

			case SDL_SCANCODE_N:
				screen->showNotification("Ejemplo de notificacion", "con 2 lineas de texto", 0);
				break;

			default:
				break;
			}
		}
	}
}

// Comprueba el objeto input
void checkInput()
{
	inputPressed = "";
	if (input->checkInput(INPUT_LEFT))
	{
		inputPressed = "Izquierda";
	}
	if (input->checkInput(INPUT_RIGHT))
	{
		inputPressed = "Derecha";
	}
	if (input->checkInput(INPUT_UP))
	{
		inputPressed = "Arriba";
	}
	if (input->checkInput(INPUT_DOWN))
	{
		inputPressed = "Abajo";
	}
}

// Actualiza el sprite
void updateSprite()
{
	if (sprite->getPosX() + sprite->getWidth() > options->screen.nativeWidth or sprite->getPosX() < 0)
	{
		sprite->undoMoveX();
		int spr_direction = 1;
		int spr_force = 1;
		if (sprite->getVelX() > 0)
		{
			spr_direction = -1;
		}
		if (SDL_GetTicks() % 2 == 0)
		{
			spr_force = 2;
		}
		sprite->setVelX(spr_force * spr_direction);
		JA_PlaySound(sound);
	}

	if (sprite->getPosY() + sprite->getHeight() > options->screen.nativeHeight or sprite->getPosY() < 0)
	{
		sprite->undoMoveY();
		int spr_direction = 1;
		int spr_force = 1;
		if (sprite->getVelY() > 0)
		{
			spr_direction = -1;
		}
		if (SDL_GetTicks() % 2 == 0)
		{
			spr_force = 2;
		}
		sprite->setVelY(spr_force * spr_direction);
		JA_PlaySound(sound);
	}

	sprite->update();
}

// Actualiza el degradado
void updateGradient()
{
	if (counter % 1 == 0)
	{
		gradBreathDirection == 0 ? gradCurrentColor-- : gradCurrentColor++;
		if (gradCurrentColor == gradColorMin)
		{
			gradBreathDirection = 1;
		}
		if (gradCurrentColor == gradColorMax)
		{
			gradBreathDirection = 0;
		}
	}
}

// Actualiza el efecto de fuego
void updateFire()
{
	const int w = fireWidth;
	const int h = fireHeight;
	const int mod = 157 - fireModifier;

	// Bloquea la textura para poder escribir en los pixeles
	SDL_LockTexture(fireTexture, NULL, (void **)&fireBuffer, &pitch);

	// Crea la fila inferior del fuego con pixels aleatorios
	for (int x = 0; x < w; ++x)
	{
		fire[h - 1][x] = rand() % 256;
		fireBuffer[x + (h - 1) * 320] = palette[fire[h - 1][x]];
	}

	// Calcula los pixeles del efecto de fuego, desde la fila superior hasta la inferior
	for (int y = 0; y < h - 1; ++y)
		for (int x = 0; x < w; ++x)
		{
			fire[y][x] = ((fire[(y + 1) % h][(x - 1 + w) % w] + fire[(y + 1) % h][(x) % w] + fire[(y + 1) % h][(x + 1) % w] + fire[(y + 2) % h][(x) % w]) * 32) / mod;
			fireBuffer[x + y * 320] = palette[fire[y][x]];
		}

	// Desbloquea la textura
	SDL_UnlockTexture(fireTexture);
}

// Actualiza el efecto de fondo
void updateFX()
{
	switch (fx)
	{
	case 0: // Degradado
		updateGradient();
		break;

	case 1: // Fuego
		updateFire();
		break;

	default:
		break;
	}
}

// Actualiza la lógica del programa
void update()
{
	// Comprueba el teclado y los eventos
	checkEvents();

	// Comprueba el objeto input
	checkInput();

	// Actualiza la lógica del programa
	if (SDL_GetTicks() - ticks > ticksSpeed)
	{
		// Actualiza la variable
		ticks = SDL_GetTicks();

		// Incrementa el contador
		counter++;

		// Actualiza los objetos
		osk->update();
		screen->update();

		// Actualiza el sprite
		updateSprite();

		// Actualiza el efecto de fondo
		updateFX();
	}
}

// Dibuja un degradado de fondo
void renderGradient()
{
	const int gradFirstLine = options->screen.nativeHeight / 3;
	const int gradLastLine = options->screen.nativeHeight;
	const int gradNumLines = gradLastLine - gradFirstLine;
	const int gradColorFrom = 0;

	for (int i = gradFirstLine; i < gradLastLine; ++i)
	{
		float step = ((float)(i - gradFirstLine) / gradNumLines);
		int color = gradColorFrom + ((gradCurrentColor - gradColorFrom) * step);
		SDL_SetRenderDrawColor(renderer, color, 0x00, 0x00, 0xFF);
		SDL_RenderDrawLine(renderer, 0, i, options->screen.nativeWidth, i);
	}

	for (int i = gradLastLine; i > gradFirstLine; --i)
	{
		float step = ((float)(i - gradFirstLine) / gradNumLines);
		int alpha = 0 + ((255 - 0) * step);
		SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0xFF, alpha);
		SDL_RenderDrawLine(renderer, 0, gradLastLine - i, options->screen.nativeWidth, gradLastLine - i);
	}
}

// Dibuja el efecto de fuego
void renderFire()
{
	SDL_RenderCopy(renderer, fireTexture, &fireSource, NULL);
}

// Dibuja el efecto de fondo
void renderFX()
{
	switch (fx)
	{
	case 0: // Degradado
		renderGradient();
		break;

	case 1: // Fuego
		renderFire();
		break;

	default:
		break;
	}
}

// Dibuja el texto
void renderText()
{
	const string fxName = fx == 0 ? "Degradado" : "Fuego";
	text->setZoom(2);
	text->writeDX(TXT_CENTER | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize(), "Jail Engine DEMO", 1, {255, 255, 255}, 1, {0, 0, 192});
	text->disableZoom();
	text->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize() * 4, "2023 JailDesigner", 1, {240, 240, 240}, 1, {0, 0, 192});
	debugText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize() * 7, "Pulsa 'F1' o 'F2' para disminuir o aumentar la ventana", 1, {240, 240, 240}, 1, {0, 0, 192});
	debugText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize() * 9, "Pulsa 'ESPACIO' para cambiar el efecto de fondo: " + fxName, 1, {240, 240, 240}, 1, {0, 0, 192});
	if (fxName == "Fuego")
		debugText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize() * 11, "Pulsa 'F3' o 'F4' para modificar el fuego: " + to_string(fireModifier), 1, {240, 240, 240}, 1, {0, 0, 192});
	debugText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize() * 13, "Pulsa 'N' para mostrar una notificacion", 1, {240, 240, 240}, 1, {0, 0, 192});
	debugText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize() * 15, controllerName, 1, {240, 240, 240}, 1, {0, 0, 192});
	debugText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, text->getCharacterSize() * 17, inputPressed, 1, {240, 240, 240}, 1, {0, 0, 192});
	debugText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, options->screen.nativeWidth / 2, options->screen.nativeHeight - (text->getCharacterSize() * 2), "Pulsa 'ESCAPE' para terminar el programa", 1, {240, 240, 240}, 1, {0, 0, 192});
}

// Dibuja los elementos del programa en pantalla
void render()
{
	// Prepara el objeto screen para dibujar
	screen->start();
	screen->clean();

	// Dibuja el efecto de fondo
	renderFX();

	// Dibuja el sprite
	sprite->render();

	// Dinuja el texto
	renderText();

	// Dibuja el teclado en pantalla
	osk->render();

	// Vuelca el buffer en pantalla
	screen->blit();
}

// Libera la memoria reservada
void freeAll()
{
	// Finaliza el sprite
	if (sprite != nullptr)
		delete sprite;
	if (texture != nullptr)
		delete texture;

	// Finaliza el teclado en pantalla
	if (osk != nullptr)
		delete osk;

	// Finaliza el texto
	if (text != nullptr)
		delete text;
	if (debugText != nullptr)
		delete debugText;

	// Finaliza el objeto input
	if (input != nullptr)
		delete input;

	// Finaliza el objeto screen
	if (screen != nullptr)
		delete screen;

	// Finaliza jail_audio
	JA_DeleteSound(sound);
	JA_DeleteMusic(music);

	// Finaliza el objeto con la lista de recuros
	if (asset != nullptr)
		delete asset;

	// Finaliza las opciones
	if (options != nullptr)
		delete options;

	// Finaliza SDL y la ventana
	if (event != nullptr)
		delete event;
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_Quit();
}

int main(int argc, char *argv[])
{
	// Inicializa todo
	initAll(argv);

	// Bucle principal
	while (!should_exit)
	{
		// Actualiza la lógica del programa
		update();

		// Dibuja los elementos del programa en pantalla
		render();
	}

	// Libera la memoria reservada
	freeAll();

	return 0;
}