/*
PolyGun

Copyright (c) 2023 mrkubax10 <mrkubax10@onet.pl>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include "game/engine/resource_manager.hpp"

#include <filesystem>
#include <fstream>

#include "common/fs.hpp"
#include "common/logger.hpp"
#include "common/resource_id_parser.hpp"
#include "common/network/network_manager.hpp"
#include "game/renderer/master_renderer.hpp"
#include "game/renderer/texture.hpp"

using namespace polygun::engine;

ResourceManager::ResourceManager(renderer::MasterRenderer* master_renderer, std::mutex& network_mutex) :
	ThreadSafe(),
	m_user_dir(utils::get_user_dir()),
	m_master_renderer(master_renderer),
	m_network_manager(nullptr),
	m_network_mutex(network_mutex),
	m_notifiers(),
	m_textures(),
	m_surfaces(),
	m_empty_texture(master_renderer->create_texture()),
	m_empty_surface(),
	m_pending_resources(),
	m_validated_resources()
{
	const std::string empty_path = "res/textures/empty.png";
	m_empty_texture->load_from_file(empty_path);
	m_empty_surface.load_from_file(empty_path);
}

ResourceManager::~ResourceManager() {
	for(const auto& [key, val] : m_textures)
		delete val.m_resource;
	delete m_empty_texture;
}

void ResourceManager::on_packet(network::NetworkPacket& packet) {
	std::string resource_id;
	switch(packet.get_id()) {
	case network::NetworkPacketID::PACKET_RESOURCE:
		if(packet.read(resource_id)) {
			std::vector<PendingResource>::iterator it = std::find_if(m_pending_resources.begin(), m_pending_resources.end(), [resource_id](const PendingResource& element) {
				return element.m_resource_id==resource_id;
			});
			if(it==m_pending_resources.end())
				break;
			utils::ResourceIDParser parser;
			if(!parser.parse(resource_id))
				break;
			std::vector<uint8_t> resource_data;
			if(!packet.read(resource_data))
				break;
			const std::string folder_path = m_user_dir+"/cache/"+parser.get_resource_folder();
			std::filesystem::create_directories(folder_path);
			const std::string full_path = m_user_dir+"/cache/"+parser.get_resource_path();
			if(resource_data.size()>0) {
				std::ofstream file(full_path, std::ios::binary);
				if(!file.good()) {
					LOG_ERROR("Failed to open '%s' for writing", full_path.c_str());
					break;
				}
				file.write(reinterpret_cast<const char*>(resource_data.data()), resource_data.size());
				file.close();
			}
			m_notifiers[it->m_notifier_name] = true;
			m_pending_resources.erase(it);
			m_validated_resources.push_back(resource_id);
			LOG_VERBOSE("Fetched and saved resource '%s'", resource_id.c_str());
		}
		break;
	case network::NetworkPacketID::PACKET_RESOURCE_CHECKSUM:
		// TODO: Implement checksum algorithm
		break;
	}
}

polygun::renderer::Texture* ResourceManager::get_texture(const std::string& resource_id, const std::string& notifier_name) {
	acquire();

	utils::ResourceIDParser parser;
	if(!parser.parse(resource_id)) {
		release();
		return m_empty_texture;
	}

	// Check if resource is already pending
	std::vector<PendingResource>::iterator it = std::find_if(m_pending_resources.begin(), m_pending_resources.end(), [resource_id](const PendingResource& element) {
		return element.m_resource_id==resource_id;
	});
	if(it!=m_pending_resources.end()) {
		release();
		return m_empty_texture;
	}

	// Check if resource is loaded
	if(m_textures.count(resource_id)) {
		LoadedResource<renderer::Texture*>& resource = m_textures[resource_id];
		resource.m_last_used = std::chrono::steady_clock::now();
		release();
		return resource.m_resource;
	}

	// Check if requested texture is from builtin mod
	if(parser.get_mod_name()=="builtin") {
		renderer::Texture* texture = m_master_renderer->create_texture();
		texture->load_from_file("res/"+parser.get_resource_subpath());
		LoadedResource<renderer::Texture*>& loaded_resource = m_textures[resource_id];
		loaded_resource.m_last_used = std::chrono::steady_clock::now();
		loaded_resource.m_resource = texture;
		m_validated_resources.push_back(resource_id);
		LOG_VERBOSE("Loaded texture '%s'", resource_id.c_str());
		release();
		return texture;
	}

	// Check if resource is in cache
	const std::string resource_path = m_user_dir+"/cache/"+parser.get_resource_path();
	std::ifstream file(resource_path, std::ios::binary);
	if(file.good()) {
		// TODO: Send request to server for resource checksum
		// For now just assume that it didn't change since downloading
		file.close();
		renderer::Texture* texture = m_master_renderer->create_texture();
		texture->load_from_file(resource_path);
		LoadedResource<renderer::Texture*>& loaded_resource = m_textures[resource_id];
		loaded_resource.m_last_used = std::chrono::steady_clock::now();
		loaded_resource.m_resource = texture;
		m_validated_resources.push_back(resource_id);
		LOG_VERBOSE("Loaded texture '%s'", resource_id.c_str());
		release();
		return texture;
	}

	// Check if resource is validated (server sent empty resource)
	std::vector<std::string>::iterator it2 = std::find(m_validated_resources.begin(), m_validated_resources.end(), resource_id);
	if(it2!=m_validated_resources.end()) {
		release();
		return m_empty_texture;
	}

	// Send resource request to server
	m_network_mutex.lock();
	network::NetworkPacket packet = m_network_manager->create_packet(network::NetworkPacketFlag::FLAG_RELIABLE, network::NetworkPacketID::PACKET_RESOURCE);
	packet.write(resource_id);
	m_network_manager->send_packet(packet);
	m_network_manager->update();
	m_network_mutex.unlock();
	m_pending_resources.push_back(PendingResource{resource_id, notifier_name});

	release();
	return m_empty_texture;
}

polygun::renderer::Surface& ResourceManager::get_surface(const std::string& resource_id, const std::string& notifier_name) {
	acquire();

	utils::ResourceIDParser parser;
	if(!parser.parse(resource_id)) {
		release();
		return m_empty_surface;
	}

	// Check if resource is already pending
	std::vector<PendingResource>::iterator it = std::find_if(m_pending_resources.begin(), m_pending_resources.end(), [resource_id](const PendingResource& element) {
		return element.m_resource_id==resource_id;
	});
	if(it!=m_pending_resources.end()) {
		release();
		return m_empty_surface;
	}

	// Check if resource is loaded
	if(m_textures.count(resource_id)) {
		LoadedResource<renderer::Surface>& resource = m_surfaces[resource_id];
		resource.m_last_used = std::chrono::steady_clock::now();
		release();
		return resource.m_resource;
	}

	// Check if requested texture is from builtin mod
	if(parser.get_mod_name()=="builtin") {
		LoadedResource<renderer::Surface>& loaded_resource = m_surfaces[resource_id];
		loaded_resource.m_resource.load_from_file("res/"+parser.get_resource_subpath());
		loaded_resource.m_last_used = std::chrono::steady_clock::now();
		m_validated_resources.push_back(resource_id);
		LOG_VERBOSE("Loaded surface '%s'", resource_id.c_str());
		release();
		return loaded_resource.m_resource;
	}

	// Check if resource is in cache
	const std::string resource_path = m_user_dir+"/cache/"+parser.get_resource_path();
	std::ifstream file(resource_path, std::ios::binary);
	if(file.good()) {
		// TODO: Send request to server for resource checksum
		// For now just assume that it didn't change since downloading
		file.close();
		LoadedResource<renderer::Surface>& loaded_resource = m_surfaces[resource_id];
		loaded_resource.m_resource.load_from_file(resource_path);
		loaded_resource.m_last_used = std::chrono::steady_clock::now();
		m_validated_resources.push_back(resource_id);
		LOG_VERBOSE("Loaded surface '%s'", resource_id.c_str());
		release();
		return loaded_resource.m_resource;
	}

	// Check if resource is validated (server sent empty resource)
	std::vector<std::string>::iterator it2 = std::find(m_validated_resources.begin(), m_validated_resources.end(), resource_id);
	if(it2!=m_validated_resources.end()) {
		release();
		return m_empty_surface;
	}

	// Send resource request to server
	m_network_mutex.lock();
	network::NetworkPacket packet = m_network_manager->create_packet(network::NetworkPacketFlag::FLAG_RELIABLE, network::NetworkPacketID::PACKET_RESOURCE);
	packet.write(resource_id);
	m_network_manager->send_packet(packet);
	m_network_manager->update();
	m_network_mutex.unlock();
	m_pending_resources.push_back(PendingResource{resource_id, notifier_name});

	release();
	return m_empty_surface;
}

bool ResourceManager::is_notified(const std::string& notifier_name) {
	acquire();
	if(m_notifiers[notifier_name]) {
		m_notifiers[notifier_name] = false;
		release();
		return true;
	}
	release();
	return false;
}

bool ResourceManager::is_pending() {
	acquire();
	bool result = !m_pending_resources.empty();
	release();
	return result;
}
