/*
	Copyright (C) 2008 - 2024
	by Iris Morelle <shadowm2006@gmail.com>
	Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
	Part of the Battle for Wesnoth Project https://www.wesnoth.org/

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY.

	See the COPYING file for more details.
*/

#include "addon/manager.hpp"

#include "filesystem.hpp"
#include "log.hpp"
#include "serialization/parser.hpp"
#include "serialization/schema_validator.hpp"
#include "game_version.hpp"
#include "wml_exception.hpp"

#include <boost/algorithm/string.hpp>

static lg::log_domain log_config("config");
#define ERR_CFG LOG_STREAM(err , log_config)
#define LOG_CFG LOG_STREAM(info, log_config)
#define WRN_CFG LOG_STREAM(warn, log_config)

static lg::log_domain log_filesystem("filesystem");
#define ERR_FS  LOG_STREAM(err , log_filesystem)

static lg::log_domain log_network("network");
#define ERR_NET LOG_STREAM(err , log_network)
#define LOG_NET LOG_STREAM(info, log_network)

namespace {
	std::string get_pbl_file_path(const std::string& addon_name)
	{
		const std::string& parentd = filesystem::get_addons_dir();
		// Allow .pbl files directly in the addon dir
		const std::string exterior = parentd + "/" + addon_name + ".pbl";
		const std::string interior = parentd + "/" + addon_name + "/_server.pbl";
		return filesystem::file_exists(exterior) ? exterior : interior;
	}

	inline std::string get_info_file_path(const std::string& addon_name)
	{
		return filesystem::get_addons_dir() + "/" + addon_name + "/_info.cfg";
	}
}

bool have_addon_in_vcs_tree(const std::string& addon_name)
{
	static const std::string parentd = filesystem::get_addons_dir();
	return
		filesystem::file_exists(parentd+"/"+addon_name+"/.svn") ||
		filesystem::file_exists(parentd+"/"+addon_name+"/.git") ||
		filesystem::file_exists(parentd+"/"+addon_name+"/.hg");
}

bool have_addon_pbl_info(const std::string& addon_name)
{
	return filesystem::file_exists(get_pbl_file_path(addon_name));
}

config get_addon_pbl_info(const std::string& addon_name, bool do_validate)
{
	config cfg;
	const std::string& pbl_path = get_pbl_file_path(addon_name);
	try {
		filesystem::scoped_istream stream = filesystem::istream_file(pbl_path);
		std::unique_ptr<schema_validation::schema_validator> validator;
		if(do_validate) {
			validator = std::make_unique<schema_validation::schema_validator>(filesystem::get_wml_location("schema/pbl.cfg"));
			validator->set_create_exceptions(true);
		}
		read(cfg, *stream, validator.get());
	} catch(const config::error& e) {
		throw invalid_pbl_exception(pbl_path, e.message);
	} catch(wml_exception& e) {
		auto msg = e.user_message;
		e.user_message += " in " + addon_name;
		boost::replace_all(e.dev_message, "<unknown>", filesystem::sanitize_path(pbl_path));
		e.show();
		throw invalid_pbl_exception(pbl_path, msg);
	}

	return cfg;
}

void set_addon_pbl_info(const std::string& addon_name, const config& cfg)
{
	filesystem::scoped_ostream stream = filesystem::ostream_file(get_pbl_file_path(addon_name));
	write(*stream, cfg);
}

bool have_addon_install_info(const std::string& addon_name)
{
	return filesystem::file_exists(get_info_file_path(addon_name));
}

void get_addon_install_info(const std::string& addon_name, config& cfg)
{
	const std::string& info_path = get_info_file_path(addon_name);
	filesystem::scoped_istream stream = filesystem::istream_file(info_path);
	try {
		// The parser's read() API would normally do this at the start. This
		// is a safeguard in case read() throws later
		cfg.clear();
		config envelope;
		read(envelope, *stream);
		if(auto info = envelope.optional_child("info")) {
			cfg = std::move(*info);
		}
	} catch(const config::error& e) {
		ERR_CFG << "Failed to read add-on installation information for '"
				<< addon_name << "' from " << info_path << ":\n"
				<< e.message;
	}
}

void write_addon_install_info(const std::string& addon_name, const config& cfg)
{
	LOG_CFG << "Writing version info for add-on '" << addon_name << "'";

	const auto& info_path = get_info_file_path(addon_name);
	auto out = filesystem::ostream_file(info_path);

	*out << "#\n"
		 << "# File automatically generated by Wesnoth to keep track\n"
		 << "# of version information on installed add-ons. DO NOT EDIT!\n"
		 << "#\n";

	config envelope;
	envelope.add_child("info", cfg);
	write(*out, envelope);
}

bool remove_local_addon(const std::string& addon)
{
	const std::string addon_dir = filesystem::get_addons_dir() + "/" + addon;

	LOG_CFG << "removing local add-on: " << addon;

	if(filesystem::file_exists(addon_dir) && !filesystem::delete_directory(addon_dir, true)) {
		ERR_CFG << "Failed to delete directory/file: " << addon_dir;
		ERR_CFG << "removal of add-on " << addon << " failed!";
		return false;
	}
	return true;
}

namespace {

enum ADDON_ENUM_CRITERIA
{
	ADDON_ANY,
	ADDON_HAS_PBL,
};

std::vector<std::string> enumerate_addons_internal(ADDON_ENUM_CRITERIA filter)
{
	std::vector<std::string> res;
	std::vector<std::string> addon_dirnames;

	const auto& addons_root = filesystem::get_addons_dir();
	filesystem::get_files_in_dir(addons_root, nullptr, &addon_dirnames);

	for(const auto& addon_name : addon_dirnames) {
		if(filesystem::file_exists(addons_root + "/" + addon_name + "/_main.cfg") &&
		   (filter != ADDON_HAS_PBL || have_addon_pbl_info(addon_name)))
		{
			res.emplace_back(addon_name);
		}
	}

	return res;
}

}

std::vector<std::string> available_addons()
{
	return enumerate_addons_internal(ADDON_HAS_PBL);
}

std::vector<std::string> installed_addons()
{
	return enumerate_addons_internal(ADDON_ANY);
}

std::map<std::string, std::string> installed_addons_and_versions()
{
	std::map<std::string, std::string> addons;

	for(const std::string& addon_id : installed_addons()) {
		if(have_addon_pbl_info(addon_id)) {
			try {
				// Just grabbing the version, so don't bother validating the pbl
				addons[addon_id] = get_addon_pbl_info(addon_id, false)["version"].str();
			} catch(const invalid_pbl_exception&) {
				addons[addon_id] = "Invalid pbl file, version unknown";
			}
		} else if(filesystem::file_exists(get_info_file_path(addon_id))) {
			config info_cfg;
			get_addon_install_info(addon_id, info_cfg);
			addons[addon_id] = !info_cfg.empty() ? info_cfg["version"].str() : "Unknown";
		} else {
			addons[addon_id] = "Unknown";
		}
	}
	return addons;
}

bool is_addon_installed(const std::string& addon_name)
{
	const std::string namestem = filesystem::get_addons_dir() + "/" + addon_name;
	return filesystem::file_exists(namestem + "/_main.cfg");
}

static inline bool IsCR(const char& c)
{
	return c == '\x0D';
}

static std::string strip_cr(std::string str, bool strip)
{
	if(!strip)
		return str;
	std::string::iterator new_end = std::remove_if(str.begin(), str.end(), IsCR);
	str.erase(new_end, str.end());
	return str;
}

static filesystem::blacklist_pattern_list read_ignore_patterns(const std::string& addon_name)
{
	const std::string parentd = filesystem::get_addons_dir();
	const std::string ign_file = parentd + "/" + addon_name + "/_server.ign";

	filesystem::blacklist_pattern_list patterns;
	LOG_CFG << "searching for .ign file for '" << addon_name << "'...";
	if (!filesystem::file_exists(ign_file)) {
		LOG_CFG << "no .ign file found for '" << addon_name << "'\n"
		        << "using default ignore patterns...";
		return filesystem::default_blacklist;
	}
	LOG_CFG << "found .ign file: " << ign_file;
	auto stream = filesystem::istream_file(ign_file);
	std::string line;
	while (std::getline(*stream, line)) {
		boost::trim(line);
		const std::size_t l = line.size();
		// .gitignore & WML like comments
		if (l == 0 || !line.compare(0,2,"# ")) continue;
		if (line[l - 1] == '/') { // directory; we strip the last /
			patterns.add_directory_pattern(line.substr(0, l - 1));
		} else { // file
			patterns.add_file_pattern(line);
		}
	}
	return patterns;
}

static void archive_file(const std::string& path, const std::string& fname, config& cfg)
{
	cfg["name"] = fname;
	const bool is_cfg = (fname.size() > 4 ? (fname.substr(fname.size() - 4) == ".cfg") : false);
	cfg["contents"] = encode_binary(strip_cr(filesystem::read_file(path + '/' + fname),is_cfg));
}

static void archive_dir(const std::string& path, const std::string& dirname, config& cfg, const filesystem::blacklist_pattern_list& ignore_patterns)
{
	cfg["name"] = dirname;
	const std::string dir = path + '/' + dirname;

	std::vector<std::string> files, dirs;
	filesystem::get_files_in_dir(dir,&files,&dirs);
	for(const std::string& name : files) {
		bool valid = !filesystem::looks_like_pbl(name) && !ignore_patterns.match_file(name);
		if (valid) {
			archive_file(dir,name,cfg.add_child("file"));
		}
	}

	for(const std::string& name : dirs) {
		bool valid = !ignore_patterns.match_dir(name);
		if (valid) {
			archive_dir(dir,name,cfg.add_child("dir"),ignore_patterns);
		}
	}
}

void archive_addon(const std::string& addon_name, config& cfg)
{
	const std::string parentd = filesystem::get_addons_dir();

	filesystem::blacklist_pattern_list ignore_patterns(read_ignore_patterns(addon_name));
	archive_dir(parentd, addon_name, cfg.add_child("dir"), ignore_patterns);
}

static void unarchive_file(const std::string& path, const config& cfg)
{
	filesystem::write_file(path + '/' + cfg["name"].str(), unencode_binary(cfg["contents"]));
}

static void unarchive_dir(const std::string& path, const config& cfg, std::function<void()> file_callback = {})
{
	std::string dir;
	if (cfg["name"].empty())
		dir = path;
	else
		dir = path + '/' + cfg["name"].str();

	filesystem::make_directory(dir);

	for(const config &d : cfg.child_range("dir")) {
		unarchive_dir(dir, d, file_callback);
	}

	for(const config &f : cfg.child_range("file")) {
		unarchive_file(dir, f);
		if(file_callback) {
			file_callback();
		}
	}
}

static unsigned count_pack_files(const config& cfg)
{
	unsigned count = 0;

	for(const config& d : cfg.child_range("dir")) {
		count += count_pack_files(d);
	}

	return count + cfg.child_count("file");
}

void unarchive_addon(const config& cfg, std::function<void(unsigned)> progress_callback)
{
	const std::string parentd = filesystem::get_addons_dir();
	unsigned file_count = progress_callback ? count_pack_files(cfg) : 0, done = 0;
	auto file_callback = progress_callback
		? [&]() { progress_callback(++done * 100.0 / file_count); }
		: std::function<void()>{};
	unarchive_dir(parentd, cfg, file_callback);
}

static void purge_dir(const std::string& path, const config& removelist)
{
	std::string dir;
	if(removelist["name"].empty())
		dir = path;
	else
		dir = path + '/' + removelist["name"].str();

	if(!filesystem::is_directory(dir)) {
		return;
	}

	for(const config& d : removelist.child_range("dir")) {
		purge_dir(dir, d);
	}

	for(const config& f : removelist.child_range("file")) {
		filesystem::delete_file(dir + '/' + f["name"].str());
	}

	if(filesystem::dir_size(dir) < 1) {
		filesystem::delete_directory(dir);
	}
}

void purge_addon(const config& removelist)
{
	const std::string parentd = filesystem::get_addons_dir();
	purge_dir(parentd, removelist);
}

namespace {
	std::map< std::string, version_info > version_info_cache;
} // end unnamed namespace 5

void refresh_addon_version_info_cache()
{
	version_info_cache.clear();

	LOG_CFG << "refreshing add-on versions cache";

	const std::vector<std::string>& addons = installed_addons();
	if(addons.empty()) {
		return;
	}

	std::vector<std::string> addon_info_files(addons.size());

	std::transform(addons.begin(), addons.end(),
	               addon_info_files.begin(), get_info_file_path);

	for(std::size_t i = 0; i < addon_info_files.size(); ++i) {
		assert(i < addons.size());

		const std::string& addon = addons[i];
		const std::string& info_file = addon_info_files[i];

		if(filesystem::file_exists(info_file)) {
			config info_cfg;
			get_addon_install_info(addon, info_cfg);

			if(info_cfg.empty()) {
				continue;
			}

			const std::string& version = info_cfg["version"].str();
			LOG_CFG << "cached add-on version: " << addon << " [" << version << "]";

			version_info_cache[addon] = version;
		} else if (!have_addon_pbl_info(addon) && !have_addon_in_vcs_tree(addon)) {
			// Don't print the warning if the user is clearly the author
			WRN_CFG << "add-on '" << addon << "' has no _info.cfg; cannot read version info";
		}
	}
}

version_info get_addon_version_info(const std::string& addon)
{
	static const version_info nil;
	std::map< std::string, version_info >::iterator entry = version_info_cache.find(addon);
	return entry != version_info_cache.end() ? entry->second : nil;
}
