/** \file
 *
 *  Contains the Config class implementation.
 *
 *  Copyright (c) 2007,2008,2009 MBARI
 *  MBARI Proprietary Information.  All Rights Reserved
 *
 */

#include "Config.h"
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>

#include "component/ComponentRegistry.h"
#include "data/ConfigDataElement.h"
#include "data/Slate.h"
#include "data/StrValue.h"
#include "io/FileInStream.h"
#include "io/FileOutStream.h"
#include "supervisor/Supervisor.h"
#include "units/UnitRegistry.h"

FastMap<const Str, Str*> Config::PersistedConfigSetMap_;
bool Config::ignorePersistedConfig_ = false;

/// Copies the indicated file and its path to the destDir
void Config::CopyFileToDir( const Str& path, const char* name, const Str& destDir, Logger& logger )
{
    Str cmd( "mkdir -p " );
    cmd += destDir;
    cmd += "/";
    cmd += path;
    cmd += ";cp -r ";
    cmd += path;
    cmd += name;
    cmd += " ";
    cmd += destDir;
    cmd += "/";
    cmd += path;

    int status = system( cmd.cStr() );
    if( -1 == status )
    {
        logger.syslog( "Cannot execute " + cmd, Syslog::ERROR );
    }
}

void Config::LoadConfigFiles( const Str& path, const Str& altConfig, const Str& dataDir, Logger& logger )
{
    LoadConfigFilesInPath( path, "", dataDir, logger );
    Str hostnamePath;
    Str usernamePath;
    Str simPath;
    Str simUsernamePath;
    const size_t len( 100 );
    char hostname[len];
    *hostname = 0;
    if( 0 == gethostname( hostname, len ) )
    {
        char* dotAt = strchr( hostname, '.' );
        if( NULL != dotAt && hostname != dotAt )
        {
            *dotAt = 0;
        }
        hostnamePath =  Str( hostname ) + "/";
    }
    char* login = getlogin();
    if( hostnamePath != Str::EMPTY_STR )
    {
        LoadConfigFilesInPath( path, hostnamePath, dataDir, logger );
        if( NULL != login && *login != 0 )
        {
            usernamePath = hostnamePath + getlogin() + "/";
            LoadConfigFilesInPath( path, usernamePath, dataDir, logger );
        }
    }
#ifndef __arm__
    simPath = "sim/";
#endif //#ifndef __arm__
    if( simPath != Str::EMPTY_STR )
    {
        LoadConfigFilesInPath( path, simPath, dataDir, logger );
        if( NULL != login && *login != 0 )
        {
            simUsernamePath = simPath + getlogin() + "/";
            LoadConfigFilesInPath( path, simUsernamePath, dataDir, logger );
        }
        else
        {
            // else: try cuserid (which returns the "effective user"):
            login = cuserid( NULL );
            if( NULL != login && *login != 0 )
            {
                simUsernamePath = simPath + login + "/";
                LoadConfigFilesInPath( path, simUsernamePath, dataDir, logger );
            }
        }
    }
    if( altConfig != Str::EMPTY_STR )
    {
        if( altConfig.startsWith( "/" ) || altConfig.startsWith( "." ) )
        {
            // Read configs from fully qualified path
            LoadConfigFilesInPath( "", altConfig, dataDir, logger );
        }
        else
        {
            LoadConfigFilesInPath( path, altConfig, dataDir, logger );
            if( hostnamePath != Str::EMPTY_STR )
            {
                LoadConfigFilesInPath( path, hostnamePath + altConfig, dataDir, logger );
                if( NULL != login && *login != 0 )
                {
                    LoadConfigFilesInPath( path, usernamePath + altConfig, dataDir, logger );
                }
            }
            if( simPath != Str::EMPTY_STR )
            {
                LoadConfigFilesInPath( path, simPath + altConfig, dataDir, logger );
                if( NULL != login && *login != 0 )
                {
                    LoadConfigFilesInPath( path, simUsernamePath + altConfig, dataDir, logger );
                }
            }
        }
    }
    LoadPersistedConfigSets( logger, false, false );
}

void Config::Persist( const Str& name, const char* line, Logger& logger )
{
    struct stat st;
    char persisted_cfg[] = "Data/persisted.cfg";

    /*
     * After an unclean shutdown the persisted config would be ignored so
     * attempting to write persisted.cfg would overwrite all previously set
     * configs. We don't allow that. However, there is no reason to not allow
     * creating a new persisted.cfg. We just have to make sure that when a new
     * persisted.cfg is created we remove the ignorePersistedConfig_ flag,
     * otherwise only one config can be written because now persisted.cfg
     * exists again. This is save to do because we started with an empty file
     * and all subsequent additions are cached in memory.
     */
    if( stat( persisted_cfg, &st ) == 0 && ignorePersistedConfig_ )
    {
        logger.syslog( "Not allowing to overwrite existing Data/persisted.cfg after unclean shutdown.", Syslog::FAULT );
        return;
    }
    ignorePersistedConfig_ = false;

    Str* oldLine = PersistedConfigSetMap_.get( name );

    Str lineStr( line );
    if( NULL == oldLine )
    {
        PersistedConfigSetMap_.put( name, new Str( lineStr ) );
    }
    else if( lineStr != *oldLine )
    {
        delete PersistedConfigSetMap_.pop( name );
        PersistedConfigSetMap_.put( name, new Str( lineStr ) );
    }
    FILE* fid = fopen( persisted_cfg, "wb" );
    for( unsigned int i = 0; i < PersistedConfigSetMap_.size(); ++i )
    {
        Str* lineStrPtr = PersistedConfigSetMap_.getIndexed( i );
        fwrite( lineStrPtr->cStr(), lineStrPtr->length(), 1, fid );
    }
    fclose( fid );
}

void Config::LoadPersistedConfigSets( Logger& logger, bool reload, bool list )
{
    if( ( !reload && !list && Supervisor::WasRunning() ) || ignorePersistedConfig_ )
    {
        logger.syslog( "Ignoring configuration overrides from Data/persisted.cfg", Syslog::FAULT );
        ignorePersistedConfig_ = true;
    }
    else
    {
        logger.syslog( list ? "List" : ( reload ? "Reload" : "Read" ), "ing configuration overrides from Data/persisted.cfg", Syslog::IMPORTANT );
        FILE* fid = fopen( "Data/persisted.cfg", "rb" );
        bool someListed = false;
        Component* configDb = ComponentRegistry::Find( "configDb" );
        if( NULL != fid )
        {
            char line [ 256 ];
            while( fgets( line, sizeof line, fid ) != NULL )
            {
                Str lineStr( line );
                if( list )
                {
                    someListed = true;
                    logger.syslog( line, Syslog::IMPORTANT );
                }
                else
                {
                    ParseLine( configDb, line, lineStr.length(), false );
                    Str name;
                    size_t eqAt = lineStr.find( "=" );
                    if( eqAt != Str::NO_POS )
                    {
                        name = lineStr.substr( 0, eqAt );
                    }
                    else
                    {
                        size_t space1At = lineStr.find( " " );
                        size_t space2At = lineStr.find( " ", space1At + 1 );
                        size_t space3At = lineStr.find( " ", space2At + 1 );
                        if( space3At == Str::NO_POS )
                        {
                            space3At = lineStr.find( ";", space2At + 1 );
                        }
                        name = lineStr.substr( 0, space1At + 1 )
                               + lineStr.substr( space2At + 1, space3At - space2At - 1 );
                    }
                    Str* oldLine = PersistedConfigSetMap_.get( name );
                    if( NULL == oldLine )
                    {
                        PersistedConfigSetMap_.put( name, new Str( lineStr ) );
                    }
                    else if( lineStr != *oldLine )
                    {
                        delete PersistedConfigSetMap_.pop( name );
                        PersistedConfigSetMap_.put( name, new Str( lineStr ) );
                    }
                }
            }
            fclose( fid );
        }
        if( list && !someListed )
        {
            logger.syslog( "No configSet variables persisted", Syslog::IMPORTANT );
        }
    }
}

Config* Config::Instance( const Str& realPath, const Str& basePath, const char* name, const Str& dataDir, bool& duplicate, Logger& logger )
{
    bool secure = 0 == strncasecmp( name, "secure", 6 );
    Str realFilename( realPath + name );
    FileInStream in( realFilename.cStr() );
    logger.syslog( "Opening Config file at: " + realFilename, Syslog::INFO );
    if( !in.isReadable() )
    {
        logger.syslog( "Can not open Config file at: " + realFilename, Syslog::ERROR );
        return NULL;
    }
    Str logFilename( basePath + name );
    unsigned int lastDotAt = logFilename.findLastOf( '.' );
    Str configName = logFilename.substr( 0, lastDotAt );

    Config* config( dynamic_cast<Config*>( ComponentRegistry::Find( configName ) ) );
    duplicate = NULL != config;
    if( !duplicate )
    {
        config = new Config( configName );
    }
    const unsigned int bufferSize( 256 );
    char buffer[bufferSize];
    *buffer = 0;
    bool someParsed = false;
    while( !in.eof() )
    {
        in.readLine( buffer, bufferSize );
        someParsed |= ParseLine( config, buffer, in.bytesRead(), secure );
    }

    // Copy the configuration file into the dataDir directory
    if( dataDir != Str::EMPTY_STR )
    {
        CopyFileToDir( realPath, name, dataDir, logger );
    }
    /*
    Str copyFilename = dataDir + Str( realFilename, realFilename.findLastOf( '/' ) );
    FileInStream cpIn( realFilename.cStr() );
    FileOutStream cpOut( copyFilename.cStr(), false, false );
    while ( cpOut.isWritable() && !cpIn.eof() )
    {
        cpIn.read( buffer, bufferSize );
        cpOut.write( buffer, cpIn.bytesRead() );
    }*/

    if( !someParsed && !duplicate )
    {
        ComponentRegistry::Remove( config );
        delete config;
        config = NULL;
    }
    return config;
}

void Config::LoadConfigFilesInPath( const Str& basePath, const Str& addPath, const Str& dataDir, Logger& logger )
{
    Str realPath( basePath + addPath );
    logger.syslog( "Looking for Config files in directory: " + realPath, Syslog::INFO );
    DIR* dir( opendir( realPath.cStr() ) );
    if( NULL == dir )
    {
        if( addPath == Str::EMPTY_STR )
        {
            logger.syslog( "Unable to open directory: " + realPath, Syslog::ERROR );
        }
    }
    else
    {
        union // To use thread-safe readdir_r make sure dirent struct is big enough
        {
            struct dirent rent_;
            char b_[offsetof( struct dirent, d_name ) + NAME_MAX + 1];
        } di;
        struct dirent* entry;
        while( 0 == readdir_r( dir, &di.rent_, &entry ) && entry != NULL )
        {
            if( entry != NULL )
            {
                char* extension = strcasestr( entry->d_name, ".cfg" );
                if( extension == entry->d_name + ( strlen( entry->d_name ) - 4 ) )
                {
                    bool duplicate( false );
                    Config* config = Config::Instance( realPath, basePath, entry->d_name, dataDir, duplicate, logger );
                    if( NULL != config && !duplicate )
                    {
                        ComponentRegistry::Insert( config );
                    }
                }
            }
        }
        closedir( dir );
    }
}

bool Config::ParseLine( Component* config, char* buffer, unsigned int length, bool secure )
{
    // Parse out three space, "equals", and colon delimited words.

    const char* delim( " =;" );
    char* word0 = strtok_r( buffer, delim, &buffer );
    while( *buffer == ' ' || *buffer == '=' )
    {
        ++buffer;
    }

    if( NULL == word0 || *word0 == '/' )
    {
        return false;
    }
    if( NULL == buffer || *buffer == '/' || *buffer == 0 )
    {
        return false;
    }

    Service::ServiceType serviceType = Service::Str2ServiceType( word0 );
    if( serviceType != Service::SERVICE_UNSPECIFIED )
    {
        char* word1 = strtok_r( NULL, delim, &buffer );
        DecimationLogWriter::DecimationType decimationType = DecimationLogWriter::Str2DecimationType( word1 );
        if( decimationType == DecimationLogWriter::UNDEFINED_DECIMATION )
        {
            config->getLogger().syslog( "Unknown decimation type: ", word1, Syslog::ERROR );
            return false;
        }
        char* name = strtok_r( NULL, delim, &buffer );
        if( NULL == name || *name == 0 )
        {
            config->getLogger().syslog( Str( "Missing variable name for decimation configuration: " ) + word0 + " " + word1, Syslog::ERROR );
            return false;
        }

        DataValue* parameter = NULL;
        if( decimationType == DecimationLogWriter::LINEAR_APPROXIMATION )
        {
            char* word3 = strtok_r( NULL, delim, &buffer );
            char* endPtr;
            double value( strtod( word3, &endPtr ) );
            if( endPtr == word3 )
            {
                config->getLogger().syslog( "Could not parse value: ", word3, Syslog::ERROR );
                return false;
            }

            // followed by a unit
            char* word4 = strtok_r( NULL, delim, &buffer );
            const Unit* unit = UnitRegistry::FindUnit( word4 );
            if( NULL == unit )
            {
                config->getLogger().syslog( Str( "Could not find unit: " ) + ( word4 == NULL ? "<null>" : word4 ) + " following " + word0 + " " + word1 + " " + name + " " + word3, Syslog::ERROR );
                return false;
            }
            parameter = unit->dataValue( value );
        }
        Supervisor::ConfigureDecimation( serviceType, decimationType, name, parameter, config->getLogger() );
        return true;
    }

    bool hasDot = NULL != strchr( word0, '.' );
    ElementURI* uri;
    if( !hasDot )
    {
        if( NULL == config )
        {
            return false;
        }
        uri = Slate::FindElementURI( config->getName() + "." + word0 );
    }
    else
    {
        uri = Slate::FindElementURI( word0 );
    }
    DataElement* element = uri == NULL ? NULL : Slate::GetElement( uri->getCode() );
    // Instace of foo = "bar";
    if( *buffer == '\"' )
    {
        unsigned int maxLen = strlen( buffer );
        unsigned int wordLen = 2;
        for( ; wordLen < maxLen; ++wordLen )
        {
            if( buffer[wordLen - 1] == '\"' )
            {
                break;
            }
        }
        if( maxLen > 1 && buffer[wordLen - 1] == '\"' )
        {
            StrValue quoted( buffer + 1, wordLen - 2 );
            if( NULL != element && element->getBaseUnit() != &quoted.getBaseUnit() )
            {
                //Slate::DeRegisterElementURI( uri );
                config->getLogger().syslog( "Attempting to set a string configuration value to the non-string configuration: " + Str( word0 ), Syslog::CRITICAL );
            }
            else if( hasDot )
            {
                if( NULL == element )
                {
                    ConfigURI newUri( "", word0, Units::NONE );
                    element = new ConfigDataElement( newUri, quoted.copy() );
                    Slate::MapDataElement( &newUri, element );
                }

                static_cast<ConfigDataElement*>( element )->setConfigValue( &quoted );
                Slate::WriteOnce( element->getUri().getCode(), config, quoted );
                static_cast<ConfigDataElement*>( element )->setSecure( secure );
            }
            else
            {
                Slate::WriteOnce( word0, config, quoted );
            }
            return true;
        }
        else
        {
            config->getLogger().syslog( "Unterminated quote: ", buffer, Syslog::ERROR );
            return false;
        }
    }

    // else its a number
    char* word1 = strtok_r( NULL, delim, &buffer );
    char* endPtr;
    double value( strtod( word1, &endPtr ) );
    if( endPtr == word1 )
    {
        config->getLogger().syslog( "Could not parse value: ", word1, Syslog::ERROR );
        return false;
    }

    // followed by a unit
    char* word2 = strtok_r( NULL, delim, &buffer );
    const Unit* unit = UnitRegistry::FindUnit( word2 );
    if( NULL == unit )
    {
        config->getLogger().syslog( Str( "Could not find unit: " ) + ( word2 == NULL ? "<null>" : word2 ) + " following " + word0 + " = " + word1, Syslog::ERROR );
        return false;
    }

    if( NULL != element && element->getBaseUnit() != &unit->getBaseUnit() )
    {
        config->getLogger().syslog( Str( "Changing base units of " ) + word0 + " from " + element->getUnit()->getName() + " to " + unit->getName(), Syslog::FAULT );
        //Slate::DeRegisterElementURI( uri );
    }

    bool ok = true;
    if( hasDot )
    {
        DataValue* dataValue = unit->dataValue( value );
        if( NULL == element )
        {
            ConfigURI newUri( "", word0, *unit );
            element = new ConfigDataElement( newUri, dataValue );
            Slate::MapDataElement( &newUri, element );
            dataValue = dataValue->copy();
        }

        static_cast<ConfigDataElement*>( element )->setConfigValue( dataValue );
        //printf("Slate::WriteOnce( %s(%hd), %s, %s #%08ZX )\n",element->getUri().cStr(),element->getUri().getCode(),config->getName().cStr(), dataValue->toString().cStr(), (size_t)dataValue);
        Slate::WriteOnce( element->getUri().getCode(), config, *dataValue );
        static_cast<ConfigDataElement*>( element )->setSecure( secure );
        delete dataValue;

    }
    else
    {
        ok = Slate::WriteOnce( word0, config, *unit, value );
    }

    if( ok )
    {
        return true;
    }
    else
    {
        config->getLogger().syslog( "Could not write: " + Str( word0 ) + " = " +
                                    + value + " " + unit->getName(), Syslog::ERROR );
        return false;
    }
}

Config::Config( const Str& name )
    : Component( name, COMPONENT_CONFIG, NULL )
{}

/// Release static resources.
void Config::Uninitialize()
{
    while( PersistedConfigSetMap_.size() > 0 )
    {
        delete PersistedConfigSetMap_.popIndexed( PersistedConfigSetMap_.size() - 1 );
    }
}
