//------------------------------------------------------------------------------
// <copyright file="LqrSMACCmdLineParser.cpp" company="LiquidRobotics">
//
// Copyright (c) 2014 Liquid Robotics, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This software is associated with a release.  Liquid Robotics makes no
// representation that the design, interfaces or component distribution will
// remain unchanged in future releases.
//
// </copyright>
//
// <summary>
// 	Declaration of CmdLineParser, a generic parser that takes a string as input
//  and produces tokens, respecting quotes and escape characters
// </summary>
//
// <owner>Mike Cookson</owner>
//------------------------------------------------------------------------------

#include "LqrSMACCmdLineParser.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <stdexcept>
#include <sstream>
#include <boost/algorithm/string.hpp>

using namespace std;

namespace LiquidRobotics {
namespace SMACFramework {

CmdLineParser::CmdLineParser(const string& cmdLine)
: originalCmdLine(cmdLine)
{
	Parse(cmdLine, tokens, positions);
}

CmdLineParser::CmdLineParser(const string& cmdLine, const string& commentDef)
: originalCmdLine(cmdLine)
{
	Parse(cmdLine, tokens, comment, commentDef);
}

const CmdLineParser::string& CmdLineParser::operator[](size_t index) const
{
	return tokens[index];
}

int CmdLineParser::Count() const
{
	return tokens.size();
}

const CmdLineParser::string& CmdLineParser::GetOriginalCmdLine() const
{
	return originalCmdLine;
}

const CmdLineParser::StringArray& CmdLineParser::GetTokens() const
{
	return tokens;
}

const CmdLineParser::string& CmdLineParser::GetComment() const
{
	return comment;
}

//#define TOKEN_STATE_OUTPUTS

struct TokenState {

	TokenState(const string& _cmdLine)
	: cmdLine(_cmdLine)
	, first(true)
	, pos(0)
	, lastAccepted(0)
	, firstPos(0)
	{
		ci = cmdLine.begin();
	}

	void accept()
	{
#ifdef TOKEN_STATE_OUTPUTS
		cout << "accept" << endl;
#endif
		if (first) { first = false; firstPos = pos; }
		lastAccepted = pos;
	}

	void accept(char c)
	{
#ifdef TOKEN_STATE_OUTPUTS
		cout << "accept " << c << endl;
#endif
		accept();
		token.put(c);
	}

	bool end()
	{
		return ci == cmdLine.end();
	}

	char get()
	{
		char c = 0;

		if (ci != cmdLine.end()) {
			pos = ci-cmdLine.begin();
			c = *ci++;
		}

#ifdef TOKEN_STATE_OUTPUTS
		cout << "get: " << c << endl;
#endif
		return c;
	}

	string getToken(std::pair<size_t,size_t>& position)
	{
		const string result = token.str();
		position.first = firstPos;
		position.second = lastAccepted;
		reset();
#ifdef TOKEN_STATE_OUTPUTS
		cout << "getToken returning <" << position.first << "," << position.second << "> \"" << result << "\"" << endl;
#endif
		return result;
	}

	void reset()
	{
#ifdef TOKEN_STATE_OUTPUTS
		cout << "reset" << endl;
#endif
		first = true;
		firstPos = 0;
		token.str("");
	}

	const string& cmdLine;
	bool first;
	string::const_iterator ci;
	size_t pos;
	size_t lastAccepted;
	size_t firstPos;
	stringstream token;
};

void CmdLineParser::Parse(const string& cmdLine, StringArray& result, PositionPairArray& positions)
{
	stringstream escSeq;
	bool inEsc = false;
	bool inQuote = false;
	bool haveTok = false;
	char quoteChar = '\"';
	int escSeqLen = 0;
	int escSeqSoFar = 0;

	result.clear();
	positions.clear();

	TokenState ts(cmdLine);

	while(!ts.end())
	{
		char c = ts.get();

		if (inEsc)
		{
			if (!escSeqSoFar)
			{
				++escSeqSoFar;
				ts.accept();
				escSeq << c;

				switch(c)
				{
				case 'X':
				case 'x':

					escSeqLen = 3;
					break;

				case '0':
				case '1':
				case '2':
				case '3':

					escSeqLen = 4;
					break;

				default:

					escSeqLen = 1;
					break;
				}
			}

			if (escSeqLen <= escSeqSoFar)
			{
				string escSeq0 = escSeq.str();
				long num;
				char* endPtr = NULL;

				inEsc = false;

				switch(escSeq0[0])
				{
				case '0':
				case '1':
				case '2':
				case '3':

					num = strtol(escSeq0.c_str(), &endPtr, 8);

					if (endPtr && *endPtr)
						throw invalid_argument("Invalid octal escape sequence");
					else
						c = (char) num;

					break;

				case 'n':		c = '\n';			break;
				case 'r':		c = '\r';			break;
				case 't':		c = '\t';			break;

				case 'X':
				case 'x':

					num = strtol(escSeq0.c_str(), &endPtr, 16);

					if (endPtr && *endPtr)
						throw invalid_argument("Invalid hex escape sequence");
					else
						c = (char) num;

					break;
				}

				ts.accept(c);
			}
		}
		else if (c == '\\')
		{
			ts.accept();
			inEsc = true;
			escSeqSoFar = 0;
			escSeq.str("");
		}
		else if (!inQuote)
		{
			if ((c == '\"') || (c == '\''))
			{
				ts.accept();
				haveTok = inQuote = true;
				quoteChar = c;
			}
			else if (!isspace(c))
				ts.accept(c);
			else
			{
				PositionPair position;
				string tokStr = ts.getToken(position);

				if (tokStr.length() || haveTok)
				{
					haveTok = false;
					result.push_back(tokStr);
					positions.push_back(position);
				}
			}
		}
		else if (c == quoteChar)
		{
			inQuote = false;
			ts.accept();
		}
		else
			ts.accept(c);
	}

	if (inEsc)
		throw invalid_argument("Unexpected end of string while in escape sequence");
	else if (inQuote)
		throw invalid_argument("Unexpected end of string while in quoted string");
	else
	{
		PositionPair position;
		string tokStr = ts.getToken(position);

		if (tokStr.length() || haveTok) {
			result.push_back(tokStr);
			positions.push_back(position);
		}
	}
}

inline bool CommentMatches(string::const_iterator ci, string::const_iterator ciEnd, const string& commentDef)
{
	size_t numRemaining = ciEnd-ci;
	size_t tempSize = min(numRemaining,commentDef.length());
	string temp(ci,ci+tempSize);

	return boost::equals(temp, commentDef);
}

void CmdLineParser::Parse(const string& cmdLine, StringArray& result, string& comment, const string& commentDef)
{
	stringstream token, escSeq, remark;
	bool inEsc = false;
	bool inQuote = false;
	bool haveTok = false;
	char quoteChar = '\"';
	int escSeqLen = 0;
	int escSeqSoFar = 0;
	bool inComment = false;

	result.clear();
	comment.clear();
	//cout << "cmdLine = " << cmdLine << endl;

	for (string::const_iterator it = cmdLine.begin(); it != cmdLine.end(); )
	{
		char c = *it++;

		// cout << "c = " << c << endl;

		if (inEsc)
		{
			if (!escSeqSoFar)
			{
				++escSeqSoFar;
				escSeq << c;

				switch(c)
				{
				case 'X':
				case 'x':

					escSeqLen = 3;
					break;

				case '0':
				case '1':
				case '2':
				case '3':

					escSeqLen = 4;
					break;

				default:

					escSeqLen = 1;
					break;
				}
			}

			if (escSeqLen <= escSeqSoFar)
			{
				string escSeq0 = escSeq.str();
				long num;
				char* endPtr = NULL;

				inEsc = false;

				switch(escSeq0[0])
				{
				case '0':
				case '1':
				case '2':
				case '3':

					num = strtol(escSeq0.c_str(), &endPtr, 8);

					if (endPtr && *endPtr)
						throw invalid_argument("Invalid octal escape sequence");
					else
						c = (char) num;

					break;

				case 'n':
					c = '\n';
					break;
				case 'r':
					c = '\r';
					break;
				case 't':
					c = '\t';
					break;

				case 'X':
				case 'x':

					num = strtol(escSeq0.c_str(), &endPtr, 16);

					if (endPtr && *endPtr)
						throw invalid_argument("Invalid hex escape sequence");
					else
						c = (char) num;

					break;
				}
			}

			token << c;
		}
		else if (c == '\\')
		{
			inEsc = true;
			escSeqSoFar = 0;
			escSeq.str("");
		}
		else if (inComment)
			remark << c;
		else if (!inQuote)
		{
			if ((c == '\"') || (c == '\''))
			{
				haveTok = inQuote = true;
				quoteChar = c;
			}
			else if (!isspace(c) && !inComment)
			{
				if (CommentMatches(it-1,cmdLine.end(),commentDef))
				{
					inComment = true;

					string::const_iterator ciBegin = it-1;
					it = ciBegin+commentDef.length();
					remark << string(ciBegin,it);

					// no space before comment
					string tokStr = token.str();
					if (tokStr.length() || haveTok)
					{
						haveTok = false;
						token.str("");
						result.push_back(tokStr);
					}
				}
				else
					token << c;
			}
			else
			{
				string tokStr = token.str();

				if (tokStr.length() || haveTok)
				{
					haveTok = false;
					token.str("");
					result.push_back(tokStr);
				}
			}
		}
		else if (c == quoteChar)
			inQuote = false;
		else
			token << c;
	}

	if (inEsc)
		throw invalid_argument("Unexpected end of string while in escape sequence");
	else if (inQuote)
		throw invalid_argument("Unexpected end of string while in quoted string");
	else if (inComment)
		comment = remark.str();
	else
	{
		string tokStr = token.str();

		if (tokStr.length() || haveTok)
			result.push_back(tokStr);
	}

	//cout << "tokens = " << result.size() << endl;
	//cout << "comment = " << comment << endl;
}


string CmdLineParser::MakeCmdLine() const
{
	return MakeCmdLine(tokens);
}

string CmdLineParser::MakeCmdLine(const StringArray& tokens)
{
	stringstream cmdLine;
	bool first = true;

	for (StringArray::const_iterator it = tokens.begin(); it != tokens.end(); ++it)
	{
		if (first)
			first = false;
		else
			cmdLine << " ";

		const string& token = *it;

		for (string::const_iterator it2 = token.begin(); it2 != token.end(); ++it2)
		{
			const char c = *it2;

			if (strchr("\'\"\\", c))
				cmdLine << "\\" << c;
			else
				cmdLine << c;
		}
	}

	return cmdLine.str();
}

void CmdLineParser::SetTokens(const StringArray& newValue)
{
	tokens.assign(newValue.begin(), newValue.end());
}

string CmdLineParser::GetTail(size_t index) const
{
	string result;
	if (index < positions.size())
		result = originalCmdLine.substr(positions[index].second+1);
	return result;
}

}
}
