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

#include "NetCdfLogWriter.h"
#include "data/BlobValue.h"
#include "data/ElementURI.h"
#include "data/Slate.h"
#include "data/UniversalURI.h"
#include "io/OutStream.h"
#include "logger/DataEntry.h"
#include "units/Units.h"

#define MAX_CODES (ElementURI::NO_CODE + 1)

/// Streamless Constructor
NetCdfLogWriter::NetCdfLogWriter( const Str& filename, bool version4, int varCount, char** varList, char** unitList,
                                  int attCount, char** attList )
    : FilterLogWriter( NULL, varCount, varList, unitList, false, attCount, attList ),
      filename_( filename ),
      netCdfWriter_( NetCdfWriter::NewNetCdfWriter( filename.cStr(), version4 ) ),
      netCdfInitialized_( false ),
      finalized_( false ),
      vars_( new NetCdf::NetCdfVar * [ varCount + attCount ] ),
      varUris_( new const ElementURI * [ varCount + attCount ] ),
      timeVars_( new NetCdf::NetCdfVar * [ varCount + attCount ] )
{
    for( int i = 0; i < varCount_ + attCount; ++i )
    {
        vars_[i] = NULL;
        varUris_[i] = NULL;
        timeVars_[i] = NULL;
    }
    initializeNetCdfLogWriter();
}

unsigned int NetCdfLogWriter::writeHeader()
{
    return 0;
}

unsigned int NetCdfLogWriter::write( const LogEntry *entry, const Unit* unit )
{
    // No mutex lock because super is called below...
    // MutexLocker lock( writeMutex_ );

    if( !initializedFilterLogWriter_ && slateElementURICount_ != Slate::GetElementURICount() )
    {
        initializeFilterLogWriter();
    }
    if( !initializedFilterLogWriter_ )
    {
        return 0;
    }
    if( !netCdfInitialized_ )
    {
        netCdfInitialize();
        if( netCdfInitialized_ )
        {
            netCdfWriter_->initialize();
        }
    }
    if( !netCdfInitialized_ )
    {
        return 0;
    }

    return FilterLogWriter::write( entry, unit );

}

// Override this method to do something special with data entries
unsigned int NetCdfLogWriter::writeData( const DataEntry *dataEntry, int index, const Unit* unit )
{
    if( NULL != dataEntry && NULL !=  vars_[ index ] && NULL !=  vars_[ index ]->dataValue_ )
    {
        dataEntry->getDataValue()->copyTo( *( vars_[ index ]->dataValue_ ) );
        timeVars_[ index ]->dataValue_->setFrom( Units::SECOND, dataEntry->getTimestamp().asDouble() );

        netCdfWriter_->writeVarRecord( vars_[ index ], varUris_[ index ] );
        netCdfWriter_->writeVarRecord( timeVars_[ index ] );
    }
    return 0;

}

// Override this method to do something special with attribute entries
unsigned int NetCdfLogWriter::writeAtt( const DataEntry *dataEntry, int attIndex, const Unit* unit )
{
    return writeData( dataEntry, varCount_ + attIndex, unit );
}

/// Destructor
NetCdfLogWriter::~NetCdfLogWriter()
{
    if( !finalized_ )
    {
        writeFooter();
    }
    delete[] vars_;
    delete[] varUris_;
    delete[] timeVars_;
    delete netCdfWriter_;
}

unsigned int NetCdfLogWriter::writeFooter()
{
    if( wroteFooter_ )
    {
        return 0;
    }
    wroteFooter_ = true;

    if( netCdfInitialized_ )
    {
        finalized_ = netCdfWriter_->finalize();
    }
    else
    {
        printf( "No netCDF file written\n" );
        for( int i = 0; i < varCount_; ++i )
        {
            if( uriCodes_[ i ] == ElementURI::NO_CODE )
            {
                printf( "Could not find variable \"%s\"\n", varList_[ i ] );
            }
        }
        if( initializedFilterLogWriter_ )
        {
            for( int i = 0; i < varCount_; ++i )
            {
                if( NULL == vars_[i] )
                {
                    printf( "Never ran writer for variable \"%s\"\n", varList_[ i ] );
                }
            }
        }
    }
    return 0;
}

void NetCdfLogWriter::initializeNetCdfLogWriter()
{}

void NetCdfLogWriter::netCdfInitialize()
{
    netCdfInitialized_ = true;
    for( int varIndex = 0; varIndex < varCount_ + attCount_; ++varIndex )
    {
        if( vars_[ varIndex ] == NULL )
        {
            short code = varIndex < varCount_ ? uriCodes_[ varIndex ] : attUriCodes_[varIndex - varCount_];
            ElementURI* uri = Slate::GetElementURI( code );
            if( uri != NULL && uri->startsWith( "#.", 2 ) ) continue;
            for( unsigned short accessIndex = 0; accessIndex < Slate::GetDataAccessCount(); ++accessIndex )
            {
                DataAccess* dataAccess = Slate::GetDataAccess( accessIndex );
                if( NULL == dataAccess ) continue;
                if( dataAccess->getBinaryType() != NO_TYPE &&
                        ( ( varIndex >= varCount_ && dataAccess->getElementCode() == code )
                          || ( varIndex < varCount_ && !isUniversal_[ varIndex ] && dataAccess->getElementCode() == code )
                          || ( varIndex < varCount_ && isUniversal_[ varIndex ] && dataAccess->getUniversalCode() == code ) ) )
                {
                    const Unit* unit = ( varIndex >= varCount_ || units_[varIndex] == NULL ) ? dataAccess->getUnit() : units_[varIndex];
                    addVar( varIndex, uri, dataAccess->getBinaryType(), *unit, dataAccess->getUniversalCode() );
                    break;
                }
            }
            if( NULL == vars_[varIndex] )
            {
                if( varIndex < varCount_ && NULL != units_[varIndex] )
                {
                    ElementURI aUri = ElementURI( "", varList_[ varIndex ] );
                    size_t dotAt = aUri.findLastOf( '.' );
                    const char* universalChars = varList_[ varIndex ];
                    if( dotAt != Str::NO_POS )
                    {
                        universalChars += dotAt + 1;
                    }
                    unsigned short universalCode = Slate::GetElementURICode( universalChars );
                    addVar( varIndex, &aUri, units_[varIndex]->getDefaultDataType(),
                            *units_[varIndex], universalCode );
                }
                else if( !Slate::IsDirectoryRead() )
                {
                    netCdfInitialized_ = false;
                }
                else
                {
                    printf( "Not found: %hd %s\n", code, uri == NULL ? "?" : uri->cStr() );
                }
            }
        }
    }
    if( netCdfInitialized_ )
    {
        netCdfWriter_->addGlobalAtt( "Conventions", "CF-1.4" );
        netCdfWriter_->addGlobalAtt( "CF:featureType", "trajectory" );
    }
}

void NetCdfLogWriter::addVar( const int i, const ElementURI* uri, BinaryDataType binaryType, const Unit& unit, short universalCode )
{
    Str fixedUri( *uri );
    fixedUri.replaceChar( ':', '.' );
    fixedUri.replaceChar( '/', '.' );

    NetCdfGroup* group = netCdfWriter_;

    size_t dotAt;
    Str fullFixedUri = fixedUri;
    while( ( dotAt = fixedUri.find( '.' ) ) != Str::NO_POS )
    {
        Str groupName = fixedUri.substr( 0, dotAt );
        // addGroup will return an existing sub-group if available
        group = netCdfWriter_->addGroup( groupName );
        if( NULL == group || !group->isOk() )
        {
            printf( "NetCdfLogWriter failed to add group %s while processing %s (index %d).\n", groupName.cStr(), fullFixedUri.cStr(), i );
            return;
        }

        fixedUri = fixedUri.substr( dotAt + 1 );
    }
    //group = netCdfWriter_->addGroup( fixedUri, false );

    NetCdf::NetCdfType ncType = NetCdf::NC_ABSENT_TYPE;
    switch( binaryType )
    {
    case UCHAR1:
        ncType = NetCdf::NC_UBYTE_TYPE;
        break;
    case SHORT2:
        ncType = NetCdf::NC_SHORT_TYPE;
        break;
    case FLOAT2:
    case FLOAT3:
    case FLOAT4:
    case DOUBLE4:
        ncType = NetCdf::NC_FLOAT_TYPE;
        break;
    case USHORT2:
        ncType = NetCdf::NC_USHORT_TYPE;
        break;
    case INT4:
        ncType = NetCdf::NC_INT_TYPE;
        break;
    case NO_TYPE:
    case DOUBLE6:
    case DOUBLE8:
        ncType = NetCdf::NC_DOUBLE_TYPE;
        break;
    case MULTIVALUE:
        if( uri->getBlobType() == NOT_BLOB )
        {
            ncType = NetCdf::NC_STRING_TYPE;
        }
        else
        {
            switch( uri->getBlobType() )
            {
            case BLOB_INT8:
                ncType = NetCdf::NC_BYTE_TYPE;
                break;
            case BLOB_INT16LE:
            case BLOB_INT16BE:
                ncType = NetCdf::NC_SHORT_TYPE;
                break;
            case BLOB_INT32LE:
            case BLOB_INT32BE:
                ncType = NetCdf::NC_INT_TYPE;
                break;
            case BLOB_INT64LE:
            case BLOB_INT64BE:
                ncType = NetCdf::NC_INT64_TYPE;
                break;
            case BLOB_UINT8:
                ncType = NetCdf::NC_UBYTE_TYPE;
                break;
            case BLOB_UINT16LE:
            case BLOB_UINT16BE:
                ncType = NetCdf::NC_USHORT_TYPE;
                break;
            case BLOB_UINT32LE:
            case BLOB_UINT32BE:
                ncType = NetCdf::NC_UINT_TYPE;
                break;
            case BLOB_UINT64LE:
            case BLOB_UINT64BE:
                ncType = NetCdf::NC_UINT64_TYPE;
                break;
            case BLOB_FLOAT32LE:
            case BLOB_FLOAT32BE:
                ncType = NetCdf::NC_FLOAT_TYPE;
                break;
            case BLOB_FLOAT64LE:
            case BLOB_FLOAT64BE:
                ncType = NetCdf::NC_DOUBLE_TYPE;
                break;
            default:
                printf( "Unable to handle Blob type %d for %s\n", uri->getBlobType(), uri->cStr() );
                break;
            }
        }
        break;
    default:
        printf( "Unable to handle Binary type %d for %s\n", binaryType, uri->cStr() );
        break;
    }

    if( ncType == NetCdf::NC_ABSENT_TYPE )
    {
        return;
    }

    if( NULL != group->findVar( fixedUri ) )
    {
        return;
    }

    Str timeName = fixedUri + "_time";

    const NetCdf::NetCdfDim* timeDim = group->addDim( timeName, 0 );
    const NetCdf::NetCdfDim* dimM = uri->getDimension1() == 0 ? NULL : group->addDim( fixedUri + "_M", uri->getDimension1() );
    const NetCdf::NetCdfDim* dimN = uri->getDimension2() == 0 ? NULL : group->addDim( fixedUri + "_N", uri->getDimension2() );
    const NetCdf::NetCdfDim* dimO = uri->getDimension3() == 0 ? NULL : group->addDim( fixedUri + "_O", uri->getDimension3() );

    NetCdf::NetCdfVar* tmpVar = group->addVar( fixedUri, ncType, timeDim, dimM, dimN, dimO );

    if( NULL == tmpVar )
    {
        printf( "Failed to add variable: %s (index %d).\n", fullFixedUri.cStr(), i );
        return;
    }

    vars_[ i ] = tmpVar;
    varUris_[ i ] = uri;
    if( binaryType == MULTIVALUE )
    {
        if( uri->getBlobType() == NOT_BLOB )
        {
            vars_[ i ]->dataValue_ = new StrValue();
        }
        else
        {
            vars_[ i ]->dataValue_ = new BlobValue( *uri );
        }
    }
    else
    {
        vars_[ i ]->dataValue_ = unit( binaryType );
    }

    group->addVarAtt( vars_[ i ], "units", unit.getAbbreviation() );
    Str standardName( Str::EMPTY_STR );
    if( universalCode != UniversalURI::NO_CODE )
    {
        const ElementURI* universal;
        if( isUniversal_[ i ] )
        {
            universal = uri;
        }
        else
        {
            universal = Slate::GetElementURI( universalCode );
        }
        standardName = *universal;
        group->addVarAtt( vars_[ i ], "standard_name", standardName );
    }
    if( standardName != Str::EMPTY_STR
            && standardName != UniversalURI::DEPTH
            && standardName != UniversalURI::LATITUDE
            && standardName != UniversalURI::LONGITUDE )
    {
        group->addVarAtt( vars_[ i ], "coordinates", "time depth latitude longitude" );
    }
    else if( standardName != Str::EMPTY_STR )
    {
        group->addVarAtt( vars_[ i ], "long_name", varList_[i] );
    }

    timeVars_[ i ] = group->addVar( timeName, NetCdf::NC_DOUBLE_TYPE, timeDim );
    timeVars_[ i ]->dataValue_ = Units::SECOND( DOUBLE8 );
    group->addVarAtt( timeVars_[ i ], "units", "seconds since 1970-01-01 00:00:00Z" );
}
