#include "Deer/DataStore.h"
#include "Deer/Log.h"
#include "Deer/Tools/Path.h"

#include "cereal/archives/portable_binary.hpp"
#include "cereal/cereal.hpp"
#include "cereal/types/unordered_map.hpp"

#include "Deer/DataStore/DataStructure.h"
#include "Deer/DataStore/DataStructureSerialization.h"

#include <fstream>
#include <ostream>
#include <sstream>
#include <streambuf>
#include <unordered_map>

namespace Deer {
	namespace DataStore {
		Path rootPath = "./";
		std::unordered_map<std::string, DirectoryData> dirData_cache;
	} // namespace DataStore

	const DirectoryData& DataStore::getDirData(const Path& id,
	                                           const Path& subDir,
	                                           const char* extension) {
		std::string dirId = std::string(id) + "&" + std::string(subDir) + "&" +
		                    std::string(extension);
		if (dirData_cache.contains(dirId)) {
			return dirData_cache[dirId];
		}

		Path idPath = rootPath / id;

		Path searchPath = idPath / subDir;

		DirectoryData& dirData = dirData_cache[dirId];
		for (const auto& entry :
		     std::filesystem::directory_iterator(searchPath)) {
			if (entry.is_directory())
				dirData.dirs.push_back(entry.path().lexically_relative(idPath));
			else if (entry.path().extension() == extension) {
				Path ent = entry.path().lexically_relative(idPath);
				dirData.elements.push_back(ent.parent_path() / ent.stem());
			}
		}

		return dirData;
	}

	bool DataStore::loadFileData(const Path& id, const Path& name,
	                             uint8_t** data, uint32_t* size) {
		Path filePath = rootPath / id / name;
		std::ifstream file(filePath, std::ios::in | std::ios::binary);

		if (!file) {
			file.close();
			return false;
		}

		file.seekg(0, std::ios::end);
		*size = (size_t)file.tellg();
		file.seekg(0, std::ios::beg);

		*data = new uint8_t[*size];

		if (!file.read(reinterpret_cast<char*>(*data), *size)) {
			DEER_CORE_ERROR("Failed to read file: {0}",
			                filePath.generic_string().c_str());
			delete[] *data;
			return false;
		}

		file.close();
		return true;
	}

	bool DataStore::loadGlobalFileData(const Path& id, const Path& name,
	                                   uint8_t** data, uint32_t* size) {
		Path filePath = rootPath / id;
		for (auto& f :
		     std::filesystem::recursive_directory_iterator(filePath)) {
			if (f.path().stem() == name) {
				std::ifstream file(f.path(), std::ios::in | std::ios::binary);

				if (!file) {
					file.close();
					return false;
				}

				file.seekg(0, std::ios::end);
				*size = (size_t)file.tellg();
				file.seekg(0, std::ios::beg);

				*data = new uint8_t[*size];

				if (!file.read(reinterpret_cast<char*>(*data), *size)) {
					DEER_CORE_ERROR("Failed to read file: {0}",
					                filePath.generic_string().c_str());
					delete[] *data;
					return false;
				}

				file.close();
				return true;
			}
		}

		DEER_CORE_ERROR("File {0} not found", filePath.string().c_str());
		return false;
	}

	void DataStore::freeFileData(uint8_t* data) { delete[] data; }

	void DataStore::deleteFile(const Path& path) {
		Path filePath = rootPath / toLowerCasePath(path);
		std::filesystem::remove(filePath);
	}

	uint8_t* DataStore::readFile(const Path& path, uint32_t* size) {
		Path filePath = rootPath / path;
		std::ifstream file(filePath, std::ios::in | std::ios::binary);

		if (!file) {
			file.close();
			return nullptr;
		}

		file.seekg(0, std::ios::end);
		*size = (size_t)file.tellg();
		file.seekg(0, std::ios::beg);

		uint8_t* buffer = new uint8_t[*size];

		if (!file.read(reinterpret_cast<char*>(buffer), *size)) {
			DEER_CORE_ERROR("Failed to read file: {0}",
			                filePath.generic_string().c_str());
			delete[] buffer;
			return nullptr;
		}

		file.close();
		return buffer;
	}

	void DataStore::saveFile(const Path& path, uint8_t* data, uint32_t size) {
		Path filePath = rootPath / toLowerCasePath(path);
		std::filesystem::create_directories(filePath.parent_path());

		std::ofstream file(filePath, std::ios::out | std::ios::binary);

		DEER_CORE_ASSERT(file, "Error when writing file {0}",
		                 filePath.generic_string().c_str());

		file.write(reinterpret_cast<const char*>(data), size);
	}

	void DataStore::compressFiles(std::vector<Path> files, const Path& path) {
		std::unordered_map<Path, DataStructure> dataStructure;
		std::vector<uint8_t> combinedData;

		for (const Path& inputPath : files) {
			uint32_t fileSize = 0;
			uint8_t* fileData = readFile(inputPath, &fileSize);

			uint32_t start = combinedData.size();

			combinedData.insert(combinedData.end(), fileData,
			                    fileData + fileSize);
			dataStructure[inputPath] = DataStructure{.dataPath = inputPath,
			                                         .dataStart = start,
			                                         .dataSize = fileSize};

			delete[] fileData;
		}

		Path compressedPath = path;
		compressedPath += ".deer";
		Path metaPath = path;
		metaPath += ".deer.meta";

		std::stringstream buffer;
		{
			cereal::PortableBinaryOutputArchive archive(buffer);
			archive(dataStructure);
		}

		saveFile(compressedPath, combinedData.data(), combinedData.size());
		saveFile(metaPath, (uint8_t*)buffer.str().c_str(), buffer.str().size());
	}

	std::vector<Path> DataStore::getFiles(const Path& path, const std::string& extension) {
		std::vector<Path> files;
		Path lookPath = rootPath / path;

		for (const auto& entry :
		     std::filesystem::recursive_directory_iterator(lookPath)) {
			if (std::filesystem::is_regular_file(entry) &&
			    entry.path().extension() == extension) {
				files.push_back(entry.path().lexically_relative(rootPath));
			}
		}

		return files;
	}

	void DataStore::createFolder(const Path& path) {
		std::filesystem::create_directories(path);
	}
} // namespace Deer
