// standard headers
#include "Environment.h"
#include "StrException.h"

#include <iostream>
#include <fstream>
#include <cassert>
#include <cstring>
#include <sstream>

using namespace std;

// global environment
//Environment env;

// --------------------------------------------------------------------------
static inline void printEnumOptions(std::ostream &os, const EnumTable& enumTable)
{
	EnumTable::const_iterator i = enumTable.begin();
	for( ; i != enumTable.end(); ++i)
		os << " `" << i->first << "'";
}

// --------------------------------------------------------------------------
Environment::ArgList::iterator Environment::findArg(const std::string &str)
{
	for(ArgList::iterator i=argv.begin(); i!=argv.end(); ++i) 
		if (str == *i) return i;

	return argv.end();
}

// --------------------------------------------------------------------------
void
Environment::printUsage() const
{
	cout << "Usage: " << appname << endl;

	cout << "Command line options are:\n";
	cout << "\t-" << "-H\n" << endl; 
	cout << "\t-" << "-help            This help screen\n";
	cout << "\t-" << "-D <name>=<val>  Set variable 'name' to 'val'.\n";
	cout << "\t-" << "-E envfile       Set the file to read the environment from.\n";
	cout << "\n";

	// Print out all environment variable names
	cout << "Registered variable names are:\n";
	for (OptionTable::const_iterator i = options.begin(); i != options.end(); i++) 
		cout << "\t" << i->second.name << endl;
}

// --------------------------------------------------------------------------
bool Environment::doHelp(void)
{
	ArgList::iterator i1 = findArg("-H");
	ArgList::iterator i2 = findArg("-help");

	if ( i1 != argv.end() || i2 != argv.end() ) 
	{
		printUsage();
		return true;
	}
	else
		return false;
}

// --------------------------------------------------------------------------
void
Environment::doEnvironmentFile(void)
{
	ArgList::iterator i = findArg("-E");

	string envFileName;

	if(i == argv.end())
	{
		// no "-E" option on the command line

		// find the first non-option parameter and use is as the base for the env file name
		if( argv.size()>0 && argv.front().c_str()[0] != '-')
		{
			string envFileName = changeExt(argv.front(), "env");
			readEnvFile( envFileName.c_str() );
		}
	}
	else
	{
		// '-E' appears on the command line
		i = argv.erase(i);
		if( i == argv.end())
			throw "-E is not followed by the environment file name.";
		envFileName = *i;
		argv.erase(i);

		if( readEnvFile( envFileName.c_str() ) == false )
			throw "Could not read environment file specified by the -E option.";
	}
}

// --------------------------------------------------------------------------
bool
Environment::readEnvFile(const char *envFilename)
{
	static const int MaxStringLength = 1024;
	char buff[MaxStringLength], name[MaxStringLength];
	char *s, *t;
	int line = 0;
	ifstream envStream(envFilename);

	// an error had occured
	if (envStream.fail()) 
	{
		cerr << "Could not open \"" << envFilename << "\" for reading (err. "
			<< envStream.rdstate() << ").\n";
		return false;
	}

	name[0] = '\0';

	try
	{
		// main loop
		for (;;) 
		{
			// read in one line
			envStream.getline(buff, MaxStringLength-1);
			if (!envStream) break;
			line++;
			// get rid of comments
			s = strchr(buff, '#');
			if (s != NULL)
				*s = '\0';

			// get one identifier
			s = parseString(buff, name);
			// parse line
			while (s != NULL) 
			{
				if (*s == '{') 
				{   // it's a group name - make the full name
					strcat(name, ".");
					s++;
					s = parseString(s, name);
					continue;
				}

				if (*s == '}') 
				{   
					// end of group
					if (strlen(name) == 0)
						throw StrException("%s:%d: Unpaired }",envFilename,line);

					name[strlen(name) - 1] = '\0';
					t = strrchr(name, '.');
					if (t == NULL)
						name[0] = '\0';
					else
						*(t + 1) = '\0';
					s++;
					s = parseString(s, name);
					continue;
				}
				// find variable name in the table
				OptionTable::iterator iter = options.find(name);

				if (iter==options.end())
					throw StrException("%s:%d: Unregistered option `%s'.",envFilename,line,name);

				switch (iter->second.type) 
				{
				case optInt: 
					{
						strtol(s, &t, 10);

						if (t == s || (*t != ' ' && *t != '\t' && *t != '\0' && *t != '}'))
							throw StrException("%s:%d: Mismatch in int variable `%s'.",envFilename,line,name);

						iter->second.assignValue( string(s, t - s) );
						s = t;
						break;
					}
				case optFloat:
					{
						strtod(s, &t);

						if (t == s || (*t != ' ' && *t != '\t' && *t != '\0' && *t != '}'))
							throw StrException("%s:%d: Mismatch in float variable `%s'.",envFilename,line,name);

						iter->second.assignValue( string(s, t - s) );
						s = t;
						break;
					}
				case optBool:
					{
						t = s;
						while ((*t >= 'a' && *t <= 'z') || (*t >= 'A' && *t <= 'Z') || *t == '+' || *t == '-')
							t++;
						if (((!strncmp(s, "true", t - s)  && t - s == 4) ||
							(!strncmp(s, "false", t - s) && t - s == 5) ||
							(!strncmp(s, "YES", t -s)    && t - s == 3) ||
							(!strncmp(s, "NO", t - s)    && t - s == 2) ||
							(!strncmp(s, "ON", t - s)    && t - s == 2) ||
							(!strncmp(s, "OFF", t - s)   && t - s == 3) ||
							(t - s == 1 && (*s == '+' || *s == '-'))) &&
							(*t == ' ' || *t == '\t' || *t == '\0' || *t == '}')) 
						{
							iter->second.assignValue( string(s, t - s) );
							s = t;
						}
						else
							throw StrException("%s:%d: Mismatch in boolean variable `%s'.",envFilename,line,name);
						break;
					}
				case optVector:
					{
						strtod(s, &t);
						if (*t == ' ' || *t == '\t') 
						{
							while (*t == ' ' || *t == '\t')
								t++;
							if (*t != ',')
								t--;
						}
						if (t == s || (*t != ' ' && *t != '\t' && *t != ',')) 
							throw StrException("%s:%d: Mismatch in vector variable `%s'.",envFilename,line,name);

						char *u;
						strtod(t, &u);
						t = u;
						if (*t == ' ' || *t == '\t') 
						{
							while (*t == ' ' || *t == '\t')
								t++;
							if (*t != ',')
								t--;
						}
						if (t == s || (*t != ' ' && *t != '\t' && *t != ',')) 
							throw StrException("%s:%d: Mismatch in vector variable `%s'.",envFilename,line,name);

						strtod(t, &u);
						t = u;
						if (t == s || (*t != ' ' && *t != '\t' && *t != '\0' && *t != '}'))
							throw StrException("%s:%d: Mismatch in vector variable `%s'.",envFilename,line,name);

						iter->second.assignValue( string(s, t - s) );
						s = t;
						break;
					}
				case optString:
					{
						iter->second.assignValue ( string(s) );
						s += strlen(s);
						break;
					}
				case optEnum: 
					{
						const EnumTable & enumTable = iter->second.enumTable;
						if(enumTable.find(string(s)) == enumTable.end()) {
							std::ostringstream os;
							printEnumOptions(os, enumTable);
							throw StrException("%s:%d: Mismatch in enum variable `%s'. Valid values are:%s.",
								envFilename,line,name,os.str().c_str());
						}
						iter->second.assignValue( string(s) );
						s += strlen(s);
						break;
					}
				default:
					throw StrException("%s:%d: Internal error: Unknown option type.",
						envFilename,line,name);
				}

				// prepare the variable name for next pass
				t = strrchr(name, '.');
				if (t == NULL)
					name[0] = '\0';
				else
					*(t + 1) = '\0';
				// get next identifier
				s = parseString(s, name);
			}
		}
	}
	catch(...)
	{
		envStream.close(); // close file and re-throw the same exception
		throw;
	}

	envStream.close(); // normal clean-up
	return true;
}

// --------------------------------------------------------------------------
char *
Environment::parseString(char *buffer, char *string) const
{
	char *s = buffer;
	char *t = string + strlen(string);

	// skip leading whitespaces
	while (*s == ' ' || *s == '\t') 
		s++;

	if (*s == '\0') 
		return NULL; // return NULL if empty

	while 
		((*s >= 'a' && *s <= 'z') ||
		(*s >= 'A' && *s <= 'Z') ||
		(*s >= '0' && *s <= '9') ||
		*s == '_')
	{
		*t++ = *s++;
	}

	*t = '\0';

	// skip trailing whitespaces
	while (*s == ' ' || *s == '\t')
		s++;

	return s;
}

// --------------------------------------------------------------------------
void
Environment::parseDefinition(const char* str)
{
	const char *equalSignPtr = strchr(str, '=');
	if (equalSignPtr == 0)
		throw StrException("Command line: Missing '=' in command line option definition `%s'. Syntax is -D <name>=<value>.", str);

	string optName = string(str, equalSignPtr - str);
	OptionTable::iterator optIter = options.find(optName);

	if (optIter == options.end()) 
		throw StrException("Command line: Invalid option name `%s' after -D.", optName.c_str());

	OptionData &option = optIter->second;
	if ( !isTypeOk(equalSignPtr+1, option.type, option.enumTable) ) 
	{
		std::ostringstream os;
		if(option.type == optEnum) { os << " Valid values are:"; printEnumOptions(os, option.enumTable); }
		throw StrException("Command line: `%s' is not a suitable value for option `%s'.%s", 
			equalSignPtr+1, option.name.c_str(), os.str().c_str() );
	}

	std::cout << "-D variable " << optName << " set to " << equalSignPtr+1 << endl;

	// assign the value
	option.assignValue( string(equalSignPtr+1) );
}

// --------------------------------------------------------------------------
bool
Environment::init(const int inArgc, char **inArgv)
{
	// store the application name (for help message)
	appname = inArgv[0];

	// store the argument list
	for(int i=1; i<inArgc; i++)
		argv.push_back(inArgv[i]);

	// check for request for help
	if( doHelp() == true ) return true; 

	doEnvironmentFile();
	parseCmdLine();
	return false; // ok
}

// --------------------------------------------------------------------------
void
Environment::registerOption(const string&    name,
							const OptType    type,
							const char*      abbrev,
							const char*      defValue,
							const EnumTable& enumTable)
{
	OptionData  newOption;
	newOption.name  = name;
	newOption.type  = type;
	newOption.enumTable = enumTable;

	// make sure this option was not yet registered
	if( options.find(name) != options.end()) 
		throw StrException("registerOption: Option `%s' already registered.", name.c_str());

	// set the abbrevation
	// make sure the abbreviation isn't one of the reserved characters ('D' or 'h' or 'E')
	if (abbrev != NULL) 
	{
		if( strcmp(abbrev,"D")==0 || strcmp(abbrev,"E")==0 || 
			strcmp(abbrev,"H")==0 || strcmp(abbrev,"help")==0 )
		{
			throw StrException("registerOption: Option `%s' uses reserved abbreviation `%s'.", name.c_str(), abbrev);
		}
		newOption.abbrev = abbrev;
	}

	// set the default value
	if (defValue != NULL)
	{
		if( !isTypeOk(defValue, type, enumTable) )
		{
			throw StrException("registerOption: Invalid default value `%s' for option `%s'.", defValue, name.c_str());
		}
		else
		{
			newOption.assignValue( defValue );
		}
	}

	// register the new option
	options[name] = newOption;
}

// --------------------------------------------------------------------------
bool
Environment::isTypeOk(const char* value, const OptType type, const EnumTable& enumTable) const
{
	int i; float x,y,z;

	switch (type) 
	{
	case optInt: 
		return (sscanf(value, " %d ", &i) == 1);
	case optFloat: 
		return (sscanf(value, " %g ", &x) == 1);
	case optBool: 
		return 
			strcmp(value,"true")==0 || strcmp(value,"false")==0 ||
			strcmp(value,"YES")==0  || strcmp(value,"NO")==0 ||
			strcmp(value,"+")==0    || strcmp(value,"-")==0 ||
			strcmp(value,"ON")==0   || strcmp(value,"OFF")==0;
	case optVector:
		return 
			sscanf(value, "%g %g %g",&x,&y,&z)==3 ||
			sscanf(value, "%g,%g,%g",&x,&y,&z)==3;
	case optString:
		return true;
	case optEnum:
		return enumTable.find(value) != enumTable.end();
	default:
		cerr << "Internal error: Unknown option type.\n" << flush; abort();
	}
	return false;
}

// --------------------------------------------------------------------------
void
Environment::setInt(const string& name, const int value)
{
	OptionTable::iterator i = options.find(name);

	if( i == options.end() ) 
		throw StrException("setInt: Option `%s' not registered.", name.c_str());

	if( i->second.type != optInt)
		throw StrException("setInt: Option `%s' is not optInt.", name.c_str());

	char ttt[16];
	sprintf(ttt, "%.15d", value);
	i->second.assignValue(string(ttt));
}

// --------------------------------------------------------------------------
void
Environment::setFloat(const string& name, const float value)
{
	OptionTable::iterator i = options.find(name);

	if( i == options.end() )
		throw StrException("setFloat: Option `%s' not registered.", name.c_str());

	if( i->second.type != optFloat)
		throw StrException("setFloat: Option `%s' is not optFloat.", name.c_str());

	char ttt[25];
	sprintf(ttt, "%.15e", value);
	i->second.assignValue( string(ttt) );
}

// --------------------------------------------------------------------------
void
Environment::setBool(const string & name, const bool value)
{
	OptionTable::iterator i = options.find(name);

	if( i == options.end() )
		throw StrException("setBool: Option `%s' not registered.", name.c_str());

	if( i->second.type != optBool)
		throw StrException("setBool: Option `%s' is not optBool.", name.c_str());

	i->second.assignValue( string(value ? "true" : "false") );
}

// --------------------------------------------------------------------------
void
Environment::setVector(const string& name, const float x, const float y, const float z)
{
	OptionTable::iterator i = options.find(name);

	if( i == options.end() )
		throw StrException("setVector: Option `%s' not registered.", name.c_str());

	if( i->second.type != optVector)
		throw StrException("setVector: Option `%s' is not optVector.", name.c_str());

	char ttt[200];
	sprintf(ttt, "%.15e,%.15e,%.15e", x, y, z);
	i->second.assignValue( string(ttt) );
}

// --------------------------------------------------------------------------
void
Environment::setString(const string& name, const string& value)
{
	OptionTable::iterator i = options.find(name);

	if( i == options.end() )
		throw StrException("setString: Option `%s' not registered.", name.c_str());

	if( i->second.type != optString)
		throw StrException("setString: Option `%s' is not optString.", name.c_str());

	i->second.assignValue( value );
}


// --------------------------------------------------------------------------
Environment::OptionTable::const_iterator 
Environment::checkIfOptionRegisteredAndPresent(const string& name) const
{
	Environment::OptionTable::const_iterator i = options.find(name);

	if( i == options.end() )
		throw StrException("Option `%s' not registered.", name.c_str());

	if( ! i->second.hasValue() ) 
		throw StrException("Option `%s' does not have assigned value.", name.c_str());

	return i;
}

// --------------------------------------------------------------------------
int
Environment::getInt(const string &name) const
{
	OptionTable::const_iterator i = checkIfOptionRegisteredAndPresent(name);  
	assert( i!=options.end() );

	if(i->second.type != optInt) 
		throw StrException("getInt: Option `%s' is not optInt.", name.c_str());

	return strtol(i->second.getValue().c_str(), NULL, 10);
}

// --------------------------------------------------------------------------
float
Environment::getFloat(const string& name) const
{
	OptionTable::const_iterator i = checkIfOptionRegisteredAndPresent(name);

	assert( i!=options.end() );

	if(i->second.type != optFloat) 
		throw StrException("getFloat: Option `%s' is not optFloat.", name.c_str());

	return (float)strtod(i->second.getValue().c_str(), NULL);
}

// --------------------------------------------------------------------------
bool
Environment::getBool(const string& name) const
{
	OptionTable::const_iterator i = checkIfOptionRegisteredAndPresent(name);

	assert( i!=options.end() );

	if(i->second.type != optBool) 
		throw StrException("getBool: Option `%s' is not optBool.", name.c_str());

	const string& str = i->second.getValue();

	return str=="true" || str=="YES" || str =="+" || str=="ON";
}

// --------------------------------------------------------------------------
void
Environment::getVector(const string& name, float &x, float &y, float &z) const
{
	OptionTable::const_iterator i = checkIfOptionRegisteredAndPresent(name);

	assert( i!=options.end() );

	if(i->second.type != optVector) 
		throw StrException("getVector: Option `%s' is not optVector.", name.c_str());

	char *t, *s;
	x = (float)strtod(i->second.getValue().c_str(), &s);
	y = (float)strtod(s + 1, &t);
	z = (float)strtod(t + 1, NULL);
}

// --------------------------------------------------------------------------
const string&
Environment::getString(const string& name) const
{
	OptionTable::const_iterator i = checkIfOptionRegisteredAndPresent(name);

	assert( i!=options.end() );

	if(i->second.type != optString) 
		throw StrException("getString: Option `%s' is not optString.", name.c_str());

	return i->second.getValue();
}

// --------------------------------------------------------------------------
const string&
Environment::getAsString(const string& name) const
{
	OptionTable::const_iterator i = checkIfOptionRegisteredAndPresent(name);

	assert( i!=options.end() );

	return i->second.getValue();
}

// --------------------------------------------------------------------------
unsigned
Environment::getEnum(const string& name) const
{
	OptionTable::const_iterator i = checkIfOptionRegisteredAndPresent(name);

	assert( i!=options.end() );

	if(i->second.type != optEnum) 
		throw StrException("getEnum: Option `%s' is not optEnum.", name.c_str());

	EnumTable::const_iterator t = i->second.enumTable.find( i->second.getValue() );
	assert(t != i->second.enumTable.end()); // should have been checked while parsing the input
	return t->second;
}

// --------------------------------------------------------------------------
Environment::OptionTable::iterator
Environment::lookUpAbbrevation(const std::string& abbrev)
{
	OptionTable::iterator iter = options.begin();

	for ( ; iter != options.end(); ++iter)
		if (iter->second.abbrev == abbrev)
			break;

	return iter;
}

// --------------------------------------------------------------------------
void
Environment::parseCmdLine()
{
	// iterator through the command line parameters
	ArgList::iterator argIter = argv.begin();

	// process non-option arguments at the beginning of the command line
	for( ; argIter != argv.end() ; ++argIter )
	{
		if ( argIter->c_str()[0] != '-' )
			nonOptions.push_back(*argIter);
		else
			break;
	}

	while ( argIter != argv.end() ) 
	{
		if( *argIter == "-D" )
		{
			// it's the "-D name=value" syntax
			if(++argIter == argv.end())
				throw "Command line: No value after -D command line option.\n";

			parseDefinition(argIter->c_str());
			++argIter; // Advance to the next command line argument
		}
		else 
		{
			const char *argStr = argIter->c_str();
			if( argStr[0] != '-' ) 
				throw StrException("Command line: Unrecognized command line option `%s'.", argStr);

			// It's an abbreviation - look it up from the defined option.
			OptionTable::iterator optIter = lookUpAbbrevation(argStr+1);

			// Not a registered abbreviation
			if(optIter == options.end()) 
				throw StrException("Command line: Invalid command line option `%s'.", argStr);

			// Yes - a register abbreviation, get the corresponding option structure.
			OptionData &option = optIter->second;

			// Advance to the next command line argument
			++argIter; 

			// End of command line? OK for boolean option, problem otherwise.
			if(argIter == argv.end())
			{
				if(option.type != optBool)
					throw StrException("Command line: No value given for command line option `%s'.", argStr);

				option.assignValue("ON"); // it was a boolean option - ok
				break; // end of argument - stop parsing
			}
			else
			{
				// Parse the option value
				const char *valStr = argIter->c_str();
				if ( isTypeOk(valStr, option.type, option.enumTable) ) 
				{
					option.assignValue( *argIter ); // syntax ok - assign the value
					++argIter; // Advance to the next command line argument
				}
				else if( option.type == optBool && valStr[0] == '-' )
				{
					option.assignValue("ON"); // it was a boolean option - ok
					// Do not advance - we are already at the next argument
				}
				else
				{
					std::ostringstream os;
					if(option.type == optEnum) { os << " Valid values are:"; printEnumOptions(os, option.enumTable); }
					throw StrException("Command line: `%s' is not a suitable value for option `%s'.%s", 
						valStr, argStr, os.str().c_str() );
				}
			}
		}
	}
}

#if 0
void 
Environment::printOptions(ostream& ostr, const string& start_with, const string& not_start_with)
{
	OptionTable::const_iterator i = options.begin();

	for ( ; i != options.end(); i++) {
		if(i->second.name.compare(0,start_with.length(),start_with)==0 &&
			i->second.name.compare(0,not_start_with.length(),not_start_with)!=0) {
				ostr  << "struct OptionData {\n"
					<< "  const string name   = \"" << i->second.name      << "\"\n"
					<< "        string value  = \"" << i->second.value     << "\"\n"
					<< "        string abbrev = \"" << i->second.abbrev    << "\"\n"
					<< "        string defValue=\"" << i->second.defValue  << "\"\n"
					<< "  const OptType type=" << (int)i->second.type << "\n"
					<< "};" << endl;
		}
		/*
		else
		ostr << "Option " << i->second.name << " did not pass. " << endl;
		*/
	}
}
#endif
