// For licensing and usage information, read docs/winui_license.txt
//****************************************************************************
/***************************************************************************

    win_options.c

    Core options code code

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

#include <stdarg.h>
#include <stdlib.h>
#include <ctype.h>
#include "win_options.h"


//**************************************************************************
//  REPLACE MACRO: option --> win_options
//**************************************************************************

#define options win_options


//**************************************************************************
//  GLOBAL VARIABLES
//**************************************************************************

const char *const options::s_option_unadorned[MAX_UNADORNED_OPTIONS] =
{
	"<UNADORNED0>",
	"<UNADORNED1>",
	"<UNADORNED2>",
	"<UNADORNED3>",
	"<UNADORNED4>",
	"<UNADORNED5>",
	"<UNADORNED6>",
	"<UNADORNED7>",
	"<UNADORNED8>",
	"<UNADORNED9>",
	"<UNADORNED10>",
	"<UNADORNED11>",
	"<UNADORNED12>",
	"<UNADORNED13>",
	"<UNADORNED14>",
	"<UNADORNED15>"
};



//**************************************************************************
//  CORE OPTIONS ENTRY
//**************************************************************************

//-------------------------------------------------
//  entry - constructor
//-------------------------------------------------

options::entry::entry(const char *name, const char *description, UINT32 flags, const char *defvalue)
	: m_next(NULL),
		m_flags(flags),
		m_seqid(0),
		m_error_reported(false),
		m_priority(OPTION_PRIORITY_DEFAULT),
		m_description(description)
{
	// copy in the name(s) as appropriate
	if (name != nullptr)
	{
		// first extract any range
		std::string namestr(name);
		int lparen = namestr.find('(');
		int dash = namestr.find(lparen + 1, '-');
		int rparen = namestr.find(dash + 1, ')');
		if (lparen != -1 && dash != -1 && rparen != -1)
		{
			m_minimum.assign(namestr.substr(lparen + 1, dash - (lparen + 1)));
			strtrimspace(m_minimum);
			m_maximum.assign(namestr.substr(dash + 1, rparen - (dash + 1)));
			strtrimspace(m_maximum);
			namestr.erase(lparen, rparen + 1 - lparen);
		}

		// then chop up any semicolon-separated names
		int semi;
		int nameindex = 0;
		while ((semi = namestr.find(';')) != -1 && nameindex < ARRAY_LENGTH(m_name))
		{
			m_name[nameindex++].assign(namestr.substr(0, semi));
			namestr.erase(0, semi + 1);
		}

		// finally add the last item
		if (nameindex < ARRAY_LENGTH(m_name))
			m_name[nameindex++] = namestr;
	}

	// set the default value
	if (defvalue != nullptr)
		m_defdata = defvalue;
	m_data = m_defdata;
}


//-------------------------------------------------
//  set_value - update our data value
//-------------------------------------------------

void options::entry::set_value(const char *newdata, int priority)
{
	// ignore if we don't have priority
	if (priority < m_priority)
		return;

	// set the data and priority, then bump the sequence
	m_data = newdata;
	m_priority = priority;
	m_seqid++;
}


//-------------------------------------------------
//  set_default_value - set the default value of
//  an option, and reset the current value to it
//-------------------------------------------------

void options::entry::set_default_value(const char *defvalue)
{
	m_data = defvalue;
	m_defdata = defvalue;
	m_priority = OPTION_PRIORITY_DEFAULT;
}


//-------------------------------------------------
//  set_description - set the description of
//  an option
//-------------------------------------------------

void options::entry::set_description(const char *description)
{
	m_description = description;
}


void options::entry::set_flag(UINT32 mask, UINT32 flag)
{
	m_flags = ( m_flags & mask ) | flag;
}


//-------------------------------------------------
//  revert - revert back to our default if we are
//  at or below the given priority
//-------------------------------------------------

void options::entry::revert(int priority)
{
	// if our priority is low enough, revert to the default
	if (m_priority <= priority)
	{
		m_data = m_defdata;
		m_priority = OPTION_PRIORITY_DEFAULT;
	}
}



//**************************************************************************
//  CORE OPTIONS
//**************************************************************************

//-------------------------------------------------
//  options - constructor
//-------------------------------------------------

options::options()
{
}

options::options(const options_entry *entrylist)
{
	add_entries(entrylist);
}

options::options(const options_entry *entrylist1, const options_entry *entrylist2)
{
	add_entries(entrylist1);
	add_entries(entrylist2);
}

options::options(const options_entry *entrylist1, const options_entry *entrylist2, const options_entry *entrylist3)
{
	add_entries(entrylist1);
	add_entries(entrylist2);
	add_entries(entrylist3);
}

options::options(const options &src)
{
	copyfrom(src);
}


//-------------------------------------------------
//  ~options - destructor
//-------------------------------------------------

options::~options()
{
}


//-------------------------------------------------
//  operator= - assignment operator
//-------------------------------------------------

options &options::operator=(const options &rhs)
{
	// ignore self-assignment
	if (this != &rhs)
		copyfrom(rhs);
	return *this;
}


//-------------------------------------------------
//  operator== - compare two sets of options
//-------------------------------------------------

bool options::operator==(const options &rhs)
{
	// iterate over options in the first list
	for (entry *curentry = m_entrylist.first(); curentry != nullptr; curentry = curentry->next())
		if (!curentry->is_header())
		{
			// if the values differ, return false
			if (strcmp(curentry->value(), rhs.value(curentry->name())) != 0)
				return false;
		}

	return true;
}


//-------------------------------------------------
//  operator!= - compare two sets of options
//-------------------------------------------------

bool options::operator!=(const options &rhs)
{
	return !operator==(rhs);
}


//-------------------------------------------------
//  add_entry - add an entry to the current
//  options set
//-------------------------------------------------

void options::add_entry(const char *name, const char *description, UINT32 flags, const char *defvalue, bool override_existing)
{
	// allocate a new entry
	entry *newentry = global_alloc(entry(name, description, flags, defvalue));
	if (newentry->name() != nullptr)
	{
		// see if we match an existing entry
		auto checkentry = m_entrymap.find(newentry->name());
		if (checkentry != m_entrymap.end())
		{
			entry *existing = checkentry->second;
			// if we're overriding existing entries, then remove the old one
			if (override_existing)
				m_entrylist.remove(*existing);

			// otherwise, just override the default and current values and throw out the new entry
			else
			{
				existing->set_default_value(newentry->value());
				global_free(newentry);
				return;
			}
		}
	}

	// add us to the list and maps
	append_entry(*newentry);
}


//-------------------------------------------------
//  add_entries - add entries to the current
//  options sets
//-------------------------------------------------

void options::add_entries(const options_entry *entrylist, bool override_existing)
{
	// loop over entries until we hit a NULL name
	for (; entrylist->name != nullptr || (entrylist->flags & OPTION_HEADER) != 0; entrylist++)
		add_entry(*entrylist, override_existing);
}


//-------------------------------------------------
//  set_default_value - change the default value
//  of an option
//-------------------------------------------------

void options::set_default_value(const char *name, const char *defvalue)
{
	// find the entry and bail if we can't
	auto curentry = m_entrymap.find(name);
	if (curentry == m_entrymap.end())
		return;

	// update the data and default data
	curentry->second->set_default_value(defvalue);
}


//-------------------------------------------------
//  set_description - change the description
//  of an option
//-------------------------------------------------

void options::set_description(const char *name, const char *description)
{
	// find the entry and bail if we can't
	auto curentry = m_entrymap.find(name);
	if (curentry == m_entrymap.end())
		return;

	// update the data and default data
	curentry->second->set_description(description);
}


//-------------------------------------------------
//  parse_command_line - parse a series of
//  command line arguments
//-------------------------------------------------

bool options::parse_command_line(int argc, char **argv, int priority, std::string &error_string)
{
	// reset the errors and the command
	error_string.clear();
	m_command.clear();

	// iterate through arguments
	int unadorned_index = 0;
	bool retval = true;
	for (int arg = 1; arg < argc; arg++)
	{
		// determine the entry name to search for
		const char *curarg = argv[arg];
		bool is_unadorned = (curarg[0] != '-');
		const char *optionname = is_unadorned ? options::unadorned(unadorned_index++) : &curarg[1];

		// find our entry; if not found, indicate invalid option
		auto curentry = m_entrymap.find(optionname);
		if (curentry == m_entrymap.end())
		{
			error_string.append(string_format("Error: unknown option: %s\n", curarg));
			retval = false;
			if (!is_unadorned) arg++;
			continue;
		}

		// process commands first
		if (curentry->second->type() == OPTION_COMMAND)
		{
			// can only have one command
			if (!m_command.empty())
			{
				error_string.append(string_format("Error: multiple commands specified -%s and %s\n", m_command.c_str(), curarg));
				return false;
			}
			m_command = curentry->second->name();
			continue;
		}

		// get the data for this argument, special casing booleans
		const char *newdata;
		if (curentry->second->type() == OPTION_BOOLEAN)
			newdata = (strncmp(&curarg[1], "no", 2) == 0) ? "0" : "1";
		else if (is_unadorned)
			newdata = curarg;
		else if (arg + 1 < argc)
			newdata = argv[++arg];
		else
		{
			error_string.append(string_format("Error: option %s expected a parameter\n", curarg));
			return false;
		}

		// set the new data
		validate_and_set_data(*curentry->second, newdata, priority, error_string);
	}
	return retval;
}


//-------------------------------------------------
//  parse_ini_file - parse a series of entries in
//  an INI file
//-------------------------------------------------

bool options::parse_ini_file(util::core_file &inifile, int priority, int ignore_priority, std::string &error_string)
{
	// loop over lines in the file
	char buffer[4096];
	while (inifile.gets(buffer, ARRAY_LENGTH(buffer)) != nullptr)
	{
		// find the extent of the name
		char *optionname;
		for (optionname = buffer; *optionname != 0; optionname++)
			if (!isspace((UINT8)*optionname))
				break;

		// skip comments
		if (*optionname == 0 || *optionname == '#')
			continue;

		// scan forward to find the first space
		char *temp;
		for (temp = optionname; *temp != 0; temp++)
			if (isspace((UINT8)*temp))
				break;

		// if we hit the end early, print a warning and continue
		if (*temp == 0)
		{
			error_string.append(string_format("Warning: invalid line in INI: %s", buffer));
			continue;
		}

		// NULL-terminate
		*temp++ = 0;
		char *optiondata = temp;

		// scan the data, stopping when we hit a comment
		bool inquotes = false;
		for (temp = optiondata; *temp != 0; temp++)
		{
			if (*temp == '"')
				inquotes = !inquotes;
			if (*temp == '#' && !inquotes)
				break;
		}
		*temp = 0;

		// find our entry
		auto curentry = m_entrymap.find(optionname);
		if (curentry == m_entrymap.end())
		{
			if (priority >= ignore_priority)
				error_string.append(string_format("Warning: unknown option in INI: %s\n", optionname));
			continue;
		}

		// set the new data
		validate_and_set_data(*curentry->second, optiondata, priority, error_string);
	}
	return true;
}


//-------------------------------------------------
//  revert - revert options at or below a certain
//  priority back to their defaults
//-------------------------------------------------

void options::revert(int priority)
{
	// iterate over options and revert to defaults if below the given priority
	for (entry *curentry = m_entrylist.first(); curentry != nullptr; curentry = curentry->next())
		curentry->revert(priority);
}


//-------------------------------------------------
//  output_ini - output the options in INI format,
//  only outputting entries that different from
//  the optional diff
//-------------------------------------------------

const char *options::output_ini(std::string &buffer, const options *diff)
{
	// INI files are complete, so always start with a blank buffer
	buffer.clear();

	int num_valid_headers = 0;
	int unadorned_index = 0;
	const char *last_header = NULL;

	// loop over all items
	for (entry *curentry = m_entrylist.first(); curentry != nullptr; curentry = curentry->next())
	{
		const char *name = curentry->name();
		const char *value = curentry->value();
		bool is_unadorned = false;

		// check if it's unadorned
		if (name && strlen(name) && !strcmp(name, options::unadorned(unadorned_index)))
		{
			unadorned_index++;
			is_unadorned = true;
		}

		// header: record description
		if (curentry->is_header())
			last_header = curentry->description();

		// otherwise, output entries for all non-command items
		else if (!curentry->is_command())
		{
			if ( !curentry->is_internal() )
			{
				// look up counterpart in diff, if diff is specified
				if (diff == nullptr || strcmp(value, diff->value(name)) != 0)
				{
					// output header, if we have one
					if (last_header)
					{
						if (num_valid_headers++)
							buffer += '\n';
						buffer.append(string_format("#\n# %s\n#\n", last_header));
						last_header = nullptr;
					}

					// and finally output the data, skip if unadorned
					if (!is_unadorned)
					{
						if (strchr(value, ' '))
							buffer.append(string_format("%-25s \"%s\"\n", name, value));
						else
							buffer.append(string_format("%-25s %s\n", name, value));
					}
				}
			}
		}
	}
	return buffer.c_str();
}


//-------------------------------------------------
//  output_help - output option help to a string
//-------------------------------------------------

const char *options::output_help(std::string &buffer)
{
	// start empty
	buffer.clear();

	// loop over all items
	for (entry *curentry = m_entrylist.first(); curentry; curentry = curentry->next())
	{
		// header: just print
		if (curentry->is_header())
			buffer.append(string_format("\n#\n# %s\n#\n", curentry->description()));

		// otherwise, output entries for all non-deprecated items
		else if (curentry->description())
			buffer.append(string_format("-%-20s%s\n", curentry->name(), curentry->description()));
	}
	return buffer.c_str();
}


//-------------------------------------------------
//  value - return the raw option value
//-------------------------------------------------

const char *options::value(const char *name) const
{
	auto curentry = m_entrymap.find(name);
	return (curentry != m_entrymap.end()) ? curentry->second->value() : "";
}


//-------------------------------------------------
//  description - return description of option
//-------------------------------------------------

const char *options::description(const char *name) const
{
	auto curentry = m_entrymap.find(name);
	return (curentry != m_entrymap.end()) ? curentry->second->description() : "";
}


//-------------------------------------------------
//  priority - return the priority of option
//-------------------------------------------------

int options::priority(const char *name) const
{
	auto curentry = m_entrymap.find(name);
	return (curentry != m_entrymap.end()) ? curentry->second->priority() : 0;
}


//-------------------------------------------------
//  seqid - return the seqid for a given option
//-------------------------------------------------

UINT32 options::seqid(const char *name) const
{
	auto curentry = m_entrymap.find(name);
	return (curentry != m_entrymap.end()) ? curentry->second->seqid() : 0;
}

//-------------------------------------------------
//  exists - return if option exists in list
//-------------------------------------------------

bool options::exists(const char *name) const
{
	return (m_entrymap.find(name) != m_entrymap.end());
}

//-------------------------------------------------
//  set_value - set the raw option value
//-------------------------------------------------

bool options::set_value(const char *name, const char *value, int priority, std::string &error_string)
{
	// find the entry first
	auto curentry = m_entrymap.find(name);
	if (curentry == m_entrymap.end())
	{
		error_string.append(string_format("Attempted to set unknown option %s\n", name));
		return false;
	}

	// validate and set the item normally
	return validate_and_set_data(*curentry->second, value, priority, error_string);
}

bool options::set_value(const char *name, int value, int priority, std::string &error_string)
{
	return set_value(name, string_format("%d", value).c_str(), priority, error_string);
}

bool options::set_value(const char *name, float value, int priority, std::string &error_string)
{
	return set_value(name, string_format("%f", value).c_str(), priority, error_string);
}


void options::set_flag(const char *name, UINT32 mask, UINT32 flag)
{
	// find the entry first
	auto curentry = m_entrymap.find(name);
	if ( curentry == m_entrymap.end())
	{
		return;
	}
	curentry->second->set_flag(mask, flag);
}


//-------------------------------------------------
//  reset - reset the options state, removing
//  everything
//-------------------------------------------------

void options::reset()
{
	m_entrylist.reset();
	m_entrymap.clear();
}


//-------------------------------------------------
//  append_entry - append an entry to our list
//  and index it in the map
//-------------------------------------------------

void options::append_entry(options::entry &newentry)
{
	m_entrylist.append(newentry);

	// if we have names, add them to the map
	for (int name = 0; name < ARRAY_LENGTH(newentry.m_name); name++)
		if (newentry.name(name) != nullptr)
		{
			m_entrymap.insert(std::make_pair(newentry.name(name), &newentry));

			// for boolean options add a "no" variant as well
			if (newentry.type() == OPTION_BOOLEAN)
				m_entrymap.insert(std::make_pair(std::string("no").append(newentry.name(name)), &newentry));
		}
}


//-------------------------------------------------
//  remove_entry - remove an entry from our list
//  and map
//-------------------------------------------------

void options::remove_entry(options::entry &delentry)
{
	// remove all names from the map
	for (int name = 0; name < ARRAY_LENGTH(delentry.m_name); name++)
		if (!delentry.m_name[name].empty())
		{
			auto entry = m_entrymap.find(delentry.m_name[name]);
			if (entry!= m_entrymap.end()) m_entrymap.erase(entry);
		}

	// remove the entry from the list
	m_entrylist.remove(delentry);
}


//-------------------------------------------------
//  copyfrom - copy options from another set
//-------------------------------------------------

void options::copyfrom(const options &src)
{
	// reset ourselves first
	reset();

	// iterate through the src options and make our own
	for (entry *curentry = src.m_entrylist.first(); curentry != nullptr; curentry = curentry->next())
		append_entry(*global_alloc(entry(curentry->name(), curentry->description(), curentry->flags(), curentry->default_value())));
}


//-------------------------------------------------
//  validate_and_set_data - make sure the data is
//  of the appropriate type and within range,
//  then set it
//-------------------------------------------------

bool options::validate_and_set_data(options::entry &curentry, const char *newdata, int priority, std::string &error_string)
{
	// trim any whitespace
	std::string data(newdata);
	strtrimspace(data);

	// trim quotes
	if (data.find('"') == 0 && data.find_last_of('"') == data.length() - 1)
	{
		data.erase(0, 1);
		data.erase(data.length() - 1, 1);
	}

	// validate the type of data and optionally the range
	float fval;
	int ival;
	switch (curentry.type())
	{
		// booleans must be 0 or 1
		case OPTION_BOOLEAN:
			if (sscanf(data.c_str(), "%d", &ival) != 1 || ival < 0 || ival > 1)
			{
				error_string.append(string_format("Illegal boolean value for %s: \"%s\"; reverting to %s\n", curentry.name(), data.c_str(), curentry.value()));
				return false;
			}
			break;

		// integers must be integral
		case OPTION_INTEGER:
			if (sscanf(data.c_str(), "%d", &ival) != 1)
			{
				error_string.append(string_format("Illegal integer value for %s: \"%s\"; reverting to %s\n", curentry.name(), data.c_str(), curentry.value()));
				return false;
			}
			if (curentry.has_range() && (ival < atoi(curentry.minimum()) || ival > atoi(curentry.maximum())))
			{
				error_string.append(string_format("Out-of-range integer value for %s: \"%s\" (must be between %s and %s); reverting to %s\n", curentry.name(), data.c_str(), curentry.minimum(), curentry.maximum(), curentry.value()));
				return false;
			}
			break;

		// floating-point values must be numeric
		case OPTION_FLOAT:
			if (sscanf(data.c_str(), "%f", &fval) != 1)
			{
				error_string.append(string_format("Illegal float value for %s: \"%s\"; reverting to %s\n", curentry.name(), data.c_str(), curentry.value()));
				return false;
			}
			if (curentry.has_range() && (fval < atof(curentry.minimum()) || fval > atof(curentry.maximum())))
			{
				error_string.append(string_format("Out-of-range float value for %s: \"%s\" (must be between %s and %s); reverting to %s\n", curentry.name(), data.c_str(), curentry.minimum(), curentry.maximum(), curentry.value()));
				return false;
			}
			break;

		// strings can be anything
		case OPTION_STRING:
			break;

		// anything else is invalid
		case OPTION_INVALID:
		case OPTION_HEADER:
		default:
			error_string.append(string_format("Attempted to set invalid option %s\n", curentry.name()));
			return false;
	}

	// set the data
	curentry.set_value(data.c_str(), priority);
	return true;
}
