// Local configuration
#include "config.h"

// Implementation
#include "CmdLine.hpp"

// STDC++
#include <sstream>
#include <fstream>
#include <memory>
#include <cassert>

// C Libraries
#ifndef WIN32
#include <readline/readline.h>
#include <readline/history.h>
#endif

// Local
#include "Sys.hpp"
#include "Text.hpp"
#include "Misc.hpp"


#define UI_UTIL_CMDLINE_SEPLINE0 "======================================================================"
#define UI_UTIL_CMDLINE_SEPLINE1 "----------------------------------------------------------------------"

namespace UI {
namespace Util {
namespace CmdLine {

Cmd::Cmd(std::string const & name,
         std::string const & help)
	:cl_(0)
	,name_(name)
	,help_(help)
	,args_()
	,minArgs_(0)
	,tokens_()
	,isParsed_(false)
{
	assert(name_ != "");
}

Cmd::~Cmd() {}

std::string Cmd::getName() const
{
	return name_;
}

void Cmd::addArg(std::string const & name, std::string const & help)
{
	args_.push_back(make_pair(name, help));
	minArgs_ = args_.size();
}

void Cmd::addOptArg(std::string const & name, std::string const & help)
{
	args_.push_back(make_pair(name, help));
}

int Cmd::getMinArgs() const {	return minArgs_; }
int Cmd::getMaxArgs() const {	return args_.size(); }

std::string Cmd::getSyntax() const
{
	std::string result(name_);
	for (int i(0); i < (int) args_.size(); ++i)
	{
		result += " " + getArgString(i);
	}
	return result;
}

std::string Cmd::getHelp(bool showArgs) const
{
	std::string result("");
	if (name_ == "__header")
	{
		result += "\n" + help_ + "\n" + UI_UTIL_CMDLINE_SEPLINE1 + "\n";
	}
	else
	{
		result += getSyntax() + "\n   " + help_ + ".\n";
		if (showArgs)
		{
			for (int i(0); i < (int) args_.size(); ++i)
			{
				result += "   " + getArgString(i) + "\t: " + args_[i].second + ".\n";
			}
		}
	}
	return result;
}

bool Cmd::parse(std::string const & line)
{
	tokens_.clear();
	std::string l(line);
	stripLine(l);
	while (l.length() != 0)
	{
		// Skip leading spaces
		l.erase(0, l.find_first_not_of(spaces_));

		// Get token
		if (l[0] == '"')
		{
			// Argument embraced with "
			l.erase(0, 1);
			std::string::size_type p(l.find_first_of('"'));
			if (p == std::string::npos)
			{
				tokens_.push_back('"' + l.substr(0, p));  // unterminated
			}
			else
			{
				tokens_.push_back(l.substr(0, p));        // terminated
				++p;
			}
			l.erase(0, p);
		}
		else
		{
			std::string::size_type p(l.find_first_of(spaces_));
			std::string token(l.substr(0, p));
			if (token != "")
			{
				tokens_.push_back(token);
			}
			l.erase(0, p);
		}
	}
	isParsed_ = (tokens_.size() > 0 &&
	             tokens_[0] == getName() &&
	             (int) tokens_.size() > getMinArgs() &&
	             (int) tokens_.size() <= getMaxArgs()+1);

	return isParsed_;
}

bool Cmd::isParsed() const { return isParsed_; }

std::string Cmd::getArg(int i) const
{
	assert(cl_);
	assert(isParsed_);
	assert(i <= getMaxArgs());

	if (i < (int) tokens_.size())
	{
		if (tokens_[i].length() > 0 && tokens_[i][0] == '$')
		{
			// CLI expansion
			return cl_->getVar(tokens_[i].substr(1, std::string::npos));
		}
		else
		{
			// Straight argument
			return tokens_[i];
		}
	}
	else
	{
		return "";		// Not given optional argument
	}
}

int Cmd::run()
{
	assert(cl_);
	assert(isParsed_);
	assert(getArg(0) == getName());

	try { return runCmd(); }
	catch (...)
	{
		cl_->es() << "Unknown exception catched when running: " << getName() << "." << std::endl;
		return 20;
	}
}

void Cmd::setCL(CmdLine * cl)
{
	assert(cl);
	cl_ = cl;
}

std::string Cmd::getArgString(int i) const
{
	assert(i < (int)args_.size());

	std::string result("");
	if (i >= minArgs_)
	{
		result += "[";
	}
	result += "<" + args_[i].first + ">";
	if (i >= minArgs_)
	{
		result += "]";
	}
	return result;
}

std::string Cmd::stripLine(std::string const & line)
{
	std::string l(line);
	l.erase(0, l.find_first_not_of(spaces_));
	l.erase(l.find_last_not_of(spaces_)+1, std::string::npos);
	return l;
}

std::string Cmd::commandFromLine(std::string const & line)
{
	return stripLine(line).substr(0, line.find_first_of(Cmd::spaces_));
}

std::string const Cmd::spaces_(" \t\n\r\f\v");

HeaderCmd::HeaderCmd(std::string const & header)
	:Cmd("__header", header)
{}

int
HeaderCmd::runCmd()
{
	return 1;
}

/** @brief Internal command: Exit command line. */
class ExitCmd: public Cmd
{
public:
	ExitCmd()
		:Cmd("exit", "Leave session")
	{}
private:
	int runCmd()
	{
		cl_->setVar("__EXIT", "true");
		return 0;
	}
};

/** @brief Internal command: Get help. */
class HelpCmd: public Cmd
{
public:
	HelpCmd()
		:Cmd("help", "Show command help")
	{
		addOptArg("command", "Get more help for specific command");
	}
private:
	int runCmd()
	{
		int result(0);

		if (getArg(1) == "")
		{
			cl_->os() << "\nHelp for " << cl_->getVar("__TITLE") << std::endl;
			cl_->os() << UI_UTIL_CMDLINE_SEPLINE0 << std::endl;
			for (std::vector<Cmd *>::iterator i(cl_->commands_.begin()); i != cl_->commands_.end(); ++i)
			{
				cl_->os() << (*i)->getHelp(false);
			}
			cl_->os() << UI_UTIL_CMDLINE_SEPLINE0 << std::endl;
		}
		else
		{
			Cmd * c(cl_->findCmd(getArg(1)));
			if (c)
			{
				cl_->os() << c->getHelp(true);
			}
			else
			{
				cl_->es() << "Unknown command: " << getArg(1) << std::endl;
				result = 1;
			}
		}
		return result;
	}
};

/** @brief Internal command: Set a variable. */
class SetCmd: public Cmd
{
public:
	SetCmd()
		:Cmd("set", "Show or set variables; shows all variables without any argument.\n   Use \"set -e\" to enable or \"set +e\" to disable fatal errors, respectively")
	{
		addOptArg("name",  "Get value for variable <name>");
		addOptArg("value", "Set value for variable <name> to <value>");
	}
private:
	int runCmd()
	{
		if (getArg(1) == "-e")
		{
			cl_->setVar("__FATAL", "true");    // like bash's "set -e"
		}
		else if (getArg(1) == "+e")
		{
			cl_->setVar("__FATAL", "false");   // like bash's "set +e"
		}
		else if (getArg(1) == "")
		{
			cl_->showVars();                   // "set": shows all variables
		}
		else if (getArg(2) == "")
		{
			// "set ARG1"
			cl_->showVar(getArg(1));         // "set var": shows var
		}
		else
		{
			// "set ARG1 ARG2"
			cl_->setVar(getArg(1), getArg(2));
		}
		return 0;
	}
};

/** @brief Internal command: Source another script. */
class SourceCmd: public Cmd
{
public:
	SourceCmd()
		:Cmd("source", "Source (load and run all lines of) file")
	{
		addArg("file",  "File to source");
	}
private:
	int runCmd()
	{
		std::ifstream * f = new std::ifstream(getArg(1).c_str(), std::ios::in);
		if (f && f->is_open())
		{
			// Avoid leak with this bool flag ;)
			if (cl_->isNeedsDeletion_)
			{
				delete cl_->is_;
			}
			cl_->is_ = f;
			cl_->isNeedsDeletion_ = true;
			return 0;
		}
		else
		{
			cl_->es() << "File open error: " << getArg(1) << std::endl;
			return 1;
		}
	}
};

/** @brief Internal command: Sleep for n seconds. */
class SleepCmd: public Cmd
{
public:
	SleepCmd()
		:Cmd("sleep", "Delay execution for some time")
	{
		addOptArg("seconds",  "Seconds to sleep");
	}
private:
	int runCmd()
	{
		return Sys::sleep(ato<unsigned int>(getArg(1)));
	}
};

/** @brief Internal command: Print text. */
class EchoCmd: public Cmd
{
public:
	EchoCmd()
		:Cmd("echo", "Print text to stdout")
	{
		addOptArg("text",  "Text to print");
	}
private:
	int runCmd()
	{
		cl_->os() << getArg(1) << std::endl;
		return 0;
	}
};


CmdLine::CmdLine(std::istream * is,
                 std::ostream * os,
                 std::ostream * es,
                 std::string const & title,
                 std::string const & prompt)
	:is_(is)
	,isNeedsDeletion_(false)
	,os_(os)
	,es_(es)
	,commands_()
	,variables_()
{
	assert(os_);
	assert(es_);

	// Variables that influence CmdLine's behaviour
	setVar("__TITLE", title);
	setVar("__PROMPT", prompt);
	setVar("__ECHO", "false");
	setVar("__LAST_ERROR", "0");
	setVar("__ERRORS", "0");
	setVar("__FATAL", "false");
	setVar("__EXIT", "false");


	// Set generic commands
	add(new HeaderCmd("Generic command line functions"));
	add(new ExitCmd());
	add(new HelpCmd());
	add(new SetCmd());
	add(new SourceCmd());
	add(new SleepCmd());
	add(new EchoCmd());
}

CmdLine::~CmdLine()
{
	delAnySeqContainer(commands_);
}

void CmdLine::add(Cmd * cmd)
{
	assert(cmd);
	commands_.push_back(cmd);
	cmd->setCL(this);
}

Cmd * CmdLine::findCmd(std::string const & name) const
{
	for (std::vector<Cmd *>::const_iterator i(commands_.begin()); i != commands_.end(); ++i)
	{
		if ((*i)->getName() == name)
		{
			return *i;
		}
	}
	return 0;
}

std::ostream & CmdLine::os() { return *os_; }
std::ostream & CmdLine::es() { return *es_; }

std::string CmdLine::readLine(std::string const & promptVar)
{
	std::string result("");

	os() << getVar(promptVar);
	if (is_)
	{
		// Use standard istream
		char line[320];
		is_->getline(line, 320);
		result = line;
	}
	else
	{
		// Interactive; use readline/history
		char * line(::readline(""));
		if (line)
		{
			result = line;
			std::free(line);
			Cmd::stripLine(result);
			if (result != "")
			{
				::add_history(result.c_str());
			}
		}
	}
	return result;
}

std::string CmdLine::getVar(std::string const & key) const
{
	std::map<std::string, std::string>::const_iterator i(variables_.find(key));
	if (i == variables_.end())
	{
		return "";
	}
	else
	{
		return (*i).second;
	}
}

void CmdLine::setVar(std::string const & key, std::string const & value)
{
	std::map<std::string, std::string>::iterator i(variables_.find(key));
	if (i == variables_.end())
	{
		std::pair<std::map<std::string, std::string>::iterator, bool> result(variables_.insert(std::make_pair(key, value)));
		assert(result.second);
	}
	else
	{
		(*i).second = value;
	}
}

void CmdLine::showVar(std::map<std::string, std::string>::iterator i)
{
	if (i != variables_.end())
	{
		os() << (*i).first << "=\"" << (*i).second << "\"";
	}
	else
	{
		os() << "Variable not set.";
	}
	os() << std::endl;
}

void CmdLine::showVar(std::string const & key)
{
	std::map<std::string, std::string>::iterator i(variables_.find(key));
	showVar(i);
}

void CmdLine::showVars()
{
	for (std::map<std::string, std::string>::iterator i(variables_.begin()); i != variables_.end(); ++i)
	{
		showVar(i);
	}
}

int CmdLine::run()
{
	os() << std::endl << getVar("__TITLE") << std::endl;
	os() << UI_UTIL_CMDLINE_SEPLINE0 << std::endl;
	os() << "\nType \"help\" for command overview." << std::endl;

	while (getVar("__EXIT") != "true")
	{
		std::string line(readLine());
		if (getVar("__ECHO") == "true")
		{
			os() << line << std::endl;
		}

		if (line.length() != 0 && line[0] != '#' && line[0] != '\n') // empty lines or comments
		{
			setVar("__LAST_ERROR", "0");

			// Find command
			Cmd * cmd(findCmd(Cmd::commandFromLine(line)));

			if (cmd)
			{
				if (cmd->parse(line))
				{
					int exitCode(cmd->run());
					if (exitCode != 0)
					{
						es() << "Error: \"" << line << "\"  failed with exit code " << exitCode << "." << std::endl;
						setVar("__LAST_ERROR", tos<int>(exitCode));
					}
				}
				else
				{
					// Syntax error
					os() << "\nParser error in: \"" << line << "\":\n\n";
					es() << cmd->getHelp(true) << std::endl;
					setVar("__LAST_ERROR", "5");
				}
			}
			else
			{
				es() << "Unknown command." << std::endl;
				setVar("__LAST_ERROR", "10");
			}

			// Error counter
			if (getVar("__LAST_ERROR") != "0")
			{
				setVar("__ERRORS", tos<int>(ato<int>(getVar("__ERRORS"))+1));
			}
		}

		// Change back to interactive mode if stream is done
		if (is_ && is_->eof())
		{
			is_ = 0;
		}

		if (getVar("__FATAL") == "true" && ato<int>(getVar("__LAST_ERROR")) != 0)
		{
			es() << "Exiting on fatal error." << std::endl;
			setVar("__EXIT", "true");
			setVar("__ERRORS", tos<int>(-1 * ato<int>(getVar("__ERRORS"))));
		}
	}
	return ato<int>(getVar("__ERRORS"));
}

}}}
