// Properties.cc
// Neil Moore
// 7 October 1998

#include "Properties.h"
#include "util.h"
#include <fstream>
#include <list>
#include <string>
#include <cerrno>
#include <cctype>

using namespace std;

/* operator<<():  output a Properties object.
 *
 * This method prints the (property,value) pairs of a Properties object
 * to an ostream, one per line, with the format:
 *
 *   property = value
 *
 * It returns a reference to its left operand.
 */
ostream &operator<<(ostream &out, const Properties &prop) 
{
	proplist::const_iterator i;

	for(i = prop.plist->begin(); i != prop.plist->end(); i++)
		Properties::writePropLine(out, i->first, i->second);

	return out;
}

/* Properties():  default constructor.
 *
 * This method inititalises a Properties object with an empty proplist.
 */
Properties::Properties() 
{
	plist = new proplist;
}


/* Properties():  copy constructor.
 *
 * This method inititalises a Properties object to be a copy of another
 * This is a deep copy; the two Properties objects do not share data.
 */
Properties::Properties(const Properties &old) 
{
	plist = new proplist(*old.plist);
}


/* Properties():  constructor.
 *
 * This method initialises a Properties object from a file.  It is the
 * same as using the default constructor and calling load().
 *
 * This method may throw Properties::CannotOpen (indicating that the file
 * could not be opened) indirectly, by way of load().
 */
Properties::Properties(const char *filename) 
{
	Properties();
	load(filename);
}


/* ~Properties():  destructor.
 *
 * This method frees the property list of this Properties object (if it
 * has been allocated).
 */
Properties::~Properties()
{
	if(plist)
		delete plist;
}


/* operator=():  assignment operator.
 *
 * This method makes its left-hand operand a deep copy of its right-hand
 * operand.  It returns a reference to its left-hand operand.
 */
Properties &Properties::operator=(const Properties &rhs) 
{
	if(plist)
		delete plist;
	plist = new proplist(*rhs.plist);
	return *this;
}


/* load():  load a Properties object from a file.
 *
 * This method loads (property,value) pairs into a Properties object from
 * a file.  The file should consist of:
 *  * blank lines (possibly including whitespace)
 *  * lines beginning with '#' (possibly preceded by whitespace)
 *      and
 *  * lines of the form "<property>=[value]", where <property> is a valid
 *    property name (see match_prop()) and whitespace is allowed before
 *    and after <property> and [value].
 *
 * This method may throw Properties::CannotOpen (indicating that the file
 * could not be opened).  It prints error messages to cerr if a line does
 * not meet one of the above criteria.
 */
void Properties::load(const char *filename) 
{
	ifstream file;
	string property, value;
	int lineno=0;  // The user will just have to learn to count from 0.

	// If this object has already been used, clean it up.
	if (plist) {
		delete plist;
		plist = new proplist;
	}
	

	file.open(filename);
	
	if (file.fail())
		throw CannotOpen(errno, string(filename));

	
	while(!file.eof()) {
		try {
			property = "";
			value = "";
			if(readPropLine(file, property, value))
				addProperty(property, value);
		} catch (BadLine &err) {
			cerr << "bad line " << lineno << ": " << "\""
			     << err.text() << "\"\n";
		} catch (BadProperty &err) {
			// Hrm.  readPropLine() should have caught this and
			// turned it into a BadLine...
			cerr << "can't happen: bad property on line "
			     << lineno << ": \"" << err.propname() << "\"\n";
		}
		
		lineno++;
	}
	
	file.close();
}


/* getValueFor():  get the value of a property.
 *
 * This method searches the property list for the requested property,
 * and returns the value associated with that property.  If that
 * property cannot be found, getValueFor() throws NotFound.
 *
 */
string Properties::getValueFor(const string &propname) const
{
	proplist::const_iterator i;

	for(i=plist->begin(); i != plist->end(); i++) {
		if(i->first == propname) {
			return i->second;
		}
	}

	throw NotFound(propname);
}


/* getBoolValueFor():  get the value of a property, interpreted as a bool.
 *
 * This method searches the property list for the requested property, and
 * returns a bool indicating the value associated with that property.  If
 * that property cannot be found, getBoolValueFor() throws NotFound; if
 * the value of that property does not represent a bool, getBoolValueFor()
 * throws BadFormat.
 *
 * "1", "on", and "true" indicate true values; "0", "off", "false", and ""
 * indicate false values.  Anything else results in a BadFormat exception
 * being thrown.
 */
bool Properties::getBoolValueFor(const string &propname) const
{
	string val = getValueFor(propname);
	stripws(val);
	lowercasify(val);
	
	if(val == "1" || val == "on" || val == "true")
		return true;
	else if(val == "0" || val == "off" || val == "false" || val == "")
		return false;

	throw BadFormat(propname, val);
}


/* getIntValueFor():  get the value of a property, interpreted as an int.
 *
 * This method searches the property list for the requested property, and
 * returns a int indicating the value associated with that property.  If
 * that property cannot be found, getIntValueFor() throws NotFound; if the
 * value of that property does not represent an integer, getIntValueFor()
 * throws BadFormat.
 *
 * Integers are interpreted as they would be in C: a leading 0 signifies
 * an octal number, and a leading 0x signifies a hexadecimal number.
 */
int Properties::getIntValueFor(const string &propname) const
{
	char *endptr;
	string val = getValueFor(propname);
	stripws(val);
	lowercasify(val);
	
	long l = strtol(val.c_str(), &endptr, 0);
	
	if(*endptr == '\0' && val[0] != '\0')
		return (int)l;

	throw BadFormat(propname, val);
}


/* getDoubleValueFor():  get the value of a property, interpreted as a
 *                       double.
 *
 * This method searches the property list for the requested property, and
 * returns a double indicating the value associated with that property.
 * If that property cannot be found, getDoubleValueFor() throws NotFound;
 * if the value of that property does not represent a double,
 * getDoubleValueFor() throws BadFormat.
 *
 * Doubles are interpreted as they would be in C: 'E' or 'e' may be used
 * for expressing scientific notation.
 */
double Properties::getDoubleValueFor(const string &propname) const
{
	char *endptr;
	string val = getValueFor(propname);
	stripws(val);
	lowercasify(val);
	
	double d = strtod(val.c_str(), &endptr);
	
	if(*endptr == '\0' && val[0] != '\0')
		return d;

	throw BadFormat(propname, val);
}


/* addProperty():  add a (property,value) pair to the property list, or
 *                 change the value of an existing property.
 *
 * This method searches the property list for the indicated property; if
 * it exists, it sets the value of that property as requested and returns
 * the original value of that property.  Otherwise, it adds the indicated
 * (property,value) pair to the end of the property list and returns an
 * empty string.  If the propname parameter is invalid (as determined by
 * valid_property()), this method throws BadProperty.
 */
string Properties::addProperty(const string &propname, const string &value) 
{
	string old = "";
	proplist::iterator i;

	
	if(!valid_property(propname))
		throw BadProperty(propname);
	
	for(i=plist->begin(); i != plist->end(); i++) {
		if(i->first == propname) {
			old = i->second;
			i->second = value;
			return old;
		}
	}

	plist->push_back(proppair(propname, value));
	return old;
}


/* remove():  remove a property from the property list.
 *
 * This method searches the property list for the indicated property; if
 * found, remove() removes it from the list and returns the value that
 * was associated with it.  Otherwise, this method throws NotFound.
 */
string Properties::remove(const string &propname) 
{
	string old;
	proplist::iterator i;

	for(i=plist->begin(); i != plist->end(); i++) {
		if(i->first == propname) {
			old = i->second;
			plist->erase(i);
			return old;
		}
	}

	throw NotFound(propname);
}


/* isDefined():  test for the existence of a property.
 *
 * This method searches the property list for the indicated property; if
 * it is found, isDefined() returns true.  Otherwise, it returns false.
 */
bool Properties::isDefined(const string &propname) const
{
	proplist::const_iterator i;

	for(i=plist->begin(); i != plist->end(); i++)
		if(i->first == propname)
			return true;

	return false;
}


/* save():  save the property list to a file.
 *
 * This method opens a file with the requested filename, and writes to it
 * the (property,value) pairs of this object, one per line, with the
 * format:
 *
 *   property = value
 *
 * If the file could not be opened for writing, this method throws
 * CannotOpen.
 */
void Properties::save(const char *filename) const
{
	ofstream file;

	file.open(filename);
	if(file.fail())
		throw CannotOpen(errno, string(filename));
	
	file << *this;
	
	file.close();
}


/* readPropLine():  read a line from an istream, parsing it into a property
 *                  and value.
 *
 * This function reads a line from the istream `in'.  If the line is blank
 * (possibly including whitespace) or a comment (beginning with the '#'
 * character, optionally preceded by whitespace), it returns false.
 * Otherwise, it parses the line, which should be of the form:
 *
 * [ws]<property>[ws]=[ws][<value>][ws]
 *
 * , into the <property> and <value> components.  It places the two strings
 * into the aptly-named propname and value parameters, and returns true.  If
 * the property name is invalid, or the line fails to follow the above format,
 * this function throws BadLine.
 *
 * <property> must match the format described in the match_prop() comments
 * below.  <value> may consist of any string of non-newline characters,
 * although leading and trailing whitespace will be removed.
 */
bool Properties::readPropLine(istream &in, string &propname,
			      string &value)
{
	string line;
	int matched;
	
	getline(in, line);
	stripws(line);

	if(line.empty() || line[0] == '#')
		return false;
	
	matched = match_prop(line, propname);
	
	if(matched == 0)
		throw BadLine(line);

	line.erase(0, matched);

	stripws(line);
	value = line;
	return true;
}

/* writePropLine():  write a (property,value) pair to an ostream.
 *
 * This function writes a (property,value) pair to the indicated ostream.
 * The pair is written in a format that may be read with readPropLine(),
 * namely "property = value\n".
 */
void Properties::writePropLine(ostream &out, const string &propname,
			       const string &value)
{
	out << propname << " = " << value << endl;
}


/* valid_property():  test the validity of a property name.
 *
 * This function rests whether the indicated property name is valid.  It
 * relies on match_prop() to do the actual testing---this function serves
 * primarily as a wrapper for that function.  It returns true if the
 * property name is valid, false if not.
 */
bool Properties::valid_property(const string &prop)
{
	string test = prop;
	string mprop;
	
	// match_prop allows trailing and leading whitespace.  We don't.
	if(prop.length() == 0 || isspace(prop[0]))
		return false;
	if(prop.length() > 1 && isspace(prop[prop.length()-1]))
		return false;
	
	
	// match_prop() expects   property=[value].  We *could* rewrite
	// it to not do so, but that would just complicate readPropLine()
	// unnecessarily.	
	test += '=';

	match_prop(test, mprop);
	
	if(mprop == prop)
		return true;
	else
		return false;
}

/* match_prop():  match the property part of a line.
 *
 * This function matches the property part of a string (which should
 * represent a line from a properties file).  The property name (not
 * including leading and trailing whitespace and the trailing equals
 * sign) is placed into the `prop' argument, and the number of
 * characters matched (including all whitespace and the equals sign)
 * is returned.  If the string does not match the expected form (see
 * the regexp below), match_prop() sets its `prop' argument to "" and
 * returns zero.
 *
 *   It would be much more general to write or use a full regexp library.
 * However, that would be beyond the scope of this programming assignment.
 * match_prop() does implement a simple DFA, though:
 *
 *      \ CURR
 *    IN \       0       1       2       3
 *        +--------------------------------
 *  alpha |      1       1       1    fail
 *    num |   fail       1       1    fail
 *    dot |   fail       2    fail    fail
 *     ws |      0       3    fail       3
 *  equal |   fail  accept    fail  accept
 *  other |   fail    fail    fail    fail
 *
 * (CURR is the current state, IN is the next character of the input;
 * fail means return failure immediately, accept means return success
 * immediately)
 *
 *   This DFA matches the ed-style regexp:
 *     /^[ \t]*\([a-z][a-z0-9]*(\.[a-z0-9]+)*\)[ \t]*=/
 *
 *   This regexp matches a sequence of zero or more spaces and tabs,
 * followed by a string of alphanumeric characters and dots, beginning
 * with an alphabetic character, followed by a sequence of spaces and
 * tabs, then by an equal sign; dots may not appear adjacent to one
 * another, and the final character (before the trailing whitespace/
 * equal sign) may not be a dot.
 * 
 *   prop would contain the characters matched between \( and \).
 * The return value indicates the total numbers of characters matched
 * by the entire regexp.
 */

// Constants for the state table
const int FAIL =  -1;
const int ACCEPT = 9;

// And the state table itself (next_state[x][y] refers to the next state
// if the current state is x and the input is y.
const int next_state[4][6] = {
	{   1, FAIL, FAIL,    0,   FAIL, FAIL},
	{   1,    1,    2,    3, ACCEPT, FAIL},
	{   1,    1, FAIL, FAIL,   FAIL, FAIL},
	{FAIL, FAIL, FAIL,    3, ACCEPT, FAIL}
};

int Properties::match_prop(const string &str, string &prop) 
{
	int state = 0;
	unsigned pos = 0;
	int matched = 0;
	char nextchar;
	Properties::chartype in;

	prop.erase();
	
	while(true) {
		if(pos >= str.length()) {
			prop = "";
			return 0;
		}
		
		nextchar = str[pos++];
		in = chartypeof(nextchar);
		
		state = next_state[state][in];
		
		matched++;
		
		if(state == FAIL) {
			prop = "";
			return 0;
		}
		else if(state == ACCEPT)
			break;
		
		if(in != WS)
			prop += nextchar;
	}

	return matched;
}

/* chartypeof():  return the character type of a character.
 *
 *   This function returns the character type of any character.  This
 * chartype may be used as an index into next_state[state].  This function
 * should only be used by match_prop.
 */
Properties::chartype Properties::chartypeof(char c)
{
	if(isalpha(c))
		return ALPHA;
	else if(isdigit(c))
		return NUM;
	else if(c == '.')
		return DOT;
	else if(c == ' ' || c == '\t')  // not all isspace() characters count
		return WS;
	else if(c == '=')
		return EQUAL;
	else
		return OTHER;
}
