/*
* SACD Decoder plugin
* Copyright (c) 2011-2023 Maxim V.Anisiutkin <maxim.anisiutkin@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 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; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "sacd_metabase.h"
#include <array>
#include <filesystem>

auto MB_TAG_ROOT       = "root";
auto MB_TAG_STORE      = "store";
auto MB_TAG_TRACK      = "track";
auto MB_TAG_INFO       = "info";
auto MB_TAG_META       = "meta";
auto MB_TAG_REPLAYGAIN = "replaygain";
auto MB_TAG_ALBUMART   = "albumart";

auto MB_ATT_ID         = "id";
auto MB_ATT_NAME       = "name";
auto MB_ATT_TYPE       = "type";
auto MB_ATT_VALUE      = "value";
auto MB_ATT_VALSEP     = ";";
auto MB_ATT_VERSION    = "version";

auto METABASE_CATALOG  = "sacd_metabase";
auto METABASE_TYPE     = "SACD";
auto METABASE_VERSION  = "1.2";

static auto utf2xml = [](auto src) {
	auto dst{ std::string() };
	for (auto& ch : src) {
		switch (ch) {
		case '\r':
			dst += "&#13;";
			break;
		case '\n':
			dst += "&#10;";
			break;
		default:
			dst += ch;
			break;
		}
	}
	return dst;
};

static auto xml2utf = [](auto src) {
	auto dst{ std::string() };
	for (auto i = 0u; i < src.length(); i++) {
		if (src.compare(i, 5, "&#13;") == 0) {
			dst += '\r';
			i += 4;
		}
		else if (src.compare(i, 5, "&#10;") == 0) {
			if (!dst.empty()) {
				if (dst.back() != '\r') {
					dst += '\r';
				}
			}
			dst += '\n';
			i += 4;
		}
		else {
			dst += src[i];
		}
	}
	return dst;
};

static auto is_linkable_tag = [](auto tag_name) {
	return !((tag_name == "dynamic range") || (tag_name == "album dynamic range"));
};

static std::string get_md5(sacd_disc_t* p_disc) {
	auto md5_source{ make_unique<std::array<uint8_t, MASTER_TOC_LEN * SACD_LSN_SIZE>>() };
	auto md5_string{ std::string() };
	if (p_disc->read_blocks_raw(START_OF_MASTER_TOC, MASTER_TOC_LEN, md5_source.get()->data())) {
		md5_string = hasher_md5::get()->process_single(md5_source.get()->data(), md5_source.get()->size()).asString();
	}
	else {
		console::error("Cannot read MD5 hash source");
	}
	return md5_string;
}

sacd_metabase_t::sacd_metabase_t(sacd_disc_t* disc, const char* metafile) {
	initialized = false;
	store_id = get_md5(disc);
	if (store_id.empty()) {
		return;
	}
	store_path = core_api::get_profile_path();
	if (store_path.back() != std::filesystem::path::preferred_separator) {
		store_path += std::filesystem::path::preferred_separator;
	}
	store_path += METABASE_CATALOG;
	store_path += std::filesystem::path::preferred_separator;
	auto store_file = store_path;
	store_file += store_id;
	store_file += ".xml";
	if (!metafile) {
		if (!filesystem::g_exists(store_path.c_str(), fb2k::noAbort)) {
			filesystem::g_create_directory(store_path.c_str(), fb2k::noAbort);
			if (filesystem::g_exists(store_path.c_str(), fb2k::noAbort)) {
				console::info(string_printf("SACD metabase folder '%s' has been created", store_path.c_str()));
			}
			else {
				popup_message::g_show(string_printf("Cannot create SACD metabase folder '%s'", store_path.c_str()), METABASE_CATALOG, popup_message::icon_error);
			}
		}
	}
	else {
		if (!filesystem::g_exists(metafile, fb2k::noAbort)) {
			if (filesystem::g_exists(store_file.c_str(), fb2k::noAbort)) {
				filesystem::g_copy(store_file.c_str(), metafile, fb2k::noAbort);
				if (filesystem::g_exists(metafile, fb2k::noAbort)) {
					console::info(string_printf("SACD metabase file is copied from '%s' to '%s'", store_file.c_str(), metafile));
				}
				else {
					popup_message::g_show(string_printf("Cannot copy SACD metabase file to '%s'", metafile), METABASE_CATALOG, popup_message::icon_error);
				}
			}
		}
	}
	xml_file = filesystem::g_get_native_path(metafile ? metafile : store_file.c_str());
	initialized = init_xmldoc(METABASE_TYPE);
	if (!initialized) {
		console::error("Cannot initialize XML document");
	}
}

sacd_metabase_t::~sacd_metabase_t() {
}

void sacd_metabase_t::get_track_info(uint32_t track_number, file_info& track_info) {
	auto node_track{ get_node(MB_TAG_TRACK, std::to_string(track_number).c_str()) };
	if (!node_track) {
		return;
	}
	auto replaygain_info{ track_info.get_replaygain() };
	auto node_tag{ node_track.first_child() };
	while (node_tag) {
		auto node_name{ std::string(node_tag.name()) };
		auto tag_name{ node_tag.attribute(MB_ATT_NAME) };
		auto tag_value{ node_tag.attribute(MB_ATT_VALUE) };
		if (tag_name && tag_value) {
			auto att_name{ std::string(tag_name.value()) };
			auto att_value{ xml2utf(std::string(tag_value.value())) };
			if (node_name == MB_TAG_META) {
				auto head_chunk{ true };
				auto sep_pos{ size_t(0) };
				do {
					sep_pos = att_value.find_first_of(MB_ATT_VALSEP);
					auto tag_value_head{ std::string() };
					tag_value_head.append(att_value, 0, sep_pos);
					while (tag_value_head.length() > 0 && tag_value_head[0] == ' ') {
						tag_value_head.erase(0, 1);
					}
					if (head_chunk) {
						track_info.meta_set(att_name.c_str(), tag_value_head.c_str());
						head_chunk = false;
					}
					else {
						track_info.meta_add(att_name.c_str(), tag_value_head.c_str());
					}
					att_value.erase(0, sep_pos + 1);
				} while (sep_pos != ~0);
			}
			if (node_name == MB_TAG_REPLAYGAIN) {
				if (att_name == "replaygain_track_gain") {
					replaygain_info.set_track_gain_text(att_value.c_str());
				}
				if (att_name == "replaygain_track_peak") {
					replaygain_info.set_track_peak_text(att_value.c_str());
				}
				if (att_name == "replaygain_album_gain") {
					replaygain_info.set_album_gain_text(att_value.c_str());
				}
				if (att_name == "replaygain_album_peak") {
					replaygain_info.set_album_peak_text(att_value.c_str());
				}
			}
		}
		node_tag = node_tag.next_sibling();
	}
	track_info.set_replaygain(replaygain_info);
}

void sacd_metabase_t::set_track_info(uint32_t track_number, const file_info& track_info, bool is_linked) {
	auto node_track{ get_node(MB_TAG_TRACK, std::to_string(track_number).c_str(), true) };
	if (!node_track) {
		return;
	}
	delete_track_tags(node_track, MB_TAG_META, is_linked);
	for (auto i = 0u; i < track_info.meta_get_count(); i++) {
		auto tag_name{ std::string(track_info.meta_enum_name(i)) };
		auto tag_value{ std::string(track_info.meta_enum_value(i, 0)) };
		for (auto j = 1u; j < track_info.meta_enum_value_count(i); j++) {
			tag_value += MB_ATT_VALSEP;
			tag_value += track_info.meta_enum_value(i, j);
		}
		if (!is_linked || (is_linked && is_linkable_tag(tag_name.c_str()))) {
			insert_track_tag(node_track, MB_TAG_META, tag_name.c_str(), utf2xml(tag_value).c_str());
		}
	}
	if (!is_linked) {
		delete_track_tags(node_track, MB_TAG_REPLAYGAIN, false);
		auto replaygain_info{ track_info.get_replaygain() };
		replaygain_info::t_text_buffer tag_value;
		if (replaygain_info.is_track_gain_present()) {
			if (replaygain_info.format_track_gain(tag_value)) {
				insert_track_tag(node_track, MB_TAG_REPLAYGAIN, "replaygain_track_gain", tag_value);
			}
		}
		if (replaygain_info.is_track_peak_present()) {
			if (replaygain_info.format_track_peak(tag_value)) {
				insert_track_tag(node_track, MB_TAG_REPLAYGAIN, "replaygain_track_peak", tag_value);
			}
		}
		if (replaygain_info.is_album_gain_present()) {
			if (replaygain_info.format_album_gain(tag_value)) {
				insert_track_tag(node_track, MB_TAG_REPLAYGAIN, "replaygain_album_gain", tag_value);
			}
		}
		if (replaygain_info.is_album_peak_present()) {
			if (replaygain_info.format_album_peak(tag_value)) {
				insert_track_tag(node_track, MB_TAG_REPLAYGAIN, "replaygain_album_peak", tag_value);
			}
		}
	}
	auto list_tags{ node_track.first_child() };
	if (!list_tags) {
		auto node_store{ node_track.parent() };
		if (node_store) {
			node_store.remove_child(node_track.name());
		}
	}
}

void sacd_metabase_t::get_albumart(uint32_t albumart_id, std::vector<t_uint8>& albumart_data) {
	auto node_albumart{ get_node(MB_TAG_ALBUMART, std::to_string(albumart_id).c_str()) };
	if (!node_albumart) {
		return;
	}
	auto elem_cdata = node_albumart.first_child();
	if (elem_cdata) {
		auto cdata{ std::string(elem_cdata.value()) };
		pfc::array_t<uint8_t> albumart_decode;
		base64_decode_array(albumart_decode, cdata.c_str());
		albumart_data.assign(albumart_decode.get_ptr(), albumart_decode.get_ptr() + albumart_decode.get_size());
	}
}

void sacd_metabase_t::set_albumart(uint32_t albumart_id, const std::vector<t_uint8>& albumart_data) {
	auto create_node = albumart_data.size() > 0;
	auto node_albumart{ get_node(MB_TAG_ALBUMART, std::to_string(albumart_id).c_str(), create_node) };
	if (!node_albumart) {
		return;
	}
	if (create_node) {
		node_albumart.remove_children();
		pfc::string8 albumart_base64;
		base64_encode(albumart_base64, albumart_data.data(), albumart_data.size());
		node_albumart.append_child(node_cdata).set_value(albumart_base64.get_ptr());
	}
	else {
		node_albumart.parent().remove_child(node_albumart);
	}
}

void sacd_metabase_t::commit() {
	xml_doc.save_file(pfc::stringcvt::string_os_from_utf8(xml_file.c_str()), "\t", format_raw, encoding_utf8);
}

bool sacd_metabase_t::init_xmldoc(const char* store_type) {
	auto init_ok{ false };
	xml_doc.load_file(pfc::stringcvt::string_os_from_utf8(xml_file.c_str()), parse_full, encoding_utf8);
	auto elem_root{ xml_doc.child(MB_TAG_ROOT) };
	if (elem_root) {
		auto elem_store{ elem_root.child(MB_TAG_STORE) };
		if (elem_store) {
			init_ok = true;
		}
	}
	if (!init_ok) {
		xml_doc.reset();
		auto decl_root{ xml_doc.append_child(node_declaration) };
		decl_root.append_attribute("version").set_value("1.0");
		decl_root.append_attribute("encoding").set_value("utf-8");
		auto comm_root{ xml_doc.append_child(node_comment) };
		comm_root.set_value("SACD metabase file");
		auto elem_root{ xml_doc.append_child(node_element) };
		elem_root.set_name(MB_TAG_ROOT);
		auto elem_store{ elem_root.append_child(node_element) };
		elem_store.set_name(MB_TAG_STORE);
		elem_store.append_attribute(MB_ATT_ID).set_value(store_id.c_str());
		elem_store.append_attribute(MB_ATT_TYPE).set_value(store_type);
		elem_store.append_attribute(MB_ATT_VERSION).set_value(METABASE_VERSION);
		init_ok = true;
	}
	return init_ok;
}

void sacd_metabase_t::delete_track_tags(xml_node node_track, const char* tag_type, bool is_linked) {
	bool continue_to_remove{ true };
	while (continue_to_remove) {
		auto elem_tag{ node_track.first_child() };
		continue_to_remove = false;
		while (elem_tag) {
			auto tag_name{ std::string(elem_tag.name()) };
			if (tag_name == tag_type) {
				auto att_name{ elem_tag.attribute(MB_ATT_NAME) };
				if (att_name) {
					if (!is_linked || (is_linked && (tag_name == MB_TAG_META) && is_linkable_tag(tag_name))) {
						node_track.remove_child(elem_tag);
						continue_to_remove = true;
						break;
					}
				}
			}
			elem_tag = elem_tag.next_sibling();
		}
	}
}

void sacd_metabase_t::insert_track_tag(xml_node node_track, const char* tag_type, const char* tag_name, const char* tag_value) {
	auto elem_tag{ node_track.append_child(node_element) };
	elem_tag.set_name(tag_type);
	elem_tag.append_attribute(MB_ATT_NAME).set_value(tag_name);
	elem_tag.append_attribute(MB_ATT_VALUE).set_value(tag_value);
}

xml_node sacd_metabase_t::get_node(const char* tag_type, const char* att_id, bool create) {
	xml_node node_tag_type;
	auto elem_root{ xml_doc.child(MB_TAG_ROOT) };
	if (elem_root) {
		auto elem_store{ elem_root.child(MB_TAG_STORE) };
		if (elem_store) {
			auto elem_att_id{ elem_store.attribute(MB_ATT_ID) };
			auto elem_att_type{ elem_store.attribute(MB_ATT_TYPE) };
			if (elem_att_id && elem_att_type) {
				if ((std::string(elem_att_id.value()) == store_id) && (std::string(elem_att_type.value()) == METABASE_TYPE)) {
					auto elem_tag{ elem_store.first_child() };
					while (elem_tag) {
						auto elem_name{ std::string(elem_tag.name()) };
						if (elem_name == tag_type) {
							auto elem_att_id{ elem_tag.attribute(MB_ATT_ID) };
							if (elem_att_id) {
								if (std::string(elem_att_id.value()) == att_id) {
									node_tag_type = elem_tag;
									break;
								}
							}
						}
						elem_tag = elem_tag.next_sibling();
					}
				}
			}
		}
	}
	if (!node_tag_type && create) {
		node_tag_type = new_node(tag_type, att_id);
	}
	return node_tag_type;
}

xml_node sacd_metabase_t::new_node(const char* tag_type, const char* att_id) {
	auto elem_root{ xml_doc.child(MB_TAG_ROOT) };
	if (elem_root) {
		auto elem_store{ elem_root.child(MB_TAG_STORE) };
		if (elem_store) {
			auto elem_tag{ elem_store.append_child(node_element) };
			elem_tag.set_name(tag_type);
			elem_tag.append_attribute(MB_ATT_ID).set_value(att_id);
			return elem_tag;
		}
	}
	return xml_node{};
}
