#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include "jfile.h"
#include <sys/stat.h>
#include <unistd.h>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
#include <vector>

#ifndef _WIN32
#include <pwd.h>
#endif

#define DEFAULT_FILENAME "data.jf2"
#define DEFAULT_FOLDER "data/"
#define CONFIG_FILENAME "config.txt"

/*	FORMAT DEL ARXIU .JF2

4 bytes				header			capçalera "PK2" (caracter 0 de final de cadena al final) (en realitat passe de ella, pero be...)
4 bytes				num_files		nombre d'arxius inclosos
4 bytes				toc_offset		en quina posició de l'arxiu està la taula de continguts

... Ara venen tots els arxius inclosos, uno darrere de l'altre.
... Quan acaben ve la taula de continguts. La taula te tantes entrades com arxius inclosos. Cada entrada te el següent format:

(per cert, toc_offset apunta ací)
4 bytes				offset			en quina posició del arxiu de recursos comença este arxiu
4 bytes				size			tamany d'este arxiu
1 byte				path_size		tamany de la cadena amb la ruta de l'arxiu
path_size bytes		path			ruta de l'arxiu original. La usem per a trobar el arxiu que ens demanen.

EXEMPLE SIMPLE:

	- Imaginem que volem incloure un arxiu "data/hola.txt" amb el contingut "HOLA", i un arxiu "data/adios.txt" amb el contingut "ADIOS":

OFFSET		CONTINGUT		TAMANY		DESCRIPCIÓ
0			"PK2"+0				4			La capçalera
4			2					4			nombre d'arxius
8			21					4			offset a la taula de continguts

	--- COMENCEN ELS ARXIUS INCLOSOS ---

12			HOLA				4			el contingut del primer arxiu
16			ADIOS				5			el contingut del segon arxiu

	--- COMENÇA LA TAULA DE CONTINGUTS ---

21			12					4			offset al primer arxiu
25			4					4			tamany del primer axiu
29			13					1			tamany de la ruta al primer arxiu
30			"data/hola.txt"		13			la ruta al primer arxiu
43			16					4			offset al primer arxiu
47			4					4			tamany del primer axiu
51			13					1			tamany de la ruta al primer arxiu
52			"data/adios.txt"	14			la ruta al primer arxiu


	- Es un exemple raro, perque ocupa mes la ruta al arxiu que l'arxiu en si, pero espere que la idea quede clara!

	Al principi se carrega la tabla de continguts en memòria, així el acces als arxius es ràpid.



	Y com funciona tot açò? pos per defecte va a intentar llegir tots els arxius de "data.jf2". Si no troba e l'arxiu, automaticament passa a
		buscar-ho tot en la carpeta "data" en arxius independents. En principi, si no se tenen requeriments diferents, no fa falta configurar res.



	Respecte al tema de l'arxiu de configuració, està montat de forma que se pot escriure i llegir valors asociats a una clau sense calfar-se el cap.
	En l'arxiu de configuració els valor se guarden de la forma:

	CLAU=VALOR

	Cada un en una linea. Si llegim i no existeix, torna cadena buida. Si escrivim i ja exisita, se reemplaça.
	Tot son valors de cadena. Si en realitat son números, tindràs que encarregar-te tu de la caonversió de cadena a numero o al reves.

*/

namespace file
{
	// Estructures
	// ===============================================================================================================================

	// Estructura que representa un arxiu en la tabla de continguts del arxiu de recursos
	struct file_t
	{
		std::string path; // Ruta relativa de l'arxiu
		uint32_t size;	  // Tamany de l'arxiu
		uint32_t offset;  // Offset a l'arxiu dins de l'arxiu de recursos
	};

	std::vector<file_t> toc; // vector que conté la taula de continguts de l'arxiu de recursos

	/* El std::map me fa coses rares, vaig a usar un good old std::vector amb una estructura key,value propia i au, que sempre funciona */
	struct keyvalue_t
	{
		std::string key, value;
	};

	// Variables
	// ===============================================================================================================================
	static std::string resource_filename = DEFAULT_FILENAME; // Nom de l'arxiu de recursos
	static std::string resource_folder = DEFAULT_FOLDER;   // Nom de la carpeta de recursos
	static int file_source = SOURCE_FILE;	   // D'on anem a pillar els recursos, arxiu o carpeta
	static std::string config_folder;		   // Nom de la carpeta on guardar la configuració
	std::vector<keyvalue_t> config;			   // Vector amb els valors guardats a l'arxiu de configuració

	// Funcions
	// ===============================================================================================================================

	// Estableix el nom de l'arxiu on estàn guardats els recursos (es "data.jf2" per defecte)
	void setResourceFilename(const std::string str)
	{
		resource_filename = str;
	}

	// Estableix el nom de la carpeta on estàn guardats els recursos (es "data" per defecte)
	void setResourceFolder(const std::string str)
	{
		resource_folder = str;
	}

	// Estableix d'on s'han de obtenir els recursos (arxius individuals dins d'una carpeta o arxiu de recursos)
	void setSource(const int src)
	{
		file_source = src % 2; // mod 2 de forma que sempre es un valor vàlid, 0 (arxiu) o 1 (carpeta)

		// Si volem que busque en carpeta i encara no haviem especificat una carpeta, usem la per defecte
		if (/*src == SOURCE_FOLDER && */resource_folder == "")
		{
			setResourceFolder(DEFAULT_FOLDER);
		}
	}

	// Carreguem la taula de continguts de l'arxiu de recursos
	bool getTableOfContents()
	{
		// Si encara no haviem especificat un arxiu de recursos, usem el arxiu de recursos per defecte
		if (resource_filename == "")
		{
			setResourceFilename(DEFAULT_FILENAME);
		}

		// Si l'arxiu de recursos existeix...
		std::ifstream fi(resource_filename, std::ios::binary);
		if (fi.is_open())
		{
			// Llegim la capçalera (controlar que siga correcta?)
			char header[4];
			fi.read(header, 4);

			// LLegim el nombre d'arxius i la posició de la taula de continguts
			uint32_t num_files, toc_offset;
			fi.read((char *)&num_files, 4);
			fi.read((char *)&toc_offset, 4);

			// Anem a la taula de continguts
			fi.seekg(toc_offset);

			// Per a cada arxiu inclos en l'arxiu de recursos...
			for (unsigned int i = 0; i < num_files; ++i)
			{
				// Llegim en quina posició està i quant ocupa
				uint32_t file_offset, file_size;
				fi.read((char *)&file_offset, 4);
				fi.read((char *)&file_size, 4);

				// Llegim la seua ruta
				uint8_t path_size;
				fi.read((char *)&path_size, 1);
				char file_name[path_size + 1];
				fi.read(file_name, path_size);
				file_name[path_size] = 0;
				std::string filename = file_name;

				// Y afegim les dades a la taula de continguts en memòria
				toc.push_back({filename, file_size, file_offset});
			}

			// Tanquem la paradeta i tornem true
			fi.close();
			return true;
		}
		else // Si no s'ha pogut llegir el arxiu de recursos, tornem false
		{
			return false;
		}
	}

	// Obté un "std::ifstream" al arxiu que se li demana, independentment de la font (arxius individual en carpeta, o arxiu de recursos)
	std::ifstream getFileStream(const std::string resourcename, int *filesize, const bool binary)
	{
		// Si tenim configurat agafar els recursos de arxiu, pero encara no tenim la taula de continguts carregada...
		if (file_source == SOURCE_FILE and toc.size() == 0)
		{
			// Si fallem al intentar carregar la taula de continguts de l'arxiu de recursos, canviem a pillar els recursos de carpeta
			if (not getTableOfContents())
			{
				setSource(SOURCE_FOLDER);
			}
		}

		std::ifstream f;

		// Si estem pillant els recursos de un arxiu de recursos...
		if (file_source == SOURCE_FILE)
		{
			// Busquem el recurs en la taula de continguts usant la ruta
			bool found = false;
			uint32_t count = 0;
			while (!found && count < toc.size())
			{
				found = (resource_folder + resourcename == toc[count].path);
				if (!found)
				{
					count++;
				}
			}

			// Si no trobem el recurs, petem el mame
			if (!found)
			{
				printf("ERROR FATAL: No s'ha trobat el recurs '%s' a l'arxiu de recursos '%s'\n", resourcename.c_str(), resource_filename.c_str());
				exit(1);
			}

			// Agafem el tamany del recurs de la taula de continguts
			if (filesize) *filesize = toc[count].size;

			// obrim l'arxiu de recursos
			f.open(resource_filename.c_str(), binary ? std::ios::binary : std::ios::in);
			if (!f.is_open()) // En el raruno cas de que a este altures pete al obrir el arxiu de recursos, petem el mame
			{
				printf("ERROR FATAL: No s'ha pogut obrir l'arxiu de recursos '%s'\n", resource_filename.c_str());
				exit(1);
			}

			// Anem a la posició on està el recurs que volem. Amb açò "f" ja està preparat per a ser tornar.
			// Ojo, realment estic tornant un FILE* al arxiu de recursos, pero ja apuntant al moment en que comença el recurs que volem.
			// Ho dic perque si fem fseek(f, 0, SEEK_SET) tornarà al principi de l'arxiu de recursos, no del recurs. Tindre-ho en compte.
			f.seekg(toc[count].offset);
		}
		else
		{
			// Si estem pillant els recursos de carpeta, simplement obrim el arxiu en questió i tornem el FILE* associat.
			f.open((resource_folder + resourcename), binary ? std::ios::binary : std::ios::in);
			if (f.rdstate() & std::ios_base::failbit)
			{
				printf("ERROR FATAL: No s'ha pogut obrir l'arxiu '%s/%s'\n", resource_folder.c_str(), resourcename.c_str());
				exit(1);
			}
			f.seekg(0, std::ios_base::end);
			if (filesize) *filesize = f.tellg();
			f.seekg(0, std::ios_base::beg);
		}

		// Tornar el punter FILE* al arxiu. OJO! Tenim que tancar-lo quan acabem amb ell
		return f;
	}

	// Obté un "FILE*" al arxiu que se li demana, independentment de la font (arxius individual en carpeta, o arxiu de recursos)
	FILE *getFilePointer(const std::string resourcename, int *filesize, const bool binary)
	{
		// Si tenim configurat agafar els recursos de arxiu, pero encara no tenim la taula de continguts carregada...
		if (file_source == SOURCE_FILE and toc.size() == 0)
		{
			// Si fallem al intentar carregar la taula de continguts de l'arxiu de recursos, canviem a pillar els recursos de carpeta
			if (not getTableOfContents())
			{
				setSource(SOURCE_FOLDER);
			}
		}

		FILE *f;

		// Si estem pillant els recursos de un arxiu de recursos...
		if (file_source == SOURCE_FILE)
		{
			// Busquem el recurs en la taula de continguts usant la ruta
			bool found = false;
			uint32_t count = 0;
			while (!found && count < toc.size())
			{
				found = (resource_folder + resourcename == toc[count].path);
				if (!found)
				{
					count++;
				}
			}

			// Si no trobem el recurs, petem el mame
			if (!found)
			{
				printf("ERROR FATAL: No s'ha trobat el recurs '%s' a l'arxiu de recursos '%s'\n", resourcename.c_str(), resource_filename.c_str());
				exit(1);
			}

			// Agafem el tamany del recurs de la taula de continguts
			if (filesize) *filesize = toc[count].size;

			// obrim l'arxiu de recursos
			f = fopen(resource_filename.c_str(), binary ? "rb" : "r");
			if (!f) // En el raruno cas de que a este altures pete al obrir el arxiu de recursos, petem el mame
			{
				printf("ERROR FATAL: No s'ha pogut obrir l'arxiu de recursos '%s'\n", resource_filename.c_str());
				exit(1);
			}

			// Anem a la posició on està el recurs que volem. Amb açò "f" ja està preparat per a ser tornar.
			// Ojo, realment estic tornant un FILE* al arxiu de recursos, pero ja apuntant al moment en que comença el recurs que volem.
			// Ho dic perque si fem fseek(f, 0, SEEK_SET) tornarà al principi de l'arxiu de recursos, no del recurs. Tindre-ho en compte.
			fseek(f, toc[count].offset, SEEK_SET);
		}
		else
		{
			// Si estem pillant els recursos de carpeta, simplement obrim el arxiu en questió i tornem el FILE* associat.
			f = fopen((resource_folder + resourcename).c_str(), binary ? "rb" : "r");
			if(!f)
			{
				printf("ERROR FATAL: No s'ha pogut obrir l'arxiu '%s/%s'\n", resource_folder.c_str(), resourcename.c_str());
				exit(1);
			}

			fseek(f, 0, SEEK_END);
			if (filesize) *filesize = ftell(f);
			fseek(f, 0, SEEK_SET);
		}

		// Tornar el punter FILE* al arxiu. OJO! Tenim que tancar-lo quan acabem amb ell
		return f;
	}

	// Obté un buffer de memòria en format "char*" del arxiu que se li demana, independentment de la font (arxius individual en carpeta, o arxiu de recursos)
	char *getFileBuffer(const std::string resourcename, int *filesize)
	{
		int size;
		// Usem la funció anterior per a obtinde un FILE*, independentment de on pillem els recursos
		FILE *f = getFilePointer(resourcename, &size, true);

		// Reservem memòria per al buffer
		char *buffer = (char *)malloc(size);

		// llegim el contingut del arxiu i el fiquem en el buffer
		fread(buffer, size, 1, f);

		// Tanquem l'arxiu
		fclose(f);

		if (filesize) *filesize = size;

		// Tornem el buffer. OJO! recordar alliberar-lo amb free(buffer) quan acabem amb ell.
		return buffer;
	}

	// Estableix el nom de la carpeta on es guardarà la configuració
	// Adaptat del codi que va escriure JailDesigner en el JailDoctor's Dilemma
	// Vull revisar-la tranquilament algun dia
	void setConfigFolder(const std::string foldername)
	{
#ifdef _WIN32
		config_folder = std::string(getenv("APPDATA")) + "/" + foldername;
#elif __APPLE__
		struct passwd *pw = getpwuid(getuid());
		const char *homedir = pw->pw_dir;
		config_folder = std::string(homedir) + "/Library/Application Support/" + foldername;
#elif __linux__
		struct passwd *pw = getpwuid(getuid());
		const char *homedir = pw->pw_dir;
		config_folder = std::string(homedir) + "/." + foldername;
#endif

		struct stat st = {0};
		if (stat(config_folder.c_str(), &st) == -1)
		{
#ifdef _WIN32
			int ret = mkdir(config_folder.c_str());
#else
			int ret = mkdir(config_folder.c_str(), S_IRWXU);
#endif

			if (ret == -1)
			{
				printf("ERROR CREATING CONFIG FOLDER.");
				exit(EXIT_FAILURE);
			}
		}
	}

	// Obté el nom de la carpeta on es guardarà la configuració
	const std::string getConfigFolder()
	{
		return config_folder + "/";
	}

	// Carrega tots els valors guardats a l'arxiu de recursos
	void loadConfigValues()
	{
		// Buidem la taula de claus-valors en memòria
		config.clear();

		// Obrim l'arxiu de configuració
		std::string config_file = config_folder + "/config.txt";
		FILE *f = fopen(config_file.c_str(), "r");
		if (f)
		{
			// Agafem linea a linea
			char line[1024];
			while (fgets(line, sizeof(line), f))
			{
				// Separem la clau del valor
				char *value = strchr(line, '=');
				if (value)
				{
					*value = '\0';
					value++;
					value[strlen(value) - 1] = '\0';

					// i els afegim a la taula de claus-valors en memòria
					config.push_back({line, value});
				}
			}

			// tanquem la paradeta
			fclose(f);
		}
	}

	// Guardem tots els valors en la taula de claus-valors a l'arxiu de configuració
	void saveConfigValues()
	{
		// Obrim l'arxiu de configuració
		std::string config_file = config_folder + "/config.txt";
		FILE *f = fopen(config_file.c_str(), "w");
		if (f)
		{
			// Guardem cada parella clau/valor de la taula en memòria en una linea en format "clau=valor\n" en l'arxiu de configuració
			for (auto pair : config)
			{
				fprintf(f, "%s=%s\n", pair.key.c_str(), pair.value.c_str());
			}

			// tanquem la paradeta
			fclose(f);
		}
	}

	// Obté un valor de l'arxiu de configuració per a la clau donada (o cadena buida si no existeix)
	const std::string getConfigValue(const std::string key)
	{
		// Si la taula de claus-valors esta buida, la carreguem de l'arxiu de configuració
		if (config.empty())
		{
			loadConfigValues();
		}

		// busquem la clau en la taula
		for (auto pair : config)
		{
			if (pair.key == std::string(key))
			{
				// Si la trobem, tornem el seu valor
				return pair.value;
			}
		}

		// Si no la trobem, tornem cadena buida
		return "";
	}

	// Estableix un valor en l'arxiu de configuració per a la clau donada
	void setConfigValue(const std::string key, const std::string value)
	{
		// Si la taula de claus-valors esta buida, la carreguem de l'arxiu de configuració
		if (config.empty())
		{
			loadConfigValues();
		}

		// busquem la clau en la taula
		for (auto &pair : config)
		{
			if (pair.key == std::string(key))
			{
				// Si la trobem, actualitzem el seu valor i guardem els canvis a l'arxiu de configuració
				pair.value = value;
				saveConfigValues();
				return;
			}
		}

		// Si NO la trobem, afegim la nova clau i el seu valor, i guardem els canvis a l'arxiu de configuració
		config.push_back({key, value});
		saveConfigValues();
	}
}