/******************************************************************************
 * $LastChangedDate: 2011-04-23 21:07:07 -0400 (Sat, 23 Apr 2011) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Binary configuration file based on key/value tuples.
 * @remarks Similar to GNOME's gnome_config*().
 *          Documented at base_config_file.hh.
 *//*
 * LEGAL:   COPYRIGHT (C) 2004 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 ******************************************************************************/

#define BASE_CONF_BIN_CC 1
#include <vector>
#include <map>
#include <fstream>
#include <algorithm>
#include "base/module.hh"
#include "base/conf_bin.hh"
#include "base/exception.hh"
#include "base/file.hh"
#include "base/stream.hh"
#include "base/stream_funcs.hh"
using namespace base;
#include "glue/module.hh"
#include "glue/conf.hh"
using namespace glue;

namespace base {

INTERN const string BASE_CONF_BIN_VERSION_STR = "(gfx::ConfigFile v2)";  // legacy name

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////  functions  ///////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Check for obsolete ~/.palomino.cfg.
 * It was obsoleted 2008/08/10.
 * @return Obsolete filename or empty string.
 *****************************************************************************/
string
CheckObsoleteConfFilename( const string& programName )
{
    string obsoleteFilename = glue::GetHomeDirectoryName();
    obsoleteFilename += DIRECTORY_SEPARATOR_CHAR;
    obsoleteFilename += '.';
    obsoleteFilename += programName;
    obsoleteFilename += ".cfg";
    if ( IfFileExists( obsoleteFilename ) )
    {
        BEGIN_ONCE {
        CERROR << "Warning: Pathname of " << obsoleteFilename << " is obsolete." << std::endl
               << "Please rename and move it to configuration directory." << std::endl
               // ~/.palomino.cfg --> ~/.palomino/palomino.cfg
               << "~/." << programName << ".cfg --> ~/." << programName << "/" << programName << ".cfg" << std::endl;
        } END_ONCE
        return obsoleteFilename;
    }
    else
    {
        return string();
    }
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////  ConfBin  ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * ctor
 * MIGHT THROW AN EXCEPTION
 *
 * @param   programName
 *          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!
 *****************************************************************************/
ConfBin::ConfBin( const string& programName, const string& fname, bool autoflush )
{
    // Members:
    mValid       = false;
    mAutoflush   = autoflush;
    mProgramName = programName;

    // Automatically choose a filename if one wasn't specified.
    string filename;
    if ( not fname.empty() )
    {
        filename = fname;
    }
    else
    {
        // Check for obsolete configuration file.
        const string obsoleteFilename = CheckObsoleteConfFilename( programName );
        if ( obsoleteFilename.empty() )
        {
            // Configuration file is stored in a hidden directory in UNIX:
            // ~/.palomino/palomino.cfg
            // Note: This file normally might not exist yet (if program was never run before).
            // Windows accepts UNIX forward slash.
            filename  = glue::GetConfDirectoryName();
            filename += DIRECTORY_SEPARATOR_CHAR;
            filename += programName;
            filename += ".cfg";
        }
        else
        {
            // Continue using obsolete filename.
            filename = obsoleteFilename;
        }
    }

    ASSERT( not filename.empty() );
    BEGIN_ONCE {
    CLOG << "conf: " << filename << std::endl;
    } END_ONCE

    // Open configuration file for reading/writing.
    // Parse tuples in configuration file.
    if ( StreamOpen( mStream, std::ios::in | std::ios::out | std::ios::binary, filename ) )
    {
        mValid = Parse();
    }
    else
    {
        const char errMsg[] = "ERROR: ConfBin::Open() failed";
        CERROR << errMsg << std::endl;  // ensure error message is shown
        throw Exception( errMsg );
    }

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

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

    // First line is a comment describing the application and configuration version.
    mStream << "# " << mProgramName << " configuration " << BASE_CONF_BIN_VERSION_STR << std::endl;

    // Beautify configuration file by sorting its lines.
    std::vector<ConfTuple> vec;
    std::map<string,ConfTuple>::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.
    std::vector<ConfTuple>::iterator itVec;
    for( itVec = vec.begin(); itVec != vec.end(); ++itVec )
    {
        ConfTuple& tuple = *itVec;
        tuple.Write( mStream );
    }

    // Last line explicitly contains "END".
    // This is necessary in case the updated configuration has shortened.
    mStream << "END" << std::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
ConfBin::Parse( void )
{
    // Forget what you have learned.
    mTupleMap.clear();

    // 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( BASE_CONF_BIN_VERSION_STR ) == string::npos )
    {
        CERROR << "NOTICE: ignoring obsolete configuration file format" << std::endl;
        return true;  // yes, true
    }

    // Parse every line.
    while ( true )
    {
        // EOF or error?
        if ( not 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.
        ConfTuple tuple;
        if ( tuple.Read( mStream, field1 ) )
        {
            mTupleMap[tuple.mKey] = tuple; // insert tuple into STL map
        }
        else
        {
            CERROR << "ERROR: ConfBin::Parse(): error reading a tuple" << std::endl;
            return false;
        }
    }

    return false;  // unexpected
}

/*****************************************************************************
 * @fn bool ConfBin::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 CONF_BIN_WRITE(MVAL, ETYPE)                                         \
{                                                                           \
    if ( ! mValid )                                                         \
    {                                                                       \
        CERROR << "ERROR: ConfBin::Write(): object invalid" << endl;        \
        return false;                                                       \
    }                                                                       \
    if ( mTupleMap.find(key) == mTupleMap.end() )                           \
        mTupleMap[key] = ConfTuple();                                       \
    mTupleMap[key].mType = ETYPE;                                           \
    mTupleMap[key].mKey  = key;                                             \
    mTupleMap[key].MVAL  = val;                                             \
    if ( mAutoflush )                                                       \
        return Flush();                                                     \
    else                                                                    \
        return true;                                                        \
}

bool
ConfBin::Write( const string& key, const int& val )         // int
{
    CONF_BIN_WRITE( mValInt, ConfTuple::eType_INT );
}

bool
ConfBin::Write( const string& key, const double& val )      // double
{
    CONF_BIN_WRITE( mValDouble, ConfTuple::eType_DOUBLE );
}

bool
ConfBin::Write( const string& key, const string& val )      // string
{
    CONF_BIN_WRITE( mValString, ConfTuple::eType_STRING );
}

/*****************************************************************************
 * @fn bool ConfBin::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 CONF_BIN_READ(MVAL,TYPE)                                            \
{                                                                           \
    if ( ! mValid )                                                         \
    {                                                                       \
        CERROR << "ERROR: ConfBin::Read(): object invalid" << endl;         \
        return false;                                                       \
    }                                                                       \
                                                                            \
    if ( mTupleMap.find( key ) != mTupleMap.end() )                         \
    {                                                                       \
        CHECK_TYPESIG(&mTupleMap[key],TYPESIG_CONF_TUPLE);                  \
        if ( mTupleMap[key].mType == TYPE )                                 \
        {                                                                   \
            val = mTupleMap[key].MVAL;                                      \
            return true;                                                    \
        }                                                                   \
        else                                                                \
        {                                                                   \
            CERROR << "ERROR: ConfBin::Read(): type mismatch" << endl;      \
            return false;                                                   \
        }                                                                   \
    }                                                                       \
    else                                                                    \
        return false;                                                       \
}

bool
ConfBin::Read( const string& key, int& val /*OUT*/ )
{
    CONF_BIN_READ(mValInt, ConfBin::ConfTuple::eType_INT);
}

bool
ConfBin::Read( const string& key, double& val /*OUT*/ )
{
    CONF_BIN_READ(mValDouble, ConfBin::ConfTuple::eType_DOUBLE);
}

bool
ConfBin::Read( const string& key, string& val /*OUT*/ )
{
    CONF_BIN_READ(mValString, ConfBin::ConfTuple::eType_STRING);
}

/*****************************************************************************
 * @fn bool ConfBin::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 CONF_BIN_ACCESS                                                     \
{                                                                           \
    if ( acc == ConfBin::eAccess_WRITE )                                    \
        return Write( key, val );                                           \
    else                                                                    \
        return Read( key, val );                                            \
}

bool
ConfBin::Access( eAccess acc, const string& key, int& val /*IN/OUT*/ )
{
    CONF_BIN_ACCESS;
}

bool
ConfBin::Access( eAccess acc, const string& key, double& val /*IN/OUT*/ )
{
    CONF_BIN_ACCESS;
}

bool
ConfBin::Access( eAccess acc, const string& key, string& val /*IN/OUT*/ )
{
    CONF_BIN_ACCESS;
}

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

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

/*****************************************************************************
 * Tuple constructor/destructor.
 *****************************************************************************/
ConfBin::ConfTuple::ConfTuple( void )
{
    SET_TYPESIG(this,TYPESIG_CONF_TUPLE);
    Clear();
}

ConfBin::ConfTuple::~ConfTuple()
{
    INVALIDATE_TYPESIG(this,TYPESIG_CONF_TUPLE);
    Clear();
}

void
ConfBin::ConfTuple::Clear( void )
{
    mType      = eType_INVALID;
    mKey.erase();
    mValInt    = 0;
    mValDouble = 0.0;
    mValString.erase();
}

/*****************************************************************************
 * Write configuration tuple to file stream.
 *****************************************************************************/
bool
ConfBin::ConfTuple::Write( std::fstream& fs )
{
CHECK_TYPESIG(this,TYPESIG_CONF_TUPLE);

    switch ( mType )
    {
        case eType_INT:
            fs << "int "    << mKey << ' ' << mValInt    << std::endl;
        break;

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

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

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

/*****************************************************************************
 * Read configuration tuple from file stream.
 *****************************************************************************/
bool
ConfBin::ConfTuple::Read( std::fstream& fs, const string& type )
{
CHECK_TYPESIG(this,TYPESIG_CONF_TUPLE);

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

    if ( type == "int" )
    {
        fs >> mValInt;
        mType = ConfTuple::eType_INT;
    }
    else if ( type == "double" )
    {
        fs >> mValDouble;
        mType = ConfTuple::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 ( UX ( not fs.good() ) )
                return false;
            mValString += c;
        }
        mType = ConfTuple::eType_STRING;
    }
    else
    {
        CERROR << "ERROR: ConfTuple::Read(): bad type '" << mType << "'" << std::endl;
        return false;
    }

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

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

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

} // namespace base
