#include "XMLElement.hh"
#include "StringOp.hh"
#include "FileContext.hh" // for bwcompat
#include "ConfigException.hh"
#include "ranges.hh"
#include "serialize.hh"
#include "serialize_stl.hh"
#include "stl.hh"
#include "strCat.hh"
#include "unreachable.hh"
#include "xrange.hh"
#include <cassert>

using std::string;
using std::string_view;
using std::unique_ptr;

namespace openmsx {

XMLElement& XMLElement::addChild(string childName)
{
	return children.emplace_back(std::move(childName));
}
XMLElement& XMLElement::addChild(string childName, string childData)
{
	return children.emplace_back(std::move(childName), std::move(childData));
}

void XMLElement::removeChild(const XMLElement& child)
{
	children.erase(rfind_if_unguarded(children,
		[&](auto& v) { return &v == &child; }));
}

XMLElement::Attributes::iterator XMLElement::getAttributeIter(string_view attrName)
{
	return ranges::find_if(attributes,
	                       [&](auto& a) { return a.first == attrName; });
}
XMLElement::Attributes::const_iterator XMLElement::getAttributeIter(string_view attrName) const
{
	return ranges::find_if(attributes,
	                       [&](auto& a) { return a.first == attrName; });
}

const string* XMLElement::findAttribute(string_view attrName) const
{
	auto it = getAttributeIter(attrName);
	return (it != end(attributes)) ? &it->second : nullptr;
}

void XMLElement::addAttribute(string attrName, string value)
{
	assert(!hasAttribute(attrName));
	attributes.emplace_back(std::move(attrName), std::move(value));
}

void XMLElement::setAttribute(string_view attrName, string value)
{
	auto it = getAttributeIter(attrName);
	if (it != end(attributes)) {
		it->second = std::move(value);
	} else {
		attributes.emplace_back(attrName, std::move(value));
	}
}

void XMLElement::removeAttribute(string_view attrName)
{
	auto it = getAttributeIter(attrName);
	if (it != end(attributes)) {
		attributes.erase(it);
	}
}

std::vector<const XMLElement*> XMLElement::getChildren(string_view childName) const
{
	std::vector<const XMLElement*> result;
	for (auto& c : children) {
		if (c.getName() == childName) {
			result.push_back(&c);
		}
	}
	return result;
}

XMLElement* XMLElement::findChild(string_view childName)
{
	auto it = ranges::find_if(
	        children, [&](auto& c) { return c.getName() == childName; });
	return (it != end(children)) ? &*it : nullptr;
}
const XMLElement* XMLElement::findChild(string_view childName) const
{
	return const_cast<XMLElement*>(this)->findChild(childName);
}

const XMLElement* XMLElement::findNextChild(string_view childName,
	                                    size_t& fromIndex) const
{
	for (auto i : xrange(fromIndex, children.size())) {
		if (children[i].getName() == childName) {
			fromIndex = i + 1;
			return &children[i];
		}
	}
	for (auto i : xrange(fromIndex)) {
		if (children[i].getName() == childName) {
			fromIndex = i + 1;
			return &children[i];
		}
	}
	return nullptr;
}

XMLElement* XMLElement::findChildWithAttribute(string_view childName,
	string_view attrName, string_view attValue)
{
	auto it = ranges::find_if(children, [&](auto& c) {
		if (c.getName() != childName) return false;
		auto* value = c.findAttribute(attrName);
		if (!value) return false;
		return *value == attValue;
	});
	return (it != end(children)) ? &*it : nullptr;
}

const XMLElement* XMLElement::findChildWithAttribute(string_view childName,
	string_view attrName, string_view attValue) const
{
	return const_cast<XMLElement*>(this)->findChildWithAttribute(
		childName, attrName, attValue);
}

XMLElement& XMLElement::getChild(string_view childName)
{
	if (auto* elem = findChild(childName)) {
		return *elem;
	}
	throw ConfigException("Missing tag \"", childName, "\".");
}
const XMLElement& XMLElement::getChild(string_view childName) const
{
	return const_cast<XMLElement*>(this)->getChild(childName);
}

XMLElement& XMLElement::getCreateChild(string_view childName,
                                       string_view defaultValue)
{
	if (auto* result = findChild(childName)) {
		return *result;
	}
	return addChild(string(childName), string(defaultValue));
}

XMLElement& XMLElement::getCreateChildWithAttribute(
	string_view childName, string_view attrName,
	string_view attValue)
{
	if (auto* result = findChildWithAttribute(childName, attrName, attValue)) {
		return *result;
	}
	auto& result = addChild(string(childName));
	result.addAttribute(string(attrName), string(attValue));
	return result;
}

const string& XMLElement::getChildData(string_view childName) const
{
	return getChild(childName).getData();
}

string_view XMLElement::getChildData(string_view childName,
                                     string_view defaultValue) const
{
	auto* child = findChild(childName);
	return child ? child->getData() : defaultValue;
}

bool XMLElement::getChildDataAsBool(string_view childName, bool defaultValue) const
{
	auto* child = findChild(childName);
	return child ? StringOp::stringToBool(child->getData()) : defaultValue;
}

int XMLElement::getChildDataAsInt(string_view childName, int defaultValue) const
{
	auto* child = findChild(childName);
	return child ? StringOp::stringToInt(child->getData()) : defaultValue;
}

void XMLElement::setChildData(string_view childName, string value)
{
	if (auto* child = findChild(childName)) {
		child->setData(std::move(value));
	} else {
		addChild(string(childName), std::move(value));
	}
}

void XMLElement::removeAllChildren()
{
	children.clear();
}

bool XMLElement::hasAttribute(string_view attrName) const
{
	return findAttribute(attrName);
}

const string& XMLElement::getAttribute(string_view attrName) const
{
	if (auto* value = findAttribute(attrName)) {
		return *value;
	}
	throw ConfigException("Missing attribute \"", attrName, "\".");
}

string_view XMLElement::getAttribute(string_view attrName,
                                     string_view defaultValue) const
{
	auto* value = findAttribute(attrName);
	return value ? *value : defaultValue;
}

bool XMLElement::getAttributeAsBool(string_view attrName,
                                    bool defaultValue) const
{
	auto* value = findAttribute(attrName);
	return value ? StringOp::stringToBool(*value) : defaultValue;
}

int XMLElement::getAttributeAsInt(string_view attrName,
                                  int defaultValue) const
{
	auto* value = findAttribute(attrName);
	return value ? StringOp::stringToInt(*value) : defaultValue;
}

bool XMLElement::findAttributeInt(string_view attrName,
                                  unsigned& result) const
{
	if (auto* value = findAttribute(attrName)) {
		result = StringOp::stringToInt(*value);
		return true;
	} else {
		return false;
	}
}

string XMLElement::dump() const
{
	string result;
	dump(result, 0);
	return result;
}

void XMLElement::dump(string& result, unsigned indentNum) const
{
	strAppend(result, spaces(indentNum), '<', getName());
	for (const auto& [attrName, value] : attributes) {
		strAppend(result, ' ', attrName, "=\"", XMLEscape(value), '"');
	}
	if (children.empty()) {
		if (data.empty()) {
			strAppend(result, "/>\n");
		} else {
			strAppend(result, '>', XMLEscape(data), "</",
			          getName(), ">\n");
		}
	} else {
		strAppend(result, ">\n");
		for (auto& c : children) {
			c.dump(result, indentNum + 2);
		}
		strAppend(result, spaces(indentNum), "</", getName(), ">\n");
	}
}

// This routine does the following substitutions:
//  & -> &amp;   must always be done
//  < -> &lt;    must always be done
//  > -> &gt;    always allowed, but must be done when it appears as ]]>
//  ' -> &apos;  always allowed, but must be done inside quoted attribute
//  " -> &quot;  always allowed, but must be done inside quoted attribute
// So to simplify things we always do these 5 substitutions.
string XMLElement::XMLEscape(string_view s)
{
	static constexpr const char* const CHARS = "<>&\"'";
	size_t i = s.find_first_of(CHARS);
	if (i == string::npos) return string(s); // common case, no substitutions

	string result;
	result.reserve(s.size() + 10); // extra space for at least 2 substitutions
	size_t pos = 0;
	do {
		strAppend(result, s.substr(pos, i - pos));
		switch (s[i]) {
		case '<' : result += "&lt;";   break;
		case '>' : result += "&gt;";   break;
		case '&' : result += "&amp;";  break;
		case '"' : result += "&quot;"; break;
		case '\'': result += "&apos;"; break;
		default: UNREACHABLE;
		}
		pos = i + 1;
		i = s.find_first_of(CHARS, pos);
	} while (i != string::npos);
	strAppend(result, s.substr(pos));
	return result;
}

static unique_ptr<FileContext> lastSerializedFileContext;
unique_ptr<FileContext> XMLElement::getLastSerializedFileContext()
{
	return std::move(lastSerializedFileContext); // this also sets value to nullptr;
}
// version 1: initial version
// version 2: removed 'context' tag
//            also removed 'parent', but that was never serialized
//        2b: (no need to increase version) name and data members are
//            serialized as normal members instead of constructor parameters
//        2c: (no need to increase version) attributes were initially stored as
//            map<string, string>, later this was changed to
//            vector<pair<string, string>>. To keep bw-compat the serialize()
//            method converted between these two formats. Though (by luck) in
//            the XML output both datastructures are serialized to the same
//            format, so we can drop this conversion step without breaking
//            bw-compat.
template<typename Archive>
void XMLElement::serialize(Archive& ar, unsigned version)
{
	ar.serialize("name",       name,
	             "data",       data,
	             "attributes", attributes,
	             "children",   children);

	if (ar.versionBelow(version, 2)) {
		assert(ar.isLoader());
		unique_ptr<FileContext> context;
		ar.serialize("context", context);
		if (context) {
			assert(!lastSerializedFileContext);
			lastSerializedFileContext = std::move(context);
		}
	}
}
INSTANTIATE_SERIALIZE_METHODS(XMLElement);

} // namespace openmsx
