/******************************************************************************
 * $Id: gfx_config_file.cc,v 1.74 2006/11/02 02:21:02 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	Functions for a configuration file based on key/value tuples.
 * @remarks	Similar to GNOME's gnome_config*().
 *          Documented at gfx_config_file.hh.
 *//*
 * LEGAL:	COPYRIGHT (C) 2004 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE (GPL).
 ******************************************************************************/

#define GFX_CONFIG_FILE_CC 1
#include "gfx.hh"
#include "gfx_config_file.hh"
#include "gfx_stream.hh"
namespace gfx {

#define GFX_CONFIG_FILE_VERSION_STR "(gfx::ConfigFile v2)"

/*****************************************************************************
 * Constructor.
 *
 * @param appname	Required.
 * @param fname		Optional, default is empty string, specify name of configuration file.
 * @param autoflush	Optional, default is true, control flushing of configuration changes.
 *
 * NOTE/WARNING: If auto-flush is disabled (for speed), client must periodically call Flush().
 * Write() just updates a STL map, it doesn't write to the configuration file!
 *
 * Optionally, the constructor can be told which file to use for the configuration,
 * and whether to auto-flush changes to the configuration.  Setting auto-flush
 * to false is faster (lazy-writes) but then the client becomes responsible
 * for calling Flush() (actually writes to the file) or changes will be lost!
 *****************************************************************************/
ConfigFile::ConfigFile( const string& appname, const string& fname, bool autoflush )
{
	// Members:
	mValid	   = false;
	mAutoflush = autoflush;
	mAppname   = appname;

	// Automatically choose a filename if one wasn't specified.
	string filename;                // use this as file to open
	if ( fname != "" )
    {
        filename = fname;
    }
    else
	{
		// UNIX: ~/.myprog.cfg
		const char* home = getenv( "HOME" );
		if ( home == NULL )
			{ *gStreamError << "ERROR: ConfigFile: can't get HOME env var" << endl; return; }
		filename  = home;
		filename += "/.";
		filename += appname;
		filename += ".cfg";
	}

	// Open configuration file for reading/writing.
	// First try to open the file without the truncate bit.
	// But some iostream implementations might fail if the file doesn't exist.
	mStream.open( filename.c_str(), ios::in|ios::out|ios::binary );
	if ( ! mStream.good() || ! mStream.is_open() )
	{
		// This might be the file-doesn't-exist case depending
		// on the iostream implementation.  Retry with truncate bit.
		mStream.clear();
		mStream.open( filename.c_str(), ios::in|ios::out|ios::binary|ios::trunc );
	}

	// Parse tuples in configuration file.
	if ( mStream.good() && mStream.is_open() )
        mValid = Parse();
    else
		*gStreamError << "ERROR: ConfigFile::Open() failed" << endl;

    // Increase floating-point precision.
	mStream.setf( ios::fixed, ios::floatfield );
	mStream.precision( 20 );
}

/*****************************************************************************
 * Flush all tuples to configuration file.  Reverse of Parse().
 * Call this after configuration is changed.
 *****************************************************************************/
bool
ConfigFile::Flush( void )
{
    // At beginning.
	mStream.seekp(0);

    // First line is a comment describing the application and configuration version.
	mStream << "# " << mAppname << " configuration " << GFX_CONFIG_FILE_VERSION_STR << endl;

	// Beautify configuration file by sorting its lines.
	vector<ConfigTuple> vec;
	map<string,ConfigTuple>::iterator itMap;
	for ( itMap = mTupleMap.begin(); itMap != mTupleMap.end(); ++itMap )
		vec.push_back( itMap->second );
	std::sort( vec.begin(), vec.end(), CompareTuples );

	// Write every tuple to the configuration file.
	vector<ConfigTuple>::iterator itVec;
	for( itVec = vec.begin(); itVec != vec.end(); ++itVec )
	{
		ConfigTuple& tuple = *itVec;
		tuple.Write( mStream );
	}

	// Last line explicitly contains "END".
	// This is necessary in case the updated configuration has shortened.
	mStream << "END" << endl;

	return mStream.good();
}

/*****************************************************************************
 * Parse configuration file.  Reverse of Flush().
 * Every line is converted to a tuple object (except comment lines).
 * Every tuple object is inserted into a STL map for direct manipulation
 * by Read()/Write()/Remove().
 * @return True if configuration file was parsed (empty configuration is OK).
 *****************************************************************************/
bool
ConfigFile::Parse( void )
{
    // At beginning.
	mStream.seekg(0);

	// Read comment on the first line.
	// Ignore old versions of configuration file.
	string line1;
	std::getline( mStream, line1 );
	if ( mStream.eof() ) // empty configuration file is OK
	{
		mStream.clear();
		return true;
	}
	if ( line1.find( GFX_CONFIG_FILE_VERSION_STR ) == string::npos )
	{
		*gStreamError << "NOTICE: ignoring obsolete configuration file format" << endl;
		return true; // yes, true
	}

	// Parse every line.
	for (;;)
	{
		// EOF or error?
		if ( ! mStream ) return false; // failed since line with "END" wasn't found

		// Read first field.
		string field1;
		mStream >> field1;

		// Found last line?
		if ( field1 == "END" ) return true;

		// Ignore comment lines.
		if ( field1 == "#" )
		{
			string comment;
			std::getline( mStream, comment );
			continue;
		}

		// Parse tuple.
		ConfigTuple tuple;
		if ( tuple.Read( mStream, field1 ) )
		{
			mTupleMap[tuple.mKey] = tuple; // insert tuple into STL map
		}
		else
		{
			*gStreamError << "ERROR: ConfigFile::Parse(): error reading a tuple" << endl;
			return false;
		}
	}

	return false; // unexpected
}

/*****************************************************************************
 * @fn bool ConfigFile::Write( const string& key, const int& val )
 * Interface method to write a tuple to the configuration file.
 * This can do lazy-writes: if mAutoflush is false, the client must call Flush()
 * to really write the file.
 * @return True if successful.
 *****************************************************************************/
// Either update existing tuple in map, or insert a new tuple to map.
// Flush writes to file depending on autoflush.
#define CONFIG_FILE_WRITE(MVAL, ETYPE)											\
{                                                                               \
	if ( ! mValid )																\
	{																			\
		*gStreamError << "ERROR: ConfigFile::Write(): object invalid" << endl;	\
		return false;															\
	}																			\
	if ( mTupleMap.find(key) == mTupleMap.end() )                               \
        mTupleMap[key] = ConfigTuple();                                         \
	mTupleMap[key].mType = ETYPE;												\
	mTupleMap[key].mKey  = key;													\
	mTupleMap[key].MVAL  = val;													\
	if ( mAutoflush ) return Flush(); else return true;                         \
}

bool
ConfigFile::Write( const string& key, const int& val )      	// int
{
	CONFIG_FILE_WRITE( mValInt, ConfigTuple::eType_INT )
}
bool
ConfigFile::Write( const string& key, const double& val )		// double
{
	CONFIG_FILE_WRITE( mValDouble, ConfigTuple::eType_DOUBLE )
}
bool
ConfigFile::Write( const string& key, const string& val )		// string
{
	CONFIG_FILE_WRITE( mValString, ConfigTuple::eType_STRING )
}

/*****************************************************************************
 * @fn bool ConfigFile::Read( const string& key, int& val )
 * Interface method to read a tuple from the configuration file.
 * @return True if successful.
 *****************************************************************************/
/// Get tuple from STL map.
#define CONFIG_FILE_READ(MVAL,TYPE)												\
{                                                                               \
	if ( ! mValid )			                                                    \
	{																			\
		*gStreamError << "ERROR: ConfigFile::Read(): object invalid" << endl;   \
		return false;															\
	}																			\
	if ( mTupleMap.find( key ) != mTupleMap.end() )								\
	{																			\
        CHECK_TYPESIG(&mTupleMap[key],TYPESIG_LIB_CONFIG_TUPLE);				\
        if ( mTupleMap[key].mType == TYPE )     								\
        {																		\
            val = mTupleMap[key].MVAL;											\
            return true;														\
        }																		\
        else																	\
        {																		\
    		*gStreamError << "ERROR: ConfigFile::Read(): type mismatch" << endl;\
        	return false;														\
        }																		\
	}																			\
	else																		\
		return false;                                                           \
}

bool
ConfigFile::Read( const string& key, int& val /*OUT*/ )
{
	CONFIG_FILE_READ(mValInt, ConfigFile::ConfigTuple::eType_INT)
}
bool
ConfigFile::Read( const string& key, double& val /*OUT*/ )
{
	CONFIG_FILE_READ(mValDouble, ConfigFile::ConfigTuple::eType_DOUBLE)
}
bool
ConfigFile::Read( const string& key, string& val /*OUT*/ )
{
	CONFIG_FILE_READ(mValString, ConfigFile::ConfigTuple::eType_STRING)
}

/*****************************************************************************
 * @fn bool ConfigFile::Access( eAccess acc, const string& key, int& val )
 * Interface method to read or write a tuple.
 * @return True if successful.
 *****************************************************************************/
// Don't write "access" as it's in <unistd.h>
#define CONFIG_ACCESS                                                       \
{                                                                           \
	if ( acc == ConfigFile::eAccess_WRITE )                                 \
		return Write( key, val );                                           \
	else                                                                    \
		return Read( key, val );                                            \
}

bool
ConfigFile::Access( eAccess acc, const string& key, int& val /*IN/OUT*/ )
{
	CONFIG_ACCESS
}
bool
ConfigFile::Access( eAccess acc, const string& key, double& val /*IN/OUT*/ )
{
	CONFIG_ACCESS
}
bool
ConfigFile::Access( eAccess acc, const string& key, string& val /*IN/OUT*/ )
{
	CONFIG_ACCESS
}

/*****************************************************************************
 * Interface method to remove a tuple.
 * @return True if removed or was absent, or false if configuration error.
 *****************************************************************************/
bool
ConfigFile::Remove( const string& key )
{
	if ( ! mValid )
	{
		*gStreamError << "ERROR: ConfigFile::Read(): object invalid" << endl;
		return false;
	}

	map<string,ConfigTuple>::iterator itGone;
	itGone = mTupleMap.find( key );
	if ( itGone != mTupleMap.end() )
	{
		// Remove tuple from STL map.
        CHECK_TYPESIG( &itGone->second, TYPESIG_LIB_CONFIG_TUPLE );
		mTupleMap.erase( itGone );
		if ( mAutoflush ) return Flush(); else return true;
	}
	else
		return true; // doesn't exist (ok)
}

/*****************************************************************************
 * Tuple constructor/destructor.
 *****************************************************************************/
ConfigFile::ConfigTuple::ConfigTuple( void )
{
	SET_TYPESIG(this,TYPESIG_LIB_CONFIG_TUPLE);
	Clear();
}
ConfigFile::ConfigTuple::~ConfigTuple()
{
	INVALIDATE_TYPESIG(this,TYPESIG_LIB_CONFIG_TUPLE);
	Clear();
}
void
ConfigFile::ConfigTuple::Clear( void )
{
	mType	   = eType_INVALID;
    mKey.erase();
	mValInt    = 0;
	mValDouble = 0.0;
	mValString.erase();
}

/*****************************************************************************
 * Write configuration tuple to file stream.
 *****************************************************************************/
bool
ConfigFile::ConfigTuple::Write( fstream& fs )
{
CHECK_TYPESIG(this,TYPESIG_LIB_CONFIG_TUPLE);
	switch ( mType )
	{
		case eType_INT:
		fs << "int "    << mKey << ' ' << mValInt    << endl;
		break;

		case eType_DOUBLE:
		fs << "double " << mKey << ' ' << mValDouble << endl;
		break;

		case eType_STRING:
		fs << "string " << mKey << ' ' << (int)mValString.size() << ' ' << mValString << endl;
		break;

		default: ASSERT(0);	return false; break;
	}
	return fs.good();
}

/*****************************************************************************
 * Read configuration tuple from file stream.
 *****************************************************************************/
bool
ConfigFile::ConfigTuple::Read( fstream& fs, const string& type )
{
CHECK_TYPESIG(this,TYPESIG_LIB_CONFIG_TUPLE);

	mType = ConfigTuple::eType_INVALID;
	fs >> mKey;

	if ( type == "int" )
	{
		fs >> mValInt;
		mType = ConfigTuple::eType_INT;
	}
	else if ( type == "double" )
	{
		fs >> mValDouble;
		mType = ConfigTuple::eType_DOUBLE;
	}
	else if ( type == "string" )
	{
		int stringLen; fs >> stringLen;			// read string length field
		char c; fs.read( &c, 1 );				// skip one space
		mValString.erase();
		for ( int i = 0; i < stringLen; ++i )	// read string value
		{
			fs.read( &c, 1 );					// read() doesn't ignore whitespace
			if ( ! fs.good() ) return false;
			mValString += c;
		}
		mType = ConfigTuple::eType_STRING;
	}
	else
	{
		*gStreamError << "ERROR: ConfigTuple::Read(): bad type '" << mType << "'" << endl;
		return false;
	}

	// File stream ok?
	if ( fs.good() )
		return true;
	else
	{
		mType = ConfigTuple::eType_INVALID;
		return false;
	}
}

/*****************************************************************************
 * Used to sort firstly by type, secondly by key.
 *****************************************************************************/
bool
CompareTuples( const ConfigFile::ConfigTuple& t1, const ConfigFile::ConfigTuple& t2 )
{
CHECK_TYPESIG(&t1,TYPESIG_LIB_CONFIG_TUPLE);
CHECK_TYPESIG(&t2,TYPESIG_LIB_CONFIG_TUPLE);

	if ( t1.mType != t2.mType )
		return t1.mType < t2.mType;
	else
		return t1.mKey < t2.mKey;
}

} // namespace gfx
