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

#include "data/BlobValue.h"
#include "data/ElementURI.h"
#include "data/StrValue.h"
#include "utils/NetCdfWriter.h"

#include <math.h>
#include <cstdio>

#define HEADER_VERSION 1

/// Returns an newNetCdfWriter of a NetCdfWriter for a file
NetCdfWriter* NetCdfWriter::NewNetCdfWriter( const char* fileName, bool version4, const char* trajectoryName )
{
    NetCdfWriter * netCdfWriter = new NetCdfWriter( fileName, version4, trajectoryName );
    if( !netCdfWriter->isOk() )
    {
        printf( "NetCdf error: %s\n", netCdfWriter->status() );
        delete netCdfWriter;
        netCdfWriter = NULL;
    }
    return netCdfWriter;
}

/// Destructor
NetCdfWriter::~NetCdfWriter()
{
    groups_.clear( true );
    finalize();
}

const NetCdf::NetCdfAtt* NetCdfWriter::addGlobalAtt( const Str& name, const Str& value )
{
    return addAtt( gatts_, NC_GLOBAL, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addGlobalAtt( const Str& name, const unsigned char value )
{
    return addAtt( gatts_, NC_GLOBAL, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addGlobalAtt( const Str& name, const short value )
{
    return addAtt( gatts_, NC_GLOBAL, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addGlobalAtt( const Str& name, const int value )
{
    return addAtt( gatts_, NC_GLOBAL, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addGlobalAtt( const Str& name, const float value )
{
    return addAtt( gatts_, NC_GLOBAL, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addGlobalAtt( const Str& name, const double value )
{
    return addAtt( gatts_, NC_GLOBAL, name, value );
}

const NetCdf::NetCdfDim* NetCdfWriter::addDim( const Str& name, const unsigned int dimSize )
{
    if( !ok_ )
    {
        return NULL;
    }
    NetCdfDim* netCdfDim = dims_.get( name );
    if( NULL == netCdfDim )
    {
        netCdfDim = new NetCdfDim();
        netCdfDim->dimSize_ = dimSize;
        status_ = nc_def_dim( ncId_, name.cStr(), dimSize, &netCdfDim->dimId_ );
        ok_ = status_ == NC_NOERR;
        if( ok_ )
        {
            dims_.put( name, netCdfDim );
            dimArray_.push( netCdfDim );
        }
        else
        {
            delete netCdfDim;
            netCdfDim = NULL;
        }
    }
    return netCdfDim;
}

NetCdf::NetCdfVar* NetCdfWriter::addVar( const Str& name, const NetCdfType netCdfType, const NetCdfDim* dimTime, const NetCdfDim* dimM, const NetCdfDim* dimN, const NetCdfDim* dimO )
{
    if( !ok_ )
    {
        return NULL;
    }
    NetCdfVar* netCdfVar = vars_.get( name );
    if( NULL == netCdfVar )
    {
        netCdfVar = new NetCdfVar();
        const unsigned int nDims = 1 + ( NULL == dimM ? 0 : ( NULL == dimN ? 1 : ( NULL == dimO ? 2 : 3 ) ) );
        netCdfVar->nDims_ = nDims;
        netCdfVar->dimIds_ = new int[ nDims ];
        netCdfVar->dimIds_[0] = NULL != dimTime ? dimTime->dimId_ : ( NULL != timeDim_ ? timeDim_->dimId_ : 0 );
        if( NULL != dimM )
        {
            netCdfVar->dimIds_[1] = dimM->dimId_;
            if( NULL != dimN )
            {
                netCdfVar->dimIds_[2] = dimN->dimId_;
                if( NULL != dimO )
                {
                    netCdfVar->dimIds_[3] = dimO->dimId_;
                }
            }
        }
        netCdfVar->isRecord_ = true;
        netCdfVar->netCdfType_ = netCdfType;
        netCdfVar->ncId_ = ncId_;
        status_ = nc_def_var( ncId_, name.cStr(), ( nc_type )netCdfType, nDims, netCdfVar->dimIds_, &netCdfVar->varId_ );
        ok_ = status_ == NC_NOERR;
        if( ok_ && version4_ )
        {
            size_t chunksize[4] = {256, NULL == dimM ? 0 : dimM->dimSize_, NULL == dimN ? 0 : dimN->dimSize_, NULL == dimO ? 0 : dimO->dimSize_};
            status_ &= nc_def_var_chunking( ncId_, netCdfVar->varId_, NC_CHUNKED, chunksize );
            ok_ = status_ == NC_NOERR;
            if( ok_ )
            {
                status_ &= nc_def_var_deflate( ncId_, netCdfVar->varId_,  0, 1, 1 );
                ok_ = status_ == NC_NOERR;
            }
        }

        if( ok_ )
        {
            netCdfVar->varIndex_ = varArray_.size();
            netCdfVar->dataValue_ = NULL;
            vars_.put( name, netCdfVar );
            varArray_.push( netCdfVar );
        }
        else
        {
            delete netCdfVar;
            netCdfVar = NULL;
        }
    }
    return netCdfVar;
}

NetCdf::NetCdfVar* NetCdfWriter::addStaticVar( const Str& name, const NetCdfType netCdfType, const NetCdfDim* dim )
{
    if( !ok_ )
    {
        return NULL;
    }
    NetCdfVar* netCdfVar = vars_.get( name );
    if( NULL == netCdfVar )
    {
        netCdfVar = new NetCdfVar();
        const unsigned int nDims = 1;
        netCdfVar->dimIds_ = new int[ nDims ];
        netCdfVar->dimIds_[0] = dim->dimId_;
        netCdfVar->isRecord_ = false;
        netCdfVar->ncId_ = ncId_;
        status_ = nc_def_var( ncId_, name.cStr(), ( nc_type )netCdfType, nDims, netCdfVar->dimIds_, &netCdfVar->varId_ );
        ok_ = status_ == NC_NOERR;
        if( ok_ )
        {
            netCdfVar->varIndex_ = varArray_.size();
            netCdfVar->dataValue_ = NULL;
            vars_.put( name, netCdfVar );
            varArray_.push( netCdfVar );
        }
        else
        {
            delete netCdfVar;
            netCdfVar = NULL;
        }
    }
    return netCdfVar;
}

const NetCdf::NetCdfAtt* NetCdfWriter::addVarAtt( NetCdfVar* netCdfVar, const Str& name, const Str& value )
{
    return addAtt( netCdfVar->atts_, netCdfVar->varId_, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addVarAtt( NetCdfVar* netCdfVar, const Str& name, const unsigned char value )
{
    return addAtt( netCdfVar->atts_, netCdfVar->varId_, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addVarAtt( NetCdfVar* netCdfVar, const Str& name, const int value )
{
    return addAtt( netCdfVar->atts_, netCdfVar->varId_, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addVarAtt( NetCdfVar* netCdfVar, const Str& name, const float value )
{
    return addAtt( netCdfVar->atts_, netCdfVar->varId_, name, value );
}

const NetCdf::NetCdfAtt* NetCdfWriter::addVarAtt( NetCdfVar* netCdfVar, const Str& name, const double value )
{
    return addAtt( netCdfVar->atts_, netCdfVar->varId_, name, value );
}

void NetCdfWriter::setAttValue( const NetCdfAtt* netCdfAtt, const Str& value )
{
    char* chars = new char[netCdfAtt->nelems_];
    memset( chars, 0, netCdfAtt->nelems_ );
    strncpy( chars, value.cStr(), netCdfAtt->nelems_ );
    status_ = nc_put_att_text( ncId_, netCdfAtt->varId_, netCdfAtt->name_.cStr(),
                               netCdfAtt->nelems_, chars );

    delete[] chars;
    ok_ = status_ == NC_NOERR;
    if( !ok_ ) printf( "Error writing to %s: %s\n", netCdfAtt->name_.cStr(), status() );
}

void NetCdfWriter::setAttValue( const NetCdfAtt* netCdfAtt, const unsigned char value )
{
    status_ = nc_put_att_uchar( ncId_, netCdfAtt->varId_, netCdfAtt->name_.cStr(), ( nc_type )NC_BYTE, 1, &value );
    ok_ = status_ == NC_NOERR;
}

void NetCdfWriter::setAttValue( const NetCdfAtt* netCdfAtt, const int value )
{
    status_ = nc_put_att_int( ncId_, netCdfAtt->varId_, netCdfAtt->name_.cStr(), ( nc_type )NC_INT, 1, &value );
    ok_ = status_ == NC_NOERR;
}

void NetCdfWriter::setAttValue( const NetCdfAtt* netCdfAtt, const float value )
{
    status_ = nc_put_att_float( ncId_, netCdfAtt->varId_, netCdfAtt->name_.cStr(), ( nc_type )NC_FLOAT, 1, &value );
    ok_ = status_ == NC_NOERR;
}

void NetCdfWriter::setAttValue( const NetCdfAtt* netCdfAtt, const double value )
{
    status_ = nc_put_att_double( ncId_, netCdfAtt->varId_, netCdfAtt->name_.cStr(), ( nc_type )NC_DOUBLE, 1, &value );
    ok_ = status_ == NC_NOERR;
}

NetCdfGroup* NetCdfWriter::addGroup( const Str& name )
{
    NetCdfGroup* group = findGroup( name );
    if( NULL == group )
    {
        group = new NetCdfWriter( name.cStr(), ncId_ );
        if( NULL != group && group->isOk() )
        {
            groups_.put( name, group );
        }
        else
        {
            group = NULL;
        }
    }
    return group;
}

NetCdfGroup* NetCdfWriter::findGroup( const Str& name )
{
    return groups_.get( name );
}

bool NetCdfWriter::initialize()
{
    if( !ok_ )
    {
        return false;
    }

    if( initialized_ )
    {
        return true;
    }

    for( unsigned int i = 0; i < groups_.size() && ok_; ++i )
    {
        NetCdfGroup* group = groups_.getIndexed( i );
        ok_ &= group->initialize();
    }

    status_ = nc_enddef( ncId_ );

    ok_ = status_ == NC_NOERR;

    if( ok_ && NULL != trajectoryName_ )
    {
        status_ = nc_put_var_text( ncId_, trajectoryVar_->varId_, trajectoryName_ );
        ok_ = status_ == NC_NOERR;
    }

    initialized_ = ok_;
    return ok_;
}

bool NetCdfWriter::writeRecord( Timestamp& timestamp )
{
    unsigned int nVars = varArray_.size();
    const size_t index[] = { numRecs_};
    for( unsigned int i = 0; i < nVars; ++i )
    {
        NetCdfVar* netCdfVar = varArray_[ i ];
        if( !netCdfVar->isRecord_ )
        {
            continue;
        }
        double num;
        if( netCdfVar == timeVar_ )
        {
            num = timestamp.asDouble();
        }
        else if( NULL != netCdfVar->dataValue_ )
        {
            netCdfVar->dataValue_->copyTo( netCdfVar->dataValue_->getUnit(), num );
        }
        else
        {
            num = nan( "" );
        }
        if( NC_BYTE == netCdfVar->netCdfType_ )
        {
            num = ( int )num  & 0xFF;
        }
        else if( NC_SHORT == netCdfVar->netCdfType_ )
        {
            num = ( short )( ( int )num & 0xFFFF );
        }
        switch( netCdfVar->netCdfType_ )
        {
        default:
            status_ = nc_put_var1_double( ncId_, netCdfVar->varId_, index, &num );
            break;
        case NC_CHAR_TYPE:
        {
            unsigned char tmpChar( ( char )num );
            status_ = nc_put_var1_text( ncId_, netCdfVar->varId_, index, ( char* ) & tmpChar );
        }
        break;
        }

        ok_ = status_ == NC_NOERR;
        if( !ok_ )
        {
            Str varname( vars_.findKey( netCdfVar, Str( "unknown" ) ) );
            printf( "writeRecord #%d, var#%d (name=\"%s\", value=%s), write %g error: %s\n",
                    numRecs_, i, varname.cStr(),
                    NULL == netCdfVar->dataValue_ ? "null" : netCdfVar->dataValue_->toString().cStr(),
                    num, status() );
            return false;
        }
    }
    ++numRecs_;
    return true;
}

bool NetCdfWriter::writeVarRecord( NetCdfVar* netCdfVar, const ElementURI* uri )
{
    if( !netCdfVar->isRecord_ )
    {
        return false;
    }
    size_t index[] = { netCdfVar->numRecs_, 0, 0, 0};

    int dimensions = NULL == uri ? 0 : uri->getDimensions();
    BlobValue* bv = NULL;
    StrValue* sv = NULL;
    if( dimensions > 0 )
    {
        bv = dynamic_cast<BlobValue*>( netCdfVar->dataValue_ );
    }
    else if( netCdfVar->netCdfType_ == NetCdf::NC_STRING_TYPE )
    {
        sv = dynamic_cast<StrValue*>( netCdfVar->dataValue_ );
    }
    status_ = NC_NOERR;
    double value;
    if( NULL != bv )
    {
        int m = uri->getDimension1();
        for( int i = 0; status_ == NC_NOERR && i < m; ++i )
        {
            index[1] = i;
            if( dimensions > 1 )
            {
                int n = uri->getDimension2();
                for( int j = 0; status_ == NC_NOERR && j < n; ++j )
                {
                    index[2] = j;
                    if( dimensions > 2 )
                    {
                        int o = uri->getDimension3();
                        for( int k = 0; status_ == NC_NOERR && k < uri->getDimension3(); ++k )
                        {
                            index[3] = k;
                            bv->get( i * n * o + j * o + k, bv->getUnit(), value );
                            status_ = putValue( netCdfVar, index, value );
                        }
                    }
                    else
                    {
                        bv->get( i * n + j, bv->getUnit(), value );
                        status_ = putValue( netCdfVar, index, value );
                    }
                }
            }
            else
            {
                bv->get( i, bv->getUnit(), value );
                status_ = putValue( netCdfVar, index, value );
            }
        }
    }
    else if( NULL != sv )
    {
        Str str = sv->toString();
        const char* cstr = str.cStr();
        status_ = nc_put_var1_string( netCdfVar->ncId_, netCdfVar->varId_, index, &( cstr ) );
    }
    else
    {
        if( NULL != netCdfVar->dataValue_ )
        {
            netCdfVar->dataValue_->copyTo( netCdfVar->dataValue_->getUnit(), value );
        }
        else
        {
            value = nan( "" );
        }
        status_ = putValue( netCdfVar, index, value );
    }

    ok_ = status_ == NC_NOERR;
    if( !ok_ )
    {
        printf( "writeVarRecord #%zd, var#%d (value=%s, netCdfType=%d), write %g error: %s\n",
                index[ 0 ], netCdfVar->varId_,
                NULL == netCdfVar->dataValue_ ? "null" : netCdfVar->dataValue_->toString().cStr(),
                netCdfVar->netCdfType_, value, status() );
        return false;
    }
    ++netCdfVar->numRecs_;
    return true;
}

int NetCdfWriter::putValue( NetCdfVar* netCdfVar, size_t index[], double value )
{
    if( NC_BYTE == netCdfVar->netCdfType_ )
    {
        value = ( ( int )value ) & 0xFF;
    }
    else if( NC_SHORT == netCdfVar->netCdfType_ )
    {
        value = ( ( int )value ) & 0xFFFF;
    }

    int status = NC_NOERR;

    switch( netCdfVar->netCdfType_ )
    {
    case NC_ABSENT_TYPE:
    case NC_BYTE_TYPE:
    case NC_DOUBLE_TYPE:
    case NC_FLOAT_TYPE:
    case NC_INT_TYPE:
    case NC_SHORT_TYPE:
    default:
        status = nc_put_var1_double( netCdfVar->ncId_, netCdfVar->varId_, index, &value );
        break;
    case NC_CHAR_TYPE:
    {
        unsigned char tmpChar( ( char )value );
        status = nc_put_var1_text( netCdfVar->ncId_, netCdfVar->varId_, index, ( char* ) & tmpChar );
    }
    break;
    }
    return status;
}

bool NetCdfWriter::finalize()
{

    if( !ok_ )
    {
        return false;
    }

    if( finalized_ )
    {
        return false;
    }
    nc_sync( ncId_ );
    status_ = nc_close( ncId_ );
    ncId_ = -1;
    ok_ = status_ == NC_NOERR;


    finalized_ = ok_;
    return ok_;
}

/// Protected Constructor
NetCdfWriter::NetCdfWriter( const char* filename, bool version4, const char* trajectoryName )
    : NetCdf(),
      ok_( true ),
      filename_( filename ),
      numRecs_( 0 ),
      initialized_( false ),
      finalized_( false ),
      trajectoryName_( trajectoryName ),
      version4_( version4 ),
      trajectoryLenDim_( NULL ),
      trajectoryVar_( NULL ),
      timeDim_( NULL ),
      timeVar_( NULL ),
      currentLocation_( 0 )
{
    remove( filename );
    status_ = nc_create( filename, NC_CLOBBER | ( version4 ? NC_NETCDF4 : 0 ), &ncId_ );
    ok_ = status_ == NC_NOERR;
    if( ok_ )
    {
        if( NULL != trajectoryName )
        {
            trajectoryLenDim_ = addDim( "trajectoryNameLen", strlen( trajectoryName ) );
            trajectoryVar_ = addStaticVar( "trajectory_id", NC_CHAR_TYPE, trajectoryLenDim_ );
            addVarAtt( trajectoryVar_, "standard_name", "trajectory_id" );

            if( !version4 )
            {
                timeDim_ = addDim( "Time", 0 );
                timeVar_ = addVar( "Time", NC_DOUBLE_TYPE );
                addVarAtt( timeVar_, "standard_name", "time" );
                addVarAtt( timeVar_, "units", "seconds since 1970-01-01 00:00:00" );
                addVarAtt( timeVar_, "axis", "T" );
            }
        }
    }
    else
    {
        ncId_ = -1;
    }
}

/// Protected group Constructor
NetCdfWriter::NetCdfWriter( const char* name, int parentNcId )
    : NetCdf(),
      ok_( true ),
      filename_( Str::EMPTY_STR ),
      numRecs_( 0 ),
      initialized_( false ),
      finalized_( false ),
      trajectoryName_( NULL ),
      version4_( true ),
      trajectoryLenDim_( NULL ),
      trajectoryVar_( NULL ),
      timeDim_( NULL ),
      timeVar_( NULL ),
      currentLocation_( 0 )
{
    status_ = nc_def_grp( parentNcId, name, &ncId_ );
    ok_ = status_ == NC_NOERR;
    if( ok_ )
    {
        //timeDim_ = addDim( "Time", 0 );
    }
    else
    {
        printf( "nc_def_grp failed to define a new NetCdf group for var %s with error: ", name );
        switch( status_ )
        {
        case NC_EBADID:
            printf( "Bad group id (code %d).\n", NC_EBADID );
            break;
        case NC_ENAMEINUSE:
            printf( "Name is in use (code %d).\n", NC_ENAMEINUSE );
            break;
        case NC_EMAXNAME:
            printf( "Name exceed max length NC_MAX_NAME (code %d).\n", NC_EMAXNAME );
            break;
        case NC_EBADNAME:
            printf( "Name contains illegal characters (code %d).\n", NC_EBADNAME );
            break;
        case NC_ENOTNC4:
            printf( "Attempting a netCDF-4 operation on a netCDF-3 file (code %d).\n", NC_ENOTNC4 );
            break;
        case NC_ESTRICTNC3:
            printf( "File was created with the strict netcdf-3 flag, netcdf-4 operations are not allowed (code %d).\n", NC_ESTRICTNC3 );
            break;
        case NC_EHDFERR:
            printf( "An error was reported by the HDF5 layer (code %d).\n", NC_EHDFERR );
            break;
        case NC_EPERM:
            printf( "Attempt to write to a read-only file (code %d).\n", NC_EPERM );
            break;
        case NC_ENOTINDEFINE:
            printf( "Not in define mode (code %d).\n", NC_ENOTINDEFINE );
            break;
        default:
            break;
        }
        ncId_ = -1;
    }
}

const NetCdf::NetCdfAtt* NetCdfWriter::addAtt( NetCdfAtts& atts, int varId, const Str& name, const Str& value )
{
    NetCdfAtt* netCdfAtt = atts.get( name );
    if( NULL == netCdfAtt )
    {
        netCdfAtt = new NetCdfAtt();
        netCdfAtt->name_ = name;
        netCdfAtt->varId_ = varId;
        netCdfAtt->nelems_ = value.length();
        netCdfAtt->netCdfType_ = NC_CHAR_TYPE;
        atts.put( name, netCdfAtt );
        setAttValue( netCdfAtt, value );
    }
    return netCdfAtt;
}

const NetCdf::NetCdfAtt* NetCdfWriter::addAtt( NetCdfAtts& atts, int varId, const Str& name, const unsigned char value )
{
    NetCdfAtt* netCdfAtt = atts.get( name );
    if( NULL == netCdfAtt )
    {
        netCdfAtt = new NetCdfAtt();
        netCdfAtt->name_ = name;
        netCdfAtt->varId_ = varId;
        netCdfAtt->nelems_ = 1;
        netCdfAtt->netCdfType_ = NC_BYTE_TYPE;
        atts.put( name, netCdfAtt );
        setAttValue( netCdfAtt, value );
    }
    return netCdfAtt;
}

const NetCdf::NetCdfAtt* NetCdfWriter::addAtt( NetCdfAtts& atts, int varId, const Str& name, const int value )
{
    NetCdfAtt* netCdfAtt = atts.get( name );
    if( NULL == netCdfAtt )
    {
        netCdfAtt = new NetCdfAtt();
        netCdfAtt->name_ = name;
        netCdfAtt->varId_ = varId;
        netCdfAtt->nelems_ = 1;
        netCdfAtt->netCdfType_ = NC_INT_TYPE;
        atts.put( name, netCdfAtt );
        setAttValue( netCdfAtt, value );
    }
    return netCdfAtt;
}

const NetCdf::NetCdfAtt* NetCdfWriter::addAtt( NetCdfAtts& atts, int varId, const Str& name, const short value )
{
    NetCdfAtt* netCdfAtt = atts.get( name );
    if( NULL == netCdfAtt )
    {
        netCdfAtt = new NetCdfAtt();
        netCdfAtt->name_ = name;
        netCdfAtt->varId_ = varId;
        netCdfAtt->nelems_ = 1;
        netCdfAtt->netCdfType_ = NC_SHORT_TYPE;
        atts.put( name, netCdfAtt );
        setAttValue( netCdfAtt, value );
    }
    return netCdfAtt;
}

const NetCdf::NetCdfAtt* NetCdfWriter::addAtt( NetCdfAtts& atts, int varId, const Str& name, const float value )
{
    NetCdfAtt* netCdfAtt = atts.get( name );
    if( NULL == netCdfAtt )
    {
        netCdfAtt = new NetCdfAtt();
        netCdfAtt->name_ = name;
        netCdfAtt->varId_ = varId;
        netCdfAtt->nelems_ = 1;
        netCdfAtt->netCdfType_ = NC_FLOAT_TYPE;
        atts.put( name, netCdfAtt );
        setAttValue( netCdfAtt, value );
    }
    return netCdfAtt;
}

const NetCdf::NetCdfAtt* NetCdfWriter::addAtt( NetCdfAtts& atts, int varId, const Str& name, const double value )
{
    NetCdfAtt* netCdfAtt = atts.get( name );
    if( NULL == netCdfAtt )
    {
        netCdfAtt = new NetCdfAtt();
        netCdfAtt->name_ = name;
        netCdfAtt->varId_ = varId;
        netCdfAtt->nelems_ = 1;
        netCdfAtt->netCdfType_ = NC_DOUBLE_TYPE;
        atts.put( name, netCdfAtt );
        setAttValue( netCdfAtt, value );
    }
    return netCdfAtt;
}
