/*-------------------------------------------------------------------------
 * drawElements C++ Base Library
 * -----------------------------
 *
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *//*!
 * \file
 * \brief Command line parser.
 *//*--------------------------------------------------------------------*/

#include "deCommandLine.hpp"

#include <set>
#include <sstream>
#include <cstring>
#include <stdexcept>
#include <algorithm>

namespace de
{
namespace cmdline
{

namespace
{
struct Help { typedef bool ValueType; };
}

namespace detail
{

inline const char* getNamedValueName (const void* namedValue)
{
	return static_cast<const NamedValue<deUint8>*>(namedValue)->name;
}

using std::set;

TypedFieldMap::TypedFieldMap (void)
{
}

TypedFieldMap::~TypedFieldMap (void)
{
	clear();
}

void TypedFieldMap::clear (void)
{
	for (Map::const_iterator iter = m_fields.begin(); iter != m_fields.end(); ++iter)
	{
		if (iter->second.value)
			iter->second.destructor(iter->second.value);
	}
	m_fields.clear();
}

bool TypedFieldMap::contains (const std::type_info* key) const
{
	return m_fields.find(key) != m_fields.end();
}

const TypedFieldMap::Entry& TypedFieldMap::get (const std::type_info* key) const
{
	Map::const_iterator pos = m_fields.find(key);
	if (pos != m_fields.end())
		return pos->second;
	else
		throw std::out_of_range("Value not set");
}

void TypedFieldMap::set (const std::type_info* key, const Entry& value)
{
	Map::iterator pos = m_fields.find(key);

	if (pos != m_fields.end())
	{
		pos->second.destructor(pos->second.value);
		pos->second.value = DE_NULL;

		pos->second = value;
	}
	else
		m_fields.insert(std::make_pair(key, value));
}

Parser::Parser (void)
{
	addOption(Option<Help>("h", "help", "Show this help"));
}

Parser::~Parser (void)
{
}

void Parser::addOption (const OptInfo& option)
{
	m_options.push_back(option);
}

bool Parser::parse (int numArgs, const char* const* args, CommandLine* dst, std::ostream& err) const
{
	typedef map<string, const OptInfo*> OptMap;
	typedef set<const OptInfo*> OptSet;

	OptMap	shortOptMap;
	OptMap	longOptMap;
	OptSet	seenOpts;
	bool	allOk			= true;

	DE_ASSERT(dst->m_args.empty() && dst->m_options.empty());

	for (vector<OptInfo>::const_iterator optIter = m_options.begin(); optIter != m_options.end(); optIter++)
	{
		const OptInfo& opt = *optIter;

		DE_ASSERT(opt.shortName || opt.longName);

		if (opt.shortName)
		{
			DE_ASSERT(shortOptMap.find(opt.shortName) == shortOptMap.end());
			shortOptMap[opt.shortName] = &opt;
		}

		if (opt.longName)
		{
			DE_ASSERT(longOptMap.find(opt.longName) == longOptMap.end());
			longOptMap[opt.longName] = &opt;
		}

		// Set default values.
		if (opt.defaultValue)
			opt.dispatchParse(&opt, opt.defaultValue, &dst->m_options);
		else if (opt.setDefault)
			opt.setDefault(&dst->m_options);
	}

	DE_ASSERT(!dst->m_options.get<Help>());

	for (int argNdx = 0; argNdx < numArgs; argNdx++)
	{
		const char*		arg		= args[argNdx];
		int				argLen	= (int)strlen(arg);

		if (arg[0] == '-' && arg[1] == '-' && arg[2] == 0)
		{
			// End of option list (--)
			for (int optNdx = argNdx+1; optNdx < numArgs; optNdx++)
				dst->m_args.push_back(args[optNdx]);
			break;
		}
		else if (arg[0] == '-')
		{
			const bool				isLongName	= arg[1] == '-';
			const char*				nameStart	= arg + (isLongName ? 2 : 1);
			const char*				nameEnd		= std::find(nameStart, arg+argLen, '=');
			const bool				hasImmValue	= nameEnd != (arg+argLen);
			const OptMap&			optMap		= isLongName ? longOptMap : shortOptMap;
			OptMap::const_iterator	optPos		= optMap.find(string(nameStart, nameEnd));
			const OptInfo*			opt			= optPos != optMap.end() ? optPos->second : DE_NULL;

			if (!opt)
			{
				err << "Unrecognized command line option '" << arg << "'\n";
				allOk = false;
				continue;
			}

			if (seenOpts.find(opt) != seenOpts.end())
			{
				err << "Command line option '--" << opt->longName << "' specified multiple times\n";
				allOk = false;
				continue;
			}

			seenOpts.insert(opt);

			if (opt->isFlag)
			{
				if (!hasImmValue)
				{
					opt->dispatchParse(opt, DE_NULL, &dst->m_options);
				}
				else
				{
					err << "No value expected for command line option '--" << opt->longName << "'\n";
					allOk = false;
				}
			}
			else
			{
				const bool	hasValue	= hasImmValue || (argNdx+1 < numArgs);

				if (hasValue)
				{
					const char*	value	= hasValue ? (hasImmValue ? nameEnd+1 : args[argNdx+1]) : DE_NULL;

					if (!hasImmValue)
						argNdx += 1; // Skip value

					try
					{
						opt->dispatchParse(opt, value, &dst->m_options);
					}
					catch (const std::exception& e)
					{
						err << "Got error parsing command line option '--" << opt->longName << "': " << e.what() << "\n";
						allOk = false;
					}
				}
				else
				{
					err << "Expected value for command line option '--" << opt->longName << "'\n";
					allOk = false;
				}
			}
		}
		else
		{
			// Not an option
			dst->m_args.push_back(arg);
		}
	}

	// Help specified?
	if (dst->m_options.get<Help>())
		allOk = false;

	return allOk;
}

void Parser::help (std::ostream& str) const
{
	for (vector<OptInfo>::const_iterator optIter = m_options.begin(); optIter != m_options.end(); ++optIter)
	{
		const OptInfo& opt = *optIter;

		str << "  ";
		if (opt.shortName)
			str << "-" << opt.shortName;

		if (opt.shortName && opt.longName)
			str << ", ";

		if (opt.longName)
			str << "--" << opt.longName;

		if (opt.namedValues)
		{
			str << "=[";

			for (const void* curValue = opt.namedValues; curValue != opt.namedValuesEnd; curValue = (const void*)((deUintptr)curValue + opt.namedValueStride))
			{
				if (curValue != opt.namedValues)
					str << "|";
				str << getNamedValueName(curValue);
			}

			str << "]";
		}
		else if (!opt.isFlag)
			str << "=<value>";

		str << "\n";

		if (opt.description)
			str << "    " << opt.description << "\n";

		if (opt.defaultValue)
			str << "    default: '" << opt.defaultValue << "'\n";

		str << "\n";
	}
}

void CommandLine::clear (void)
{
	m_options.clear();
	m_args.clear();
}

const void* findNamedValueMatch (const char* src, const void* namedValues, const void* namedValuesEnd, size_t stride)
{
	std::string srcStr(src);

	for (const void* curValue = namedValues; curValue != namedValuesEnd; curValue = (const void*)((deUintptr)curValue + stride))
	{
		if (srcStr == getNamedValueName(curValue))
			return curValue;
	}

	throw std::invalid_argument("unrecognized value '" + srcStr + "'");
}

} // detail

// Default / parsing functions

template<>
void getTypeDefault (bool* dst)
{
	*dst = false;
}

template<>
void parseType<bool> (const char*, bool* dst)
{
	*dst = true;
}

template<>
void parseType<std::string> (const char* src, std::string* dst)
{
	*dst = src;
}

template<>
void parseType<int> (const char* src, int* dst)
{
	std::istringstream str(src);
	str >> *dst;
	if (str.bad() || !str.eof())
		throw std::invalid_argument("invalid integer literal");
}

// Tests

DE_DECLARE_COMMAND_LINE_OPT(TestStringOpt,		std::string);
DE_DECLARE_COMMAND_LINE_OPT(TestStringDefOpt,	std::string);
DE_DECLARE_COMMAND_LINE_OPT(TestIntOpt,			int);
DE_DECLARE_COMMAND_LINE_OPT(TestBoolOpt,		bool);
DE_DECLARE_COMMAND_LINE_OPT(TestNamedOpt,		deUint64);

void selfTest (void)
{
	// Parsing with no options.
	{
		Parser parser;

		{
			std::ostringstream	err;
			CommandLine			cmdLine;
			const bool			parseOk		= parser.parse(0, DE_NULL, &cmdLine, err);

			DE_TEST_ASSERT(parseOk && err.str().empty());
		}

		{
			const char*			args[]		= { "-h" };
			std::ostringstream	err;
			CommandLine			cmdLine;
			const bool			parseOk		= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(err.str().empty()); // No message about -h
		}

		{
			const char*			args[]		= { "--help" };
			std::ostringstream	err;
			CommandLine			cmdLine;
			const bool			parseOk		= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(err.str().empty()); // No message about -h
		}

		{
			const char*			args[]		= { "foo", "bar", "baz baz" };
			std::ostringstream	err;
			CommandLine			cmdLine;
			const bool			parseOk		= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(parseOk && err.str().empty());
			DE_TEST_ASSERT(cmdLine.getArgs().size() == DE_LENGTH_OF_ARRAY(args));

			for (int ndx = 0; ndx < DE_LENGTH_OF_ARRAY(args); ndx++)
				DE_TEST_ASSERT(cmdLine.getArgs()[ndx] == args[ndx]);
		}
	}

	// Parsing with options.
	{
		Parser parser;

		static const NamedValue<deUint64> s_namedValues[] =
		{
			{ "zero",	0		},
			{ "one",	1		},
			{ "huge",	~0ull	}
		};

		parser << Option<TestStringOpt>		("s",	"string",	"String option")
			   << Option<TestStringDefOpt>	("x",	"xyz",		"String option w/ default value",	"foo")
			   << Option<TestIntOpt>		("i",	"int",		"Int option")
			   << Option<TestBoolOpt>		("b",	"bool",		"Test boolean flag")
			   << Option<TestNamedOpt>		("n",	"named",	"Test named opt",	DE_ARRAY_BEGIN(s_namedValues),	DE_ARRAY_END(s_namedValues),	"one");

		{
			std::ostringstream err;
			DE_TEST_ASSERT(err.str().empty());
			parser.help(err);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Default values
		{
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(0, DE_NULL, &cmdLine, err);

			DE_TEST_ASSERT(parseOk);
			DE_TEST_ASSERT(err.str().empty());

			DE_TEST_ASSERT(!cmdLine.hasOption<TestStringOpt>());
			DE_TEST_ASSERT(!cmdLine.hasOption<TestIntOpt>());
			DE_TEST_ASSERT(cmdLine.getOption<TestNamedOpt>() == 1);
			DE_TEST_ASSERT(cmdLine.getOption<TestBoolOpt>() == false);
			DE_TEST_ASSERT(cmdLine.getOption<TestStringDefOpt>() == "foo");
		}

		// Basic parsing
		{
			const char*			args[]	= { "-s", "test value", "-b", "-i=9", "--named=huge" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(parseOk);
			DE_TEST_ASSERT(err.str().empty());

			DE_TEST_ASSERT(cmdLine.getOption<TestStringOpt>() == "test value");
			DE_TEST_ASSERT(cmdLine.getOption<TestIntOpt>() == 9);
			DE_TEST_ASSERT(cmdLine.getOption<TestBoolOpt>());
			DE_TEST_ASSERT(cmdLine.getOption<TestNamedOpt>() == ~0ull);
			DE_TEST_ASSERT(cmdLine.getOption<TestStringDefOpt>() == "foo");
		}

		// End of argument list (--)
		{
			const char*			args[]	= { "--string=foo", "-b", "--", "--int=2", "-b" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(parseOk);
			DE_TEST_ASSERT(err.str().empty());

			DE_TEST_ASSERT(cmdLine.getOption<TestStringOpt>() == "foo");
			DE_TEST_ASSERT(cmdLine.getOption<TestBoolOpt>());
			DE_TEST_ASSERT(!cmdLine.hasOption<TestIntOpt>());

			DE_TEST_ASSERT(cmdLine.getArgs().size() == 2);
			DE_TEST_ASSERT(cmdLine.getArgs()[0] == "--int=2");
			DE_TEST_ASSERT(cmdLine.getArgs()[1] == "-b");
		}

		// Value --
		{
			const char*			args[]	= { "--string", "--", "-b", "foo" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(parseOk);
			DE_TEST_ASSERT(err.str().empty());

			DE_TEST_ASSERT(cmdLine.getOption<TestStringOpt>() == "--");
			DE_TEST_ASSERT(cmdLine.getOption<TestBoolOpt>());
			DE_TEST_ASSERT(!cmdLine.hasOption<TestIntOpt>());

			DE_TEST_ASSERT(cmdLine.getArgs().size() == 1);
			DE_TEST_ASSERT(cmdLine.getArgs()[0] == "foo");
		}

		// Invalid flag usage
		{
			const char*			args[]	= { "-b=true" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Invalid named option
		{
			const char*			args[]	= { "-n=two" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Unrecognized option (-x)
		{
			const char*			args[]	= { "-x" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Unrecognized option (--xxx)
		{
			const char*			args[]	= { "--xxx" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Invalid int value
		{
			const char*			args[]	= { "--int", "1x" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Arg specified multiple times
		{
			const char*			args[]	= { "-s=2", "-s=3" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Missing value
		{
			const char*			args[]	= { "--int" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(!parseOk);
			DE_TEST_ASSERT(!err.str().empty());
		}

		// Empty value --arg=
		{
			const char*			args[]	= { "--string=", "-b", "-x", "" };
			CommandLine			cmdLine;
			std::ostringstream	err;
			bool				parseOk	= parser.parse(DE_LENGTH_OF_ARRAY(args), &args[0], &cmdLine, err);

			DE_TEST_ASSERT(parseOk);
			DE_TEST_ASSERT(err.str().empty());
			DE_TEST_ASSERT(cmdLine.getOption<TestStringOpt>() == "");
			DE_TEST_ASSERT(cmdLine.getOption<TestStringDefOpt>() == "");
			DE_TEST_ASSERT(cmdLine.getOption<TestBoolOpt>());
		}
	}
}

} // cmdline
} // de