//-------------------------------------------------------------------------------------------------
// <copyright file="ArgumentParser.cpp" company="LiquidRobotics">
//  Copyright (c) Liquid Robotics Corporation, confidential and proprietary.  All rights reserved. 
// </copyright>
//
// <summary>
//   Implementation of the ArgumentParser class, which is part of the Logger program.
//   It parses the command line arguments and fills out associated settings
//   variables.
// </summary>
//
// <owner>Mike Cookson</owner>
//-------------------------------------------------------------------------------------------------

#include "ArgumentParser.h"
#include <list>
#include <map>
#include <string>
#include <vector>
#include <boost/algorithm/string.hpp>

using namespace std;
using namespace LiquidRobotics::SMACFramework;

IMPLEMENT_INNER_EXCEPTION(ArgumentParser, HelpFileOutput, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(ArgumentParser, ProgramArgError, std::runtime_error);

enum SettingsType
{
	ST_ARCHIVE_DIR,
	ST_COMPRESSION,
	ST_DEBUG,
	ST_EXTENSION,
	ST_KEEP_WRITES_TOGETHER,
	ST_MAX_BYTES_QUEUED,
	ST_MAX_FILE_SIZE,
	ST_MAX_FILES,
	ST_MAX_RETAIN_TIME,
	ST_PREFIX,
	ST_PURGE_STRATEGY,
	ST_WORK_DIR,
};

typedef list<string> StringList;
typedef vector<string> StringVector;
typedef map<string,SettingsType> StringToSettingMap;
typedef map<SettingsType,string> SettingsMap;

static void ThrowHelp()
{
	stringstream msg;
	msg << "Logger { ... options ... }" << endl
	    << endl
	    << "Options can be specified as a collection of single-letter shortcuts preceeded by a dash and followed" << endl
	    << "by tokens to use as arguments or as long-form options preceeded by double-dashes with arguments" << endl
	    << "formatted as assignments.  For example:" << endl
	    << endl
	    << "    Logger -wc /my-partition/workfiles BZIP2 --prefix=MYLOG --extension=.EXT --max-files 10 --max-file-size 1000000" << endl
	    << endl
	    << "The 'w' and 'c' short form options are assigned '/my-partition/workfiles' and 'BZIP2'; the --prefix" << endl
	    << "option is assigned 'MYLOG'; the --extension option is assigned '.EXT'; the --max-files option is" << endl
	    << "assigned '10'; and the --max-file-size option is assigned '1000000'" << endl
	    << endl
	    << "The available options are as follows:" << endl
	    << endl
	    << "-a, --archive-dir            The directory where log archives are stored (defaults to work directory)" << endl
	    << endl
	    << "-c, --archive-compression    The type of archive compression to use: BZIP2, GZIP or NONE" << endl
	    << endl
	    << "-e, --extension              The extension used for each output file (before compression)" << endl
	    << endl
	    << "-f, --max-files              The maximum number of files (archives+work file) to retain" << endl
	    << endl
	    << "-g, --purge-strategy         The strategy to use when purging files: OLDEST or NONE" << endl
	    << endl
	    << "-k, --keep-writes-together   Allow lines to break across files (0) or keep writes/lines together (1)" << endl
	    << endl
	    << "-p, --prefix                 The first part of the file name for each file in the set" << endl
	    << endl
	    << "-q, --max-bytes-queued       The size of the inter-thread write queue in bytes (larger value delays the" << endl
	    << "                             logging client less)" << endl
	    << endl
	    << "-r, --max-retain-time        The maximum number of seconds to retain archives (0=no limit)" << endl
	    << endl
	    << "-s, --max-file-size          The maximum size of each file (before compression)" << endl
	    << endl
	    << "-w, --work-dir               The directory for the work file and default for archive files; starting" << endl
	    << "                             directory if not specified" << endl
			;

	string msgStr = msg.str();
	throw ArgumentParser::HelpFileOutput(msgStr);
}


static void Parse0(const string& arg0, StringList& args, StringToSettingMap& argsMap, SettingsMap& settingsMap, bool& debugMode, bool enableEquality)
{
	using namespace boost;
	string lhs = arg0, rhs;
	stringstream msg;
	bool pullNextArg = true;

	if (enableEquality)
	{
		StringVector items;

		split(items, arg0, is_any_of("="), token_compress_off);

		StringToSettingMap::iterator it;
		const StringVector::size_type itemsSize = items.size();

		if (itemsSize == 2)
		{
			lhs = items[0];
			rhs = items[1];
			pullNextArg = false;
		}

		else if (itemsSize != 1)
		{
			msg << "Invalid equation '" << arg0 << "'";
			throw ArgumentParser::ProgramArgError(msg.str());
		}
	}

	StringToSettingMap::iterator it = argsMap.find(lhs);

	if (it == argsMap.end())
	{
		msg << "No match for option '" << arg0 << "'";
		throw ArgumentParser::ProgramArgError(msg.str());
	}

	SettingsType result = it->second;

	if (pullNextArg)
	{
		if (!args.size())
		{
			msg << "No remaining arguments for assignment at option '" << arg0 << "'";
			throw ArgumentParser::ProgramArgError(msg.str());
		}

		rhs = args.front();
		args.erase(args.begin());
	}

	if (!rhs.size())
	{
		msg << "Nil assignment at option '" << arg0 << "'";
		throw ArgumentParser::ProgramArgError(msg.str());
	}

	if (settingsMap.find(result) != settingsMap.end())
	{
		msg << "Duplicate assignment at option '" << arg0 << "'";
		throw ArgumentParser::ProgramArgError(msg.str());
	}

	settingsMap[result] = rhs;
}

static void Parse(StringList& args, StringToSettingMap& shortMap, StringToSettingMap& longMap, bool& debugMode, FileSetSettings& settings)
{
	using namespace boost;
	SettingsMap settingsMap;
	stringstream msg;

	while(!args.empty())
	{
		const string arg = args.front();
		args.erase(args.begin());

		if (iequals(arg, "--help"))
			ThrowHelp();

		else if (starts_with(arg, "--"))
		{
			string tail = arg.substr(2);
			Parse0(tail, args, longMap, settingsMap, debugMode, true);
		}
		else if (starts_with(arg, "-"))
		{
			if ((arg.find('?') != string::npos)
			 || (arg.find('h') != string::npos)
			 || (arg.find('H') != string::npos))

				ThrowHelp();

			const string::size_type argLength = arg.length();

			for (string::size_type idx = 1; idx < argLength; ++idx)
			{
				string item = arg.substr(idx,1);
				Parse0(item, args, shortMap, settingsMap, debugMode, false);
			}
		}
		else
		{
			msg << "Unrecognized argument '" << arg << "'";
			throw ArgumentParser::ProgramArgError(msg.str());
		}
	}

	for (SettingsMap::iterator it = settingsMap.begin(); it != settingsMap.end(); ++it)
	{
		switch(it->first)
		{
		case ST_ARCHIVE_DIR:			settings.SetArchiveDirectory(it->second);													break;
		case ST_COMPRESSION:			settings.SetArchiveCompressionType(FileSetSettings::CompressionTypeFromString(it->second));	break;
		case ST_DEBUG:					debugMode = !!lexical_cast<unsigned>(it->second);											break;
		case ST_EXTENSION:				settings.SetExtension(it->second);															break;
		case ST_KEEP_WRITES_TOGETHER:	settings.SetKeepWritesTogether(!!lexical_cast<unsigned>(it->second));						break;
		case ST_MAX_BYTES_QUEUED:		settings.SetMaxBytesQueued(lexical_cast<unsigned>(it->second));								break;
		case ST_MAX_FILE_SIZE:			settings.SetMaxFileSize(lexical_cast<unsigned>(it->second));								break;
		case ST_MAX_FILES:				settings.SetMaxFiles(lexical_cast<unsigned>(it->second));									break;
		//case ST_MAX_RETAIN_TIME:		settings.SetMaxRetainTime(FileSetSettings::TimeSpanFromString(it->second));					break;
		case ST_MAX_RETAIN_TIME:		settings.SetMaxRetainTime(FileSetSettings::TimeSpan(0,0,lexical_cast<unsigned>(it->second),0));	break;
		case ST_PREFIX:					settings.SetPrefix(it->second);																break;
		case ST_PURGE_STRATEGY:			settings.SetPurgeStrategy(FileSetSettings::PurgeStrategyFromString(it->second));			break;
		case ST_WORK_DIR:				settings.SetWorkingDirectory(it->second);													break;
		default:						msg << "Invalid switch case " << (int) it->first; throw logic_error(msg.str());
		}
	}
}

ArgumentParser::ArgumentParser(int argc, char* argv[])
: debugMode(false)
{
	using namespace boost;
	StringList args;

	for (int idx = 1; idx < argc; ++idx)
		args.push_back(argv[idx]);

	StringToSettingMap shortMap;
	StringToSettingMap longMap;

	shortMap["a"] = ST_ARCHIVE_DIR;				longMap["archive-dir"] = ST_ARCHIVE_DIR;
	shortMap["c"] = ST_COMPRESSION;				longMap["archive-compression"] = ST_COMPRESSION;
	shortMap["D"] = ST_DEBUG;					longMap["debug"] = ST_DEBUG;
	shortMap["e"] = ST_EXTENSION;				longMap["extension"] = ST_EXTENSION;
	shortMap["k"] = ST_KEEP_WRITES_TOGETHER;	longMap["keep-writes-together"] = ST_KEEP_WRITES_TOGETHER;
	shortMap["q"] = ST_MAX_BYTES_QUEUED;		longMap["max-bytes-queued"] = ST_MAX_BYTES_QUEUED;
	shortMap["f"] = ST_MAX_FILES;				longMap["max-files"] = ST_MAX_FILES;
	shortMap["s"] = ST_MAX_FILE_SIZE;			longMap["max-file-size"] = ST_MAX_FILE_SIZE;
	shortMap["r"] = ST_MAX_RETAIN_TIME;			longMap["max-retain-time"] = ST_MAX_RETAIN_TIME;
	shortMap["p"] = ST_PREFIX;					longMap["prefix"] = ST_PREFIX;
	shortMap["w"] = ST_WORK_DIR;				longMap["work-dir"] = ST_WORK_DIR;
	shortMap["g"] = ST_PURGE_STRATEGY;			longMap["purge-strategy"] = ST_PURGE_STRATEGY;

	Parse(args, shortMap, longMap, debugMode, settings);
}

const FileSetSettings& ArgumentParser::Settings() const
{
	return settings;
}

bool ArgumentParser::DebugMode() const
{
	return debugMode;
}
