// license:BSD-3-Clause
// copyright-holders:Aaron Giles
/***************************************************************************

    xmlfile.c

    XML file parsing code.

***************************************************************************/

#include "xmlfile.h"

#include "osdcore.h"

#include <expat.h>

#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <locale>
#include <sstream>


namespace util::xml {

namespace {

//**************************************************************************
//  CONSTANTS
//**************************************************************************

constexpr unsigned TEMP_BUFFER_SIZE(4096U);


//**************************************************************************
//  UTILITY FUNCTIONS
//**************************************************************************

void write_escaped(core_file &file, std::string const &str)
{
	// FIXME: check for errors
	std::string::size_type pos = 0;
	while ((str.size() > pos) && (std::string::npos != pos))
	{
		std::string::size_type const found = str.find_first_of("\"&<>", pos);
		if (found != std::string::npos)
		{
			write(file, &str[pos], found - pos);
			switch (str[found])
			{
			case '"': file.puts("&quot;"); pos = found + 1; break;
			case '&': file.puts("&amp;");  pos = found + 1; break;
			case '<': file.puts("&lt;");   pos = found + 1; break;
			case '>': file.puts("&gt;");   pos = found + 1; break;
			default: pos = found;
			}
		}
		else
		{
			write(file, &str[pos], str.size() - pos);
			pos = found;
		}
	}
}

} // anonymous namespace



//**************************************************************************
//  TYPE DEFINITIONS
//**************************************************************************

struct parse_info
{
	parse_info() { memset(&parser, 0, sizeof(parser)); }

	XML_Parser      parser;
	file::ptr       rootnode;
	data_node *     curnode = nullptr;
	uint32_t        flags = 0;
};



//**************************************************************************
//  PROTOTYPES
//**************************************************************************

// expat interfaces
static bool expat_setup_parser(parse_info &info, parse_options const *opts);
static void expat_element_start(void *data, const XML_Char *name, const XML_Char **attributes);
static void expat_data(void *data, const XML_Char *s, int len);
static void expat_element_end(void *data, const XML_Char *name);



//**************************************************************************
//  XML FILE OBJECTS
//**************************************************************************

file::file() { }
file::~file() { }


//-------------------------------------------------
//  create - create a new, empty XML file
//-------------------------------------------------

file::ptr file::create()
{
	try { return ptr(new file); }
	catch (...) { return ptr(); }
}


//-------------------------------------------------
//  read - parse an XML file into its nodes
//-------------------------------------------------

file::ptr file::read(read_stream &file, parse_options const *opts)
{
	// set up the parser
	parse_info info;
	if (!expat_setup_parser(info, opts))
		return ptr();

	// loop through the file and parse it
	bool done;
	do
	{
		char tempbuf[TEMP_BUFFER_SIZE];

		// read as much as we can
		auto const [err, bytes] = util::read(file, tempbuf, sizeof(tempbuf)); // FIXME: better error handling
		done = !bytes;

		// parse the data
		if (XML_Parse(info.parser, tempbuf, bytes, done) == XML_STATUS_ERROR)
		{
			if (opts && opts->error)
			{
				opts->error->error_message = XML_ErrorString(XML_GetErrorCode(info.parser));
				opts->error->error_line = XML_GetCurrentLineNumber(info.parser);
				opts->error->error_column = XML_GetCurrentColumnNumber(info.parser);
			}

			info.rootnode.reset();
			XML_ParserFree(info.parser);
			return ptr();
		}
	}
	while (!done);

	// free the parser
	XML_ParserFree(info.parser);

	// return the root node
	return std::move(info.rootnode);
}


//-------------------------------------------------
//  string_read - parse an XML string into its
//  nodes
//-------------------------------------------------

file::ptr file::string_read(const char *string, parse_options const *opts)
{
	parse_info info;
	int length = (int)strlen(string);

	// set up the parser
	if (!expat_setup_parser(info, opts))
		return ptr();

	// parse the data
	if (XML_Parse(info.parser, string, length, 1) == XML_STATUS_ERROR)
	{
		if (opts != nullptr && opts->error != nullptr)
		{
			opts->error->error_message = XML_ErrorString(XML_GetErrorCode(info.parser));
			opts->error->error_line = XML_GetCurrentLineNumber(info.parser);
			opts->error->error_column = XML_GetCurrentColumnNumber(info.parser);
		}

		info.rootnode.reset();
		XML_ParserFree(info.parser);
		return ptr();
	}

	// free the parser
	XML_ParserFree(info.parser);

	// return the root node
	return std::move(info.rootnode);
}


//-------------------------------------------------
//  file_write - write an XML tree to a file
//-------------------------------------------------

void file::write(util::core_file &file) const
{
	// ensure this is a root node
	assert(!get_name());

	// output a simple header
	file.printf("<?xml version=\"1.0\"?>\n");
	file.printf("<!-- This file is autogenerated; comments and unknown tags will be stripped -->\n");

	// loop over children of the root and output
	write_recursive(0, file);
}



//**************************************************************************
//  XML NODE MANAGEMENT
//**************************************************************************

data_node::data_node()
	: line(0)
	, m_next(nullptr)
	, m_first_child(nullptr)
	, m_name()
	, m_value()
	, m_parent(nullptr)
	, m_attributes()
{
}

data_node::data_node(data_node *parent, const char *name, const char *value)
	: line(0)
	, m_next(nullptr)
	, m_first_child(nullptr)
	, m_name(name)
	, m_value(value ? value : "")
	, m_parent(parent)
	, m_attributes()
{
	std::transform(m_name.begin(), m_name.end(), m_name.begin(), [] (char ch) { return std::tolower(uint8_t(ch)); });
}


//-------------------------------------------------
//  free_node_recursive - recursively free
//  the data allocated to an XML node
//-------------------------------------------------

data_node::~data_node()
{
	free_children();
}


void data_node::free_children()
{
	for (data_node *nchild = nullptr; m_first_child; m_first_child = nchild)
	{
		// note the next node and free this node
		nchild = m_first_child->get_next_sibling();
		delete m_first_child;
	}
}


void data_node::set_value(char const *value)
{
	m_value.assign(value ? value : "");
}


void data_node::append_value(char const *value, int length)
{
	m_value.append(value, length);
}


void data_node::trim_whitespace()
{
	// first strip leading spaces
	std::string::iterator start = m_value.begin();
	while ((m_value.end() != start) && std::isspace(uint8_t(*start)))
		++start;
	m_value.replace(m_value.begin(), start, 0U, '\0');

	// then strip trailing spaces
	std::string::iterator end = m_value.end();
	while ((m_value.begin() != end) && std::isspace(uint8_t(*std::prev(end))))
		--end;
	m_value.replace(end, m_value.end(), 0U, '\0');
}


//-------------------------------------------------
//  data_node::count_children - count the
//  number of child nodes
//-------------------------------------------------

std::size_t data_node::count_children() const
{
	// loop over children and count
	std::size_t count = 0;
	for (data_node const *node = get_first_child(); node; node = node->get_next_sibling())
		count++;
	return count;
}


//-------------------------------------------------
//  data_node::count_children - count the
//  number of child nodes
//-------------------------------------------------

std::size_t data_node::count_attributes() const
{
	return m_attributes.size();
}


//-------------------------------------------------
//  data_node::get_child - find the first
//  child of the specified node with the specified
//  tag
//-------------------------------------------------

data_node *data_node::get_child(const char *name)
{
	return m_first_child ? m_first_child->get_sibling(name) : nullptr;
}

data_node const *data_node::get_child(const char *name) const
{
	return m_first_child ? m_first_child->get_sibling(name) : nullptr;
}


//-------------------------------------------------
//  find_first_matching_child - find the first
//  child of the specified node with the
//  specified tag or attribute/value pair
//-------------------------------------------------

data_node *data_node::find_first_matching_child(const char *name, const char *attribute, const char *matchval)
{
	return m_first_child ? m_first_child->find_matching_sibling(name, attribute, matchval) : nullptr;
}

data_node const *data_node::find_first_matching_child(const char *name, const char *attribute, const char *matchval) const
{
	return m_first_child ? m_first_child->find_matching_sibling(name, attribute, matchval) : nullptr;
}


//-------------------------------------------------
//  data_node::get_next_sibling - find the
//  next sibling of the specified node with the
//  specified tag
//-------------------------------------------------

data_node *data_node::get_next_sibling(const char *name)
{
	return m_next ? m_next->get_sibling(name) : nullptr;
}

data_node const *data_node::get_next_sibling(const char *name) const
{
	return m_next ? m_next->get_sibling(name) : nullptr;
}


//-------------------------------------------------
//  find_next_matching_sibling - find the next
//  sibling of the specified node with the
//  specified tag or attribute/value pair
//-------------------------------------------------

data_node *data_node::find_next_matching_sibling(const char *name, const char *attribute, const char *matchval)
{
	return m_next ? m_next->find_matching_sibling(name, attribute, matchval) : nullptr;
}

data_node const *data_node::find_next_matching_sibling(const char *name, const char *attribute, const char *matchval) const
{
	return m_next ? m_next->find_matching_sibling(name, attribute, matchval) : nullptr;
}


//-------------------------------------------------
//  add_child - add a new child node to the
//  given node
//-------------------------------------------------

data_node *data_node::add_child(const char *name, const char *value)
{
	if (!name || !*name)
		return nullptr;

	// new element: create a new node
	data_node *node;
	try { node = new data_node(this, name, value); }
	catch (...) { return nullptr; }

	if (!node->get_name() || (!node->get_value() && value))
	{
		delete node;
		return nullptr;
	}

	// add us to the end of the list of siblings
	data_node **pnode;
	for (pnode = &m_first_child; *pnode; pnode = &(*pnode)->m_next) { }
	*pnode = node;

	return node;
}


//-------------------------------------------------
//  get_or_add_child - find a child node of
//  the specified type; if not found, add one
//-------------------------------------------------

data_node *data_node::get_or_add_child(const char *name, const char *value)
{
	// find the child first
	data_node *const child = m_first_child->get_sibling(name);
	if (child)
		return child;

	// if not found, do a standard add child
	return add_child(name, value);
}


// recursively copy as child of another node
data_node *data_node::copy_into(data_node &parent) const
{
	data_node *const result = parent.add_child(get_name(), get_value());
	result->m_attributes = m_attributes;

	data_node *dst = result;
	data_node const *src = get_first_child();
	while (src && (&parent != dst))
	{
		dst = dst->add_child(src->get_name(), src->get_value());
		dst->m_attributes = src->m_attributes;
		data_node const *next = src->get_first_child();
		if (next)
		{
			src = next;
		}
		else
		{
			dst = dst->get_parent();
			next = src->get_next_sibling();
			if (next)
			{
				src = next;
			}
			else
			{
				do
				{
					dst = dst->get_parent();
					src = src->get_parent();
					next = src->get_next_sibling();
				}
				while (!next && (&parent != dst));
				src = next;
			}
		}
	}

	return result;
}


//-------------------------------------------------
//  delete_node - delete a node and its
//  children
//-------------------------------------------------

void data_node::delete_node()
{
	// don't allow deletion of document root
	if (m_parent)
	{
		// first unhook us from the list of children of our parent
		for (data_node **pnode = &m_parent->m_first_child; *pnode; pnode = &(*pnode)->m_next)
		{
			if (*pnode == this)
			{
				*pnode = this->m_next;
				break;
			}
		}

		// now free ourselves and our children
		delete this;
	}
	else
	{
		// remove all children of document root
		free_children();
	}
}


//-------------------------------------------------
//  get_next_sibling - find the next sibling of
//  the specified node with the specified tag
//-------------------------------------------------

data_node *data_node::get_sibling(const char *name)
{
	// loop over siblings and find a matching name
	for (data_node *node = this; node; node = node->get_next_sibling())
		if (strcmp(node->get_name(), name) == 0)
			return node;
	return nullptr;
}

data_node const *data_node::get_sibling(const char *name) const
{
	// loop over siblings and find a matching name
	for (data_node const *node = this; node; node = node->get_next_sibling())
		if (strcmp(node->get_name(), name) == 0)
			return node;
	return nullptr;
}


//-------------------------------------------------
//  find_matching_sibling - find the next
//  sibling of the specified node with the
//  specified tag or attribute/value pair
//-------------------------------------------------

data_node *data_node::find_matching_sibling(const char *name, const char *attribute, const char *matchval)
{
	// loop over siblings and find a matching attribute
	for (data_node *node = this; node; node = node->get_next_sibling())
	{
		// can pass nullptr as a wildcard for the node name
		if (!name || !strcmp(name, node->get_name()))
		{
			// find a matching attribute
			attribute_node const *const attr = node->get_attribute(attribute);
			if (attr && !strcmp(attr->value.c_str(), matchval))
				return node;
		}
	}
	return nullptr;
}

data_node const *data_node::find_matching_sibling(const char *name, const char *attribute, const char *matchval) const
{
	// loop over siblings and find a matching attribute
	for (data_node const *node = this; node; node = node->get_next_sibling())
	{
		// can pass nullptr as a wildcard for the node name
		if (!name || !strcmp(name, node->get_name()))
		{
			// find a matching attribute
			attribute_node const *const attr = node->get_attribute(attribute);
			if (attr && !strcmp(attr->value.c_str(), matchval))
				return node;
		}
	}
	return nullptr;
}



//**************************************************************************
//  XML ATTRIBUTE MANAGEMENT
//**************************************************************************

bool data_node::has_attribute(const char *attribute) const
{
	return get_attribute(attribute) != nullptr;
}


//-------------------------------------------------
//  get_attribute - get the value of the
//  specified attribute, or nullptr if not found
//-------------------------------------------------

data_node::attribute_node *data_node::get_attribute(const char *attribute)
{
	// loop over attributes and find a match
	for (attribute_node &anode : m_attributes)
		if (strcmp(anode.name.c_str(), attribute) == 0)
			return &anode;
	return nullptr;
}

data_node::attribute_node const *data_node::get_attribute(const char *attribute) const
{
	// loop over attributes and find a match
	for (attribute_node const &anode : m_attributes)
		if (strcmp(anode.name.c_str(), attribute) == 0)
			return &anode;
	return nullptr;
}


//-------------------------------------------------
//  get_attribute_string_ptr - get a pointer to
//  the string value of the specified attribute;
//  if not found, return = nullptr
//-------------------------------------------------

std::string const *data_node::get_attribute_string_ptr(const char *attribute) const
{
	attribute_node const *attr = get_attribute(attribute);
	return attr ? &attr->value : nullptr;
}


//-------------------------------------------------
//  get_attribute_string - get the string
//  value of the specified attribute; if not
//  found, return = the provided default
//-------------------------------------------------

const char *data_node::get_attribute_string(const char *attribute, const char *defvalue) const
{
	attribute_node const *attr = get_attribute(attribute);
	return attr ? attr->value.c_str() : defvalue;
}


//-------------------------------------------------
//  get_attribute_int - get the integer
//  value of the specified attribute; if not
//  found, return = the provided default
//-------------------------------------------------

long long data_node::get_attribute_int(const char *attribute, long long defvalue) const
{
	attribute_node const *attr = get_attribute(attribute);
	if (!attr)
		return defvalue;
	std::string const &string = attr->value;

	std::istringstream stream;
	stream.imbue(std::locale::classic());
	long long result;
	if (string[0] == '$')
	{
		stream.str(&string[1]);
		unsigned long long uvalue;
		stream >> std::hex >> uvalue;
		result = static_cast<long long>(uvalue);
	}
	else if ((string[0] == '0') && ((string[1] == 'x') || (string[1] == 'X')))
	{
		stream.str(&string[2]);
		unsigned long long uvalue;
		stream >> std::hex >> uvalue;
		result = static_cast<long long>(uvalue);
	}
	else if (string[0] == '#')
	{
		stream.str(&string[1]);
		stream >> result;
	}
	else
	{
		stream.str(&string[0]);
		stream >> result;
	}

	return stream ? result : defvalue;
}


//-------------------------------------------------
//  get_attribute_int_format - return the
//  format of the given integer attribute
//-------------------------------------------------

data_node::int_format data_node::get_attribute_int_format(const char *attribute) const
{
	attribute_node const *attr = get_attribute(attribute);
	if (!attr)
		return int_format::DECIMAL;
	std::string const &string = attr->value;

	if (string[0] == '$')
		return int_format::HEX_DOLLAR;
	else if (string[0] == '0' && string[1] == 'x')
		return int_format::HEX_C;
	else if (string[0] == '#')
		return int_format::DECIMAL_HASH;
	else
		return int_format::DECIMAL;
}


//-------------------------------------------------
//  get_attribute_float - get the float
//  value of the specified attribute; if not
//  found, return = the provided default
//-------------------------------------------------

float data_node::get_attribute_float(const char *attribute, float defvalue) const
{
	attribute_node const *attr = get_attribute(attribute);
	if (!attr)
		return defvalue;

	std::istringstream stream(attr->value);
	stream.imbue(std::locale::classic());
	float result;
	return (stream >> result) ? result : defvalue;
}


//-------------------------------------------------
//  set_attribute - set a new attribute and
//  string value on the node
//-------------------------------------------------

void data_node::set_attribute(const char *name, const char *value)
{
	attribute_node *anode;

	// first find an existing one to replace
	anode = get_attribute(name);

	if (anode != nullptr)
	{
		// if we found it, free the old value and replace it
		anode->value = value;
	}
	else
	{
		// otherwise, create a new node
		add_attribute(name, value);
	}
}


//-------------------------------------------------
//  set_attribute_int - set a new attribute and
//  integer value on the node
//-------------------------------------------------

void data_node::set_attribute_int(const char *name, long long value)
{
	set_attribute(name, string_format(std::locale::classic(), "%d", value).c_str());
}


//-------------------------------------------------
//  set_attribute_float - set a new attribute and
//  float value on the node
//-------------------------------------------------

void data_node::set_attribute_float(const char *name, float value)
{
	set_attribute(name, string_format(std::locale::classic(), "%f", value).c_str());
}



//**************************************************************************
//  MISCELLANEOUS INTERFACES
//**************************************************************************

//-------------------------------------------------
//  normalize_string - normalize a string
//  to ensure it doesn't contain embedded tags
//-------------------------------------------------

std::string normalize_string(std::string_view string)
{
	std::string result;
	result.reserve(string.length());

	for (char ch : string)
	{
		switch (ch)
		{
		case '\"':  result.append("&quot;");    break;
		case '&':   result.append("&amp;");     break;
		case '<':   result.append("&lt;");      break;
		case '>':   result.append("&gt;");      break;
		default:    result.append(1, ch);       break;
		}
	}

	return result;
}



//**************************************************************************
//  EXPAT INTERFACES
//**************************************************************************

//-------------------------------------------------
//  expat_setup_parser - set up expat for parsing
//-------------------------------------------------

static bool expat_setup_parser(parse_info &info, parse_options const *opts)
{
	// setup info structure
	info = parse_info();
	if (opts != nullptr)
	{
		info.flags = opts->flags;
		if (opts->error != nullptr)
		{
			opts->error->error_message = nullptr;
			opts->error->error_line = 0;
			opts->error->error_column = 0;
		}
	}

	// create a root node
	info.rootnode = file::create();
	if (!info.rootnode)
		return false;
	info.curnode = info.rootnode.get();

	// create the XML parser
	info.parser = XML_ParserCreate(nullptr);
	if (!info.parser)
	{
		info.rootnode.reset();
		return false;
	}

	// configure the parser
	XML_SetElementHandler(info.parser, expat_element_start, expat_element_end);
	XML_SetCharacterDataHandler(info.parser, expat_data);
	XML_SetUserData(info.parser, &info);

	// optional parser initialization step
	if (opts != nullptr && opts->init_parser != nullptr)
		(*opts->init_parser)(info.parser);
	return true;
}


//------------------------------------------------
//  expat_element_start - expat callback for a new
//  element
//------------------------------------------------

static void expat_element_start(void *data, const XML_Char *name, const XML_Char **attributes)
{
	auto *info = (parse_info *) data;
	data_node **curnode = &info->curnode;
	data_node *newnode;
	int attr;

	// add a new child node to the current node
	newnode = (*curnode)->add_child(name, nullptr);
	if (newnode == nullptr)
		return;

	// remember the line number
	newnode->line = XML_GetCurrentLineNumber(info->parser);

	// add all the attributes as well
	for (attr = 0; attributes[attr]; attr += 2)
		newnode->add_attribute(attributes[attr+0], attributes[attr+1]);

	// set us up as the current node
	*curnode = newnode;
}


//-------------------------------------------------
//  expat_data - expat callback for an additional
//  element data
//-------------------------------------------------

static void expat_data(void *data, const XML_Char *s, int len)
{
	auto *info = (parse_info *) data;
	data_node **curnode = &info->curnode;
	(*curnode)->append_value(s, len);
}


//-------------------------------------------------
//  expat_element_end - expat callback for the end
//  of an element
//-------------------------------------------------

static void expat_element_end(void *data, const XML_Char *name)
{
	auto *info = (parse_info *) data;
	data_node **curnode = &info->curnode;

	// strip leading/trailing spaces from the value data
	if (!(info->flags & PARSE_FLAG_WHITESPACE_SIGNIFICANT))
		(*curnode)->trim_whitespace();

	// back us up a node
	*curnode = (*curnode)->get_parent();
}



//**************************************************************************
//  NODE/ATTRIBUTE ADDITIONS
//**************************************************************************

//-------------------------------------------------
//  add_attribute - add a new attribute to the
//  given node
//-------------------------------------------------

void data_node::add_attribute(const char *name, const char *value)
{
	try
	{
		attribute_node &anode = *m_attributes.emplace(m_attributes.end(), name, value);
		std::transform(anode.name.begin(), anode.name.end(), anode.name.begin(), [] (char ch) { return std::tolower(uint8_t(ch)); });
	}
	catch (...)
	{
		osd_printf_warning("error adding attribute to XML data node\n");
	}
}



//**************************************************************************
//  RECURSIVE TREE OPERATIONS
//**************************************************************************

//-------------------------------------------------
//  write_node_recursive - recursively write
//  an XML node and its children to a file
//-------------------------------------------------

void data_node::write_recursive(int indent, util::core_file &file) const
{
	if (!get_name())
	{
		// root node doesn't generate tag
		for (data_node const *child = this->get_first_child(); child; child = child->get_next_sibling())
			child->write_recursive(indent, file);
	}
	else
	{
		// output this tag
		file.printf("%*s<%s", indent, "", get_name());

		// output any attributes, escaping as necessary
		for (attribute_node const &anode : m_attributes)
		{
			file.printf(" %s=\"", anode.name);
			write_escaped(file, anode.value);
			file.puts("\"");
		}

		if (!get_first_child() && !get_value())
		{
			// if there are no children and no value, end the tag here
			file.printf(" />\n");
		}
		else
		{
			// otherwise, close this tag and output more stuff
			file.printf(">\n");

			// if there is a value, output that here
			if (!m_value.empty())
			{
				file.printf("%*s", indent + 4, "");
				write_escaped(file, m_value);
				file.puts("\n");
			}

			// loop over children and output them as well
			if (get_first_child())
			{
				for (data_node const *child = this->get_first_child(); child; child = child->get_next_sibling())
					child->write_recursive(indent + 4, file);
			}

			// write a closing tag
			file.printf("%*s</%s>\n", indent, "", get_name());
		}
	}
}

} // namespace util::xml
