/*
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 "server/mod_manager.hpp"

#include <scriptstdstring/scriptstdstring.h>
#include <filesystem>
#include <fstream>
#include <cerrno>
#include <cstring>
#include <iostream>

#include "common/logger.hpp"
#include "common/resource_id_parser.hpp"
#include "server/mod_api.hpp"
#include "server/server.hpp"

using namespace polygun::server;

ModManager* polygun::server::g_current_modmanager = nullptr;

static void message_callback(const asSMessageInfo* msg, void* param) {
	switch(msg->type) {
	case asMSGTYPE_ERROR:
		LOG_ERROR("%s(%d, %d): %s", msg->section, msg->row, msg->col, msg->message);
		break;
	case asMSGTYPE_WARNING:
		LOG_WARNING("%s(%d, %d): %s", msg->section, msg->row, msg->col, msg->message);
		break;
	case asMSGTYPE_INFORMATION:
		LOG_INFO("%s(%d, %d): %s", msg->section, msg->row, msg->col, msg->message);
		break;
	}
}

ModManager::ModManager(Server& server) :
	m_server(server),
	m_engine(asCreateScriptEngine()),
	m_context(m_engine->CreateContext()),
	m_modules(),
	m_current_module(nullptr),
	m_current_node_id(1)
{
	int error;
	if((error=m_engine->SetMessageCallback(asFUNCTION(message_callback), nullptr, asCALL_CDECL))<0)
		LOG_FATAL("Failed to set AngelScript message callback (error code: %d)", error);
	RegisterStdString(m_engine);

	g_current_modmanager = this;

	init_api();
}

ModManager::~ModManager() {
	m_context->Release();
	m_engine->ShutDownAndRelease();
}

void ModManager::enumerate_modules() {
	for(const std::string& folder : m_server.get_config().m_module_search_path) {
		for(const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(folder)) {
			if(entry.is_directory()) {
				const std::string mod_name = entry.path().stem().string();
				Mod mod(*this, mod_name, folder+"/"+mod_name);
				m_modules.push_back(mod);
			}
		}
	}
}

void ModManager::run_modules() {
	for(Mod& mod : m_modules)
		mod.execute();
}

bool ModManager::load_mod_resource(const std::string& name, std::vector<uint8_t>& output) {
	utils::ResourceIDParser parser;
	if(!parser.parse(name))
		return false;
	std::string mod_path;
	if(!get_mod_path_by_name(parser.get_mod_name(), mod_path)) {
		LOG_ERROR("Failed to get mod '%s' path", parser.get_mod_name().c_str());
		return false;
	}
	std::string resource_path = mod_path+"/"+parser.get_resource_subpath();
	// Load file data
	std::ifstream file(resource_path, std::ios::binary);
	if(!file.good()) {
		LOG_ERROR("Failed to open resource file '%s': '%s'", resource_path.c_str(), strerror(errno));
		return false;
	}
	file.seekg(0, std::ios_base::end);
	std::streampos file_size = file.tellg();
	file.seekg(0);
	char* file_data = new char[file_size];
	file.read(file_data, file_size);
	output.assign(file_data, file_data+file_size);
	delete[] file_data;
	file.close();
	return true;
}

bool ModManager::get_mod_path_by_name(const std::string& name, std::string& output) {
	for(const Mod& mod : m_modules) {
		if(mod.get_name()==name) {
			output = mod.get_mod_path();
			return true;
		}
	}
	return false;
}

void ModManager::init_api() {
	int error;
	// Register API property
	if((error=m_engine->SetDefaultNamespace("polygun"))<0)
		LOG_FATAL("Failed to register 'polygun' namespace (error code: %d)", error);

	// Logging
	register_api_function("void log_info(const string& in)", asFUNCTION(mod_api::log_info));
	register_api_function("void log_warning(const string& in)", asFUNCTION(mod_api::log_warning));
	register_api_function("void log_error(const string& in)", asFUNCTION(mod_api::log_error));
	register_api_function("void log_fatal(const string& in)", asFUNCTION(mod_api::log_fatal));
	register_api_function("void log_verbose(const string& in)", asFUNCTION(mod_api::log_verbose));

	// Registering nodes
	if((error=m_engine->RegisterObjectType("cNode", 0, asOBJ_REF))<0)
		LOG_FATAL("Failed to register 'cNode' class (error code: %d)", error);
	if((error=m_engine->RegisterObjectBehaviour("cNode", asBEHAVE_FACTORY, "cNode @f()", asFUNCTION(mod_api::Node::create), asCALL_CDECL))<0)
		LOG_FATAL("Failed to register factory 'cNode' behaviour (error code: %d)", error);
	if((error=m_engine->RegisterObjectBehaviour("cNode", asBEHAVE_ADDREF, "void f()", asMETHOD(mod_api::Node, reference), asCALL_THISCALL))<0)
		LOG_FATAL("Failed to register addref 'cNode' behaviour (error code: %d)", error);
	if((error=m_engine->RegisterObjectBehaviour("cNode", asBEHAVE_RELEASE, "void f()", asMETHOD(mod_api::Node, release), asCALL_THISCALL))<0)
		LOG_FATAL("Failed to register release 'cNode' behaviour (error code: %d)", error);
	register_object_method("cNode", "string get_texture_filename()", asMETHOD(mod_api::Node, get_texture_filename));
	register_object_method("cNode", "string get_display_icon()", asMETHOD(mod_api::Node, get_display_icon));
	register_object_method("cNode", "string get_top_texture_filename()", asMETHOD(mod_api::Node, get_top_texture_filename));
	register_object_method("cNode", "string get_bottom_texture_filename()", asMETHOD(mod_api::Node, get_bottom_texture_filename));
	register_object_method("cNode", "string get_left_texture_filename()", asMETHOD(mod_api::Node, get_left_texture_filename));
	register_object_method("cNode", "string get_right_texture_filename()", asMETHOD(mod_api::Node, get_right_texture_filename));
	register_object_method("cNode", "string get_front_texture_filename()", asMETHOD(mod_api::Node, get_front_texture_filename));
	register_object_method("cNode", "string get_back_texture_filename()", asMETHOD(mod_api::Node, get_back_texture_filename));
	register_api_function("void register_node(const string& in, const cNode& in)", asFUNCTION(mod_api::register_node));
}

void ModManager::register_api_function(const std::string& signature, const asSFuncPtr& ptr) {
	int error;
	if((error=m_engine->RegisterGlobalFunction(signature.c_str(), ptr, asCALL_GENERIC))<0)
		LOG_FATAL("Failed to register API function '%s' (error code: %d)", signature.c_str(), error);
}

void ModManager::register_object_method(const std::string& class_name, const std::string& signature, const asSFuncPtr& ptr) {
	int error;
	if((error=m_engine->RegisterObjectMethod(class_name.c_str(), signature.c_str(), ptr, asCALL_THISCALL))<0)
		LOG_FATAL("Failed to register method '%s' for class '%s' (error code: %d)", error, signature.c_str(), class_name.c_str(), error);
}
