#include "common/Database.h"
#include "common/Utilities.h"

#include <algorithm>
#include <cstring>
#include <iomanip>
#include <sstream>
#include <string>
#include <tuple>


/********************************************************************
 * Constructors/destructor                                           *
 ********************************************************************/
Database::Database()  = default;
Database::~Database() = default;
Database::Database( const Database& rhs ) : KeyData( rhs )
{
    d_data.clear();
    for ( const auto& tmp : rhs.d_data )
        putData( tmp.first, tmp.second->clone() );
}
Database& Database::operator=( const Database& rhs )
{
    if ( this == &rhs )
        return *this;
    d_data.clear();
    for ( const auto& tmp : rhs.d_data )
        putData( tmp.first, tmp.second->clone() );
    return *this;
}
Database::Database( Database&& rhs ) { std::swap( d_data, rhs.d_data ); }
Database& Database::operator=( Database&& rhs )
{
    if ( this != &rhs )
        std::swap( d_data, rhs.d_data );
    return *this;
}


/********************************************************************
 * Clone the database                                                *
 ********************************************************************/
std::shared_ptr<KeyData> Database::clone() const { return cloneDatabase(); }
std::shared_ptr<Database> Database::cloneDatabase() const
{
    auto db = std::make_shared<Database>();
    for ( const auto& tmp : d_data )
        db->putData( tmp.first, tmp.second->clone() );
    return db;
}


/********************************************************************
 * Get the data object                                               *
 ********************************************************************/
bool Database::keyExists( const std::string& key ) const
{
    return d_data.find( key ) != d_data.end();
}
std::shared_ptr<KeyData> Database::getData( const std::string& key )
{
    auto it = d_data.find( key );
    if ( it == d_data.end() ) {
        char msg[1000];
        sprintf( msg, "Variable %s was not found in database", key.c_str() );
        ERROR( msg );
    }
    return it->second;
}
std::shared_ptr<const KeyData> Database::getData( const std::string& key ) const
{
    return const_cast<Database*>( this )->getData( key );
}
bool Database::isDatabase( const std::string& key ) const
{
    auto ptr  = getData( key );
    auto ptr2 = std::dynamic_pointer_cast<const Database>( ptr );
    return ptr2 != nullptr;
}
std::shared_ptr<Database> Database::getDatabase( const std::string& key )
{
    std::shared_ptr<KeyData> ptr   = getData( key );
    std::shared_ptr<Database> ptr2 = std::dynamic_pointer_cast<Database>( ptr );
    if ( ptr2 == nullptr ) {
        char msg[1000];
        sprintf( msg, "Variable %s is not a database", key.c_str() );
        ERROR( msg );
    }
    return ptr2;
}
std::shared_ptr<const Database> Database::getDatabase( const std::string& key ) const
{
    return const_cast<Database*>( this )->getDatabase( key );
}
std::vector<std::string> Database::getAllKeys() const
{
    std::vector<std::string> keys;
    keys.reserve( d_data.size() );
    for ( const auto& it : d_data )
        keys.push_back( it.first );
    return keys;
}
void Database::putDatabase( const std::string& key, std::shared_ptr<Database> db )
{
    d_data[key] = std::move( db );
}
void Database::putData( const std::string& key, std::shared_ptr<KeyData> data )
{
    d_data[key] = std::move( data );
}


/********************************************************************
 * Is the data of the given type                                     *
 ********************************************************************/
template<>
bool Database::isType<double>( const std::string& key ) const
{
    auto type = getData( key )->type();
    return type == "double";
}
template<>
bool Database::isType<float>( const std::string& key ) const
{
    auto type = getData( key )->type();
    return type == "double";
}
template<>
bool Database::isType<int>( const std::string& key ) const
{
    bool pass = true;
    auto type = getData( key )->type();
    if ( type == "double" ) {
        auto data = getVector<double>( key );
        for ( auto tmp : data )
            pass = pass && static_cast<double>( static_cast<int>( tmp ) ) == tmp;
    } else {
        pass = false;
    }
    return pass;
}
template<>
bool Database::isType<std::string>( const std::string& key ) const
{
    auto type = getData( key )->type();
    return type == "string";
}
template<>
bool Database::isType<bool>( const std::string& key ) const
{
    auto type = getData( key )->type();
    return type == "bool";
}


/********************************************************************
 * Get a vector                                                      *
 ********************************************************************/
template<>
std::vector<std::string> Database::getVector<std::string>(
    const std::string& key, const Units& ) const
{
    std::shared_ptr<const KeyData> ptr = getData( key );
    if ( std::dynamic_pointer_cast<const EmptyKeyData>( ptr ) )
        return std::vector<std::string>();
    const auto* ptr2 = dynamic_cast<const KeyDataString*>( ptr.get() );
    if ( ptr2 == nullptr ) {
        ERROR( "Key '" + key + "' is not a string" );
    }
    return ptr2->d_data;
}
template<>
std::vector<bool> Database::getVector<bool>( const std::string& key, const Units& ) const
{
    std::shared_ptr<const KeyData> ptr = getData( key );
    if ( std::dynamic_pointer_cast<const EmptyKeyData>( ptr ) )
        return std::vector<bool>();
    const auto* ptr2 = dynamic_cast<const KeyDataBool*>( ptr.get() );
    if ( ptr2 == nullptr ) {
        ERROR( "Key '" + key + "' is not a bool" );
    }
    return ptr2->d_data;
}
template<class TYPE>
std::vector<TYPE> Database::getVector( const std::string& key, const Units& unit ) const
{
    std::shared_ptr<const KeyData> ptr = getData( key );
    if ( std::dynamic_pointer_cast<const EmptyKeyData>( ptr ) )
        return std::vector<TYPE>();
    std::vector<TYPE> data;
    if ( std::dynamic_pointer_cast<const KeyDataDouble>( ptr ) ) {
        const auto* ptr2                 = dynamic_cast<const KeyDataDouble*>( ptr.get() );
        const std::vector<double>& data2 = ptr2->d_data;
        double factor                    = 1;
        if ( !unit.isNull() ) {
            INSIST( !ptr2->d_unit.isNull(), "Field " + key + " must have units" );
            factor = ptr2->d_unit.convert( unit );
            INSIST( factor != 0, "Unit conversion failed" );
        }
        data.resize( data2.size() );
        for ( size_t i = 0; i < data2.size(); i++ )
            data[i] = static_cast<TYPE>( factor * data2[i] );
    } else if ( std::dynamic_pointer_cast<const KeyDataString>( ptr ) ) {
        ERROR( "Converting std::string to another type" );
    } else if ( std::dynamic_pointer_cast<const KeyDataBool>( ptr ) ) {
        ERROR( "Converting std::bool to another type" );
    } else {
        ERROR( "Unable to convert data format" );
    }
    return data;
}


/********************************************************************
 * Put a vector                                                      *
 ********************************************************************/
template<>
void Database::putVector<std::string>(
    const std::string& key, const std::vector<std::string>& data, const Units& )
{
    std::shared_ptr<KeyDataString> ptr( new KeyDataString() );
    ptr->d_data = data;
    d_data[key] = ptr;
}
template<>
void Database::putVector<bool>(
    const std::string& key, const std::vector<bool>& data, const Units& )
{
    std::shared_ptr<KeyDataBool> ptr( new KeyDataBool() );
    ptr->d_data = data;
    d_data[key] = ptr;
}
template<class TYPE>
void Database::putVector( const std::string& key, const std::vector<TYPE>& data, const Units& unit )
{
    std::shared_ptr<KeyDataDouble> ptr( new KeyDataDouble() );
    ptr->d_unit = unit;
    ptr->d_data.resize( data.size() );
    for ( size_t i = 0; i < data.size(); i++ )
        ptr->d_data[i] = static_cast<double>( data[i] );
    d_data[key] = ptr;
}


/********************************************************************
 * Print the database                                                *
 ********************************************************************/
void Database::print( std::ostream& os, const std::string& indent ) const
{
    for ( const auto& it : d_data ) {
        os << indent << it.first;
        if ( dynamic_cast<const Database*>( it.second.get() ) ) {
            const auto* db = dynamic_cast<const Database*>( it.second.get() );
            os << " {\n";
            db->print( os, indent + "   " );
            os << indent << "}\n";
        } else {
            os << " = ";
            it.second->print( os, "" );
        }
    }
}
std::string Database::print( const std::string& indent ) const
{
    std::stringstream ss;
    print( ss, indent );
    return ss.str();
}


/********************************************************************
 * Read input database file                                          *
 ********************************************************************/
Database::Database( const std::string& filename )
{
    // Read the input file into memory
    FILE* fid = fopen( filename.c_str(), "rb" );
    if ( fid == nullptr )
        ERROR( "Error opening file " + filename );
    fseek( fid, 0, SEEK_END );
    size_t bytes = ftell( fid );
    rewind( fid );
    auto* buffer  = new char[bytes + 4];
    size_t result = fread( buffer, 1, bytes, fid );
    fclose( fid );
    if ( result != bytes )
        ERROR( "Error reading file " + filename );
    buffer[bytes + 0] = '\n';
    buffer[bytes + 1] = '}';
    buffer[bytes + 2] = '\n';
    buffer[bytes + 3] = 0;
    // Create the database entries
    loadDatabase( buffer, *this );
    // Free temporary memory
    delete[] buffer;
}
std::shared_ptr<Database> Database::createFromString( const std::string& data )
{
    std::shared_ptr<Database> db( new Database() );
    auto* buffer = new char[data.size() + 4];
    memcpy( buffer, data.data(), data.size() );
    buffer[data.size() + 0] = '\n';
    buffer[data.size() + 1] = '}';
    buffer[data.size() + 2] = '\n';
    buffer[data.size() + 3] = 0;
    loadDatabase( buffer, *db );
    delete[] buffer;
    return db;
}
enum class token_type {
    newline,
    line_comment,
    block_start,
    block_stop,
    quote,
    equal,
    bracket,
    end_bracket,
    end
};
inline size_t length( token_type type )
{
    size_t len = 0;
    if ( type == token_type::newline || type == token_type::quote || type == token_type::equal ||
         type == token_type::bracket || type == token_type::end_bracket ||
         type == token_type::end ) {
        len = 1;
    } else if ( type == token_type::line_comment || type == token_type::block_start ||
                type == token_type::block_stop ) {
        len = 2;
    }
    return len;
}
inline std::tuple<size_t, token_type> find_next_token( const char* buffer )
{
    size_t i = 0;
    while ( true ) {
        if ( buffer[i] == '\n' || buffer[i] == '\r' ) {
            return std::pair<size_t, token_type>( i + 1, token_type::newline );
        } else if ( buffer[i] == 0 ) {
            return std::pair<size_t, token_type>( i + 1, token_type::end );
        } else if ( buffer[i] == '"' ) {
            return std::pair<size_t, token_type>( i + 1, token_type::quote );
        } else if ( buffer[i] == '=' ) {
            return std::pair<size_t, token_type>( i + 1, token_type::equal );
        } else if ( buffer[i] == '{' ) {
            return std::pair<size_t, token_type>( i + 1, token_type::bracket );
        } else if ( buffer[i] == '}' ) {
            return std::pair<size_t, token_type>( i + 1, token_type::end_bracket );
        } else if ( buffer[i] == '/' ) {
            if ( buffer[i + 1] == '/' ) {
                return std::pair<size_t, token_type>( i + 2, token_type::line_comment );
            } else if ( buffer[i + 1] == '*' ) {
                return std::pair<size_t, token_type>( i + 2, token_type::block_start );
            }
        } else if ( buffer[i] == '*' ) {
            if ( buffer[i + 1] == '/' )
                return std::pair<size_t, token_type>( i + 2, token_type::block_stop );
        }
        i++;
    }
    return std::pair<size_t, token_type>( 0, token_type::end );
}
inline std::string deblank( const std::string& str )
{
    size_t i1 = 0xFFFFFFF, i2 = 0;
    for ( size_t i = 0; i < str.size(); i++ ) {
        if ( str[i] != ' ' ) {
            i1 = std::min( i1, i );
            i2 = std::max( i2, i );
        }
    }
    return i1 <= i2 ? str.substr( i1, i2 - i1 + 1 ) : std::string();
}
size_t skip_comment( const char* buffer )
{
    auto tmp                     = find_next_token( buffer );
    const token_type end_comment = ( std::get<1>( tmp ) == token_type::line_comment ) ?
                                       token_type::newline :
                                       token_type::block_stop;
    size_t pos = 0;
    while ( std::get<1>( tmp ) != end_comment ) {
        if ( std::get<1>( tmp ) == token_type::end )
            ERROR( "Encountered end of file before block comment end" );
        pos += std::get<0>( tmp );
        tmp = find_next_token( &buffer[pos] );
    }
    pos += std::get<0>( tmp );
    return pos;
}
inline std::string lower( const std::string& str )
{
    std::string tmp( str );
    std::transform( tmp.begin(), tmp.end(), tmp.begin(), ::tolower );
    return tmp;
}
static std::tuple<size_t, std::shared_ptr<KeyData>> read_value(
    const char* buffer, const std::string& key )
{
    // Get the value as a std::string
    size_t pos            = 0;
    token_type type       = token_type::end;
    std::tie( pos, type ) = find_next_token( &buffer[pos] );
    size_t len            = pos - length( type );
    while ( type != token_type::newline ) {
        if ( type == token_type::quote ) {
            size_t i            = 0;
            std::tie( i, type ) = find_next_token( &buffer[pos] );
            pos += i;
            while ( type != token_type::quote ) {
                ASSERT( type != token_type::end );
                std::tie( i, type ) = find_next_token( &buffer[pos] );
                pos += i;
            }
        } else if ( type == token_type::line_comment || type == token_type::block_start ) {
            len = pos - length( type );
            pos += skip_comment( &buffer[pos - length( type )] ) - length( type );
            break;
        }
        size_t i            = 0;
        std::tie( i, type ) = find_next_token( &buffer[pos] );
        pos += i;
        len = pos - length( type );
    }
    const std::string value = deblank( std::string( buffer, len ) );
    // Split the value to an array of values
    std::vector<std::string> values;
    size_t i0 = 0, i = 0, count = 0;
    for ( ; i < value.size(); i++ ) {
        if ( value[i] == '"' ) {
            count++;
        } else if ( value[i] == ',' && count % 2 == 0 ) {
            values.push_back( deblank( value.substr( i0, i - i0 ) ) );
            i0 = i + 1;
        }
    }
    values.push_back( deblank( value.substr( i0 ) ) );
    // Convert the string value to the database value
    std::shared_ptr<KeyData> data;
    if ( value.empty() ) {
        data.reset( new EmptyKeyData() );
    } else if ( value.find( '"' ) != std::string::npos ) {
        auto* data2 = new KeyDataString();
        data.reset( data2 );
        data2->d_data.resize( values.size() );
        for ( size_t i = 0; i < values.size(); i++ ) {
            ASSERT( values[i].size() >= 2 );
            ASSERT( values[i][0] == '"' && values[i][values[i].size() - 1] == '"' );
            data2->d_data[i] = values[i].substr( 1, values[i].size() - 2 );
        }
    } else if ( lower( value ) == "true" || lower( value ) == "false" ) {
        auto* data2 = new KeyDataBool();
        data.reset( data2 );
        data2->d_data.resize( values.size() );
        for ( size_t i = 0; i < values.size(); i++ ) {
            ASSERT( values[i].size() >= 2 );
            if ( lower( values[i] ) != "true" && lower( values[i] ) != "false" )
                ERROR( "Error converting " + key + " to logical array" );
            data2->d_data[i] = lower( values[i] ) == "true";
        }
    } else { // if ( value.find('.')!=std::string::npos || value.find('e')!=std::string::npos ) {
        auto* data2 = new KeyDataDouble();
        data.reset( data2 );
        data2->d_data.resize( values.size(), 0 );
        for ( size_t i = 0; i < values.size(); i++ ) {
            Units unit;
            std::tie( data2->d_data[i], unit ) = KeyDataDouble::read( values[i] );
            if ( !unit.isNull() )
                data2->d_unit = unit;
        }
        //} else {
        //    ERROR("Unable to determine data type: "+value);
    }
    return std::tuple<size_t, std::shared_ptr<KeyData>>( pos, data );
}
size_t Database::loadDatabase( const char* buffer, Database& db )
{
    size_t pos = 0;
    while ( true ) {
        size_t i;
        token_type type;
        std::tie( i, type ) = find_next_token( &buffer[pos] );
        const std::string key =
            deblank( std::string( &buffer[pos], std::max<int>( i - length( type ), 1 ) - 1 ) );
        if ( type == token_type::line_comment || type == token_type::block_start ) {
            // Comment
            INSIST( key.empty(), "Key should be empty: " + key );
            pos += skip_comment( &buffer[pos] );
        } else if ( type == token_type::newline ) {
            INSIST( key.empty(), "Key should be empty: " + key );
            pos += i;
        } else if ( type == token_type::equal ) {
            // Reading key/value pair
            ASSERT( !key.empty() );
            pos += i;
            std::shared_ptr<KeyData> data;
            std::tie( i, data ) = read_value( &buffer[pos], key );
            ASSERT( data.get() != nullptr );
            db.d_data[key] = data;
            pos += i;
        } else if ( type == token_type::bracket ) {
            // Read database
            ASSERT( !key.empty() );
            pos += i;
            std::shared_ptr<Database> database( new Database() );
            pos += loadDatabase( &buffer[pos], *database );
            db.d_data[key] = database;
        } else if ( type == token_type::end_bracket ) {
            // Finished with the database
            pos += i;
            break;
        } else {
            ERROR( "Error loading data" );
        }
    }
    return pos;
}


/********************************************************************
 * Data type helper functions                                        *
 ********************************************************************/
void KeyDataDouble::print( std::ostream& os, const std::string& indent ) const
{
    os << indent;
    for ( size_t i = 0; i < d_data.size(); i++ ) {
        if ( i > 0 )
            os << ", ";
        if ( d_data[i] != d_data[i] ) {
            os << "nan";
        } else if ( d_data[i] == std::numeric_limits<double>::infinity() ) {
            os << "inf";
        } else if ( d_data[i] == -std::numeric_limits<double>::infinity() ) {
            os << "-inf";
        } else {
            os << std::setprecision( 12 ) << d_data[i];
        }
    }
    if ( !d_unit.isNull() )
        os << " " << d_unit.str();
    os << std::endl;
}
std::tuple<double, Units> KeyDataDouble::read( const std::string& str )
{
    std::string tmp = deblank( str );
    size_t index    = tmp.find( " " );
    if ( index != std::string::npos ) {
        return std::make_tuple(
            readValue( tmp.substr( 0, index ) ), Units( tmp.substr( index + 1 ) ) );
    } else {
        return std::make_tuple( readValue( tmp ), Units() );
    }
}
double KeyDataDouble::readValue( const std::string& str )
{
    const std::string tmp = lower( str );
    double data           = 0;
    if ( tmp == "inf" || tmp == "infinity" ) {
        data = std::numeric_limits<double>::infinity();
    } else if ( tmp == "-inf" || tmp == "-infinity" ) {
        data = -std::numeric_limits<double>::infinity();
    } else if ( tmp == "nan" ) {
        data = std::numeric_limits<double>::quiet_NaN();
    } else if ( tmp.find( '/' ) != std::string::npos ) {
        ERROR( "Error reading value" );
    } else {
        char* pos = nullptr;
        data      = strtod( tmp.c_str(), &pos );
        if ( static_cast<size_t>( pos - tmp.c_str() ) == tmp.size() + 1 )
            ERROR( "Error reading value" );
    }
    return data;
}


/********************************************************************
 * Instantiations                                                    *
 ********************************************************************/
template std::vector<char> Database::getVector<char>( const std::string&, const Units& ) const;
template std::vector<int> Database::getVector<int>( const std::string&, const Units& ) const;
template std::vector<size_t> Database::getVector<size_t>( const std::string&, const Units& ) const;
template std::vector<float> Database::getVector<float>( const std::string&, const Units& ) const;
template std::vector<double> Database::getVector<double>( const std::string&, const Units& ) const;
template void Database::putVector<char>(
    const std::string&, const std::vector<char>&, const Units& );
template void Database::putVector<int>( const std::string&, const std::vector<int>&, const Units& );
template void Database::putVector<size_t>(
    const std::string&, const std::vector<size_t>&, const Units& );
template void Database::putVector<float>(
    const std::string&, const std::vector<float>&, const Units& );
template void Database::putVector<double>(
    const std::string&, const std::vector<double>&, const Units& );
template bool Database::isType<int>( const std::string& ) const;
template bool Database::isType<float>( const std::string& ) const;
template bool Database::isType<double>( const std::string& ) const;
template bool Database::isType<std::string>( const std::string& ) const;
