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

#include "DataReceiver.h"

#include "supervisor/CommandExec.h"

#include "../Lib/aes/AESUtil.h"
#include "../Lib/zlib/zutil.h"
#include <stdint.h>
#include <stdlib.h>

#define BLOCKLEN 16
#define BLOCKMODE AES::CBC

AES DataReceiver::Crypt_;

bool DataReceiver::ReceiveSbd( const char* data, size_t dataLen, const char* keyText, Logger& logger )
{
    Str errorMsg = DataReceiver::TryReceiveSbd( data, dataLen, keyText, logger );
    if( errorMsg != Str::EMPTY_STR )
    {
        logger.syslog( errorMsg, Syslog::CRITICAL );
    }
    return errorMsg == Str::EMPTY_STR;
}

Str DataReceiver::TryReceiveSbd( const char* data, size_t dataLen, const char* keyText, Logger& logger )
{

    // Check the length
    if( ( dataLen % BLOCKLEN ) != 4 )
    {
        return Str( "Packet size is not N*16+4: 0x" ) + dataLen;
    }

    // Decrypt

    // Read the crc
    unsigned long crc( 0 );
    for( int i = 0; i < 4; ++i )
    {
        crc <<= 8;
        crc |= ( int )( unsigned char )data[dataLen - 1];
        dataLen--;
    }

    // Decrypt
    unsigned char key[32];
    unsigned char decrypted[256] = {0};
    int keyLen = makeKey( keyText, key );
    uint32_t blocks = ( dataLen + BLOCKLEN - 1 ) / BLOCKLEN;
    Crypt_.SetParameters( keyLen * 8, BLOCKLEN * 8 );
    Crypt_.StartDecryption( key );
    Crypt_.Decrypt( ( unsigned char* )data, decrypted, blocks, BLOCKMODE );

    // Check the crc
    unsigned long crcChk = crc32( 0, ( unsigned char* )decrypted, dataLen );
    if( crcChk != crc )
    {
        return Str( "CRC Error 0x" ) + Str( ( int )crc, 16 ) + " != computed 0x" + Str( ( int )crcChk, 16 );
    }

    Receive( ( const char* )decrypted, dataLen, logger );

    return Str::EMPTY_STR;
}

void DataReceiver::Receive( const char* data, size_t dataLen, Logger& logger )
{

    unsigned int flags = ( unsigned int )( unsigned char )data[0];

    if( ( flags & FILE_FLAG ) == 0 )
    {
        logger.syslog( "Received command: ", data, Syslog::INFO );
        CommandExec::DoCommand( data, false );
        return;
    }

    unsigned int stamp = ( ( unsigned int )( unsigned char )data[1] )
                         + ( ( ( unsigned int )( unsigned char )data[2] ) << 8 )
                         + ( ( ( unsigned int )( unsigned char )data[3] ) << 16 )
                         + ( ( ( unsigned int )( unsigned char )data[4] ) << 24 );

    char infoFilename[] = "Logs/latest/FFFFFFFF.info"; // Contains the info (flags, stamp, size, path)
    snprintf( infoFilename, sizeof( infoFilename ), "Logs/latest/%08X.info", stamp );

    char partFilename[] = "Logs/latest/FFFFFFFF.part"; // Contains the data. Assume no streams of zeroes
    snprintf( partFilename, sizeof( partFilename ), "Logs/latest/%08X.part", stamp );

    char streamCmd[MAX_STREAM_CMD] = {0};
    char targetPath[MAX_TARGET] = {0};
    unsigned int crc = 0;
    size_t size = 0;

    if( ( data[0] & FILE_PART ) == 0 )
    {
        FileStart( data, dataLen, flags, stamp, infoFilename, partFilename, crc, size, streamCmd, targetPath, logger );
    }
    else
    {
        FilePart( data, dataLen, flags, stamp, infoFilename, partFilename, crc, size, streamCmd, targetPath, logger );
    }

    if( *streamCmd == 0 )
    {
        return;
    }
    if( !FileDone( partFilename, crc, size, targetPath, logger ) )
    {
        return;
    }

    char commandFmt[MAX_STREAM_CMD + 5 + sizeof( partFilename )];
    snprintf( commandFmt, sizeof( commandFmt ), "cat %s %s", partFilename, streamCmd );

    char* stringAt = commandFmt;
    int sCount = 0;
    while( NULL != ( stringAt = strstr( stringAt, "%s" ) ) )
    {
        ++sCount;
        ++stringAt;
    }

    char command[sizeof( commandFmt ) + MAX_TARGET * 3 ];
    switch( sCount )
    {
    case 0:
        break;
    case 1:
        snprintf( command, sizeof( command ), commandFmt, targetPath );
        break;
    case 2:
        snprintf( command, sizeof( command ), commandFmt, targetPath, targetPath );
        break;
    case 3:
        snprintf( command, sizeof( command ), commandFmt, targetPath, targetPath, targetPath );
        break;
    default:
        logger.syslog( "Too many refecences to target in stream cmd: ", streamCmd, Syslog::FAULT );
        return;
    }

    ParsedCommand parsedCommand;
    Str commandStr( command );
    parsedCommand.setStringArg( commandStr );
    logger.syslog( "Executing " + Str( command ), Syslog::IMPORTANT );
    CommandExec::CommandBang( &parsedCommand, 0 );

    Str md5sumCmd( "md5sum " );
    md5sumCmd += targetPath;
    parsedCommand.setStringArg( md5sumCmd );
    CommandExec::CommandBang( &parsedCommand, 0 );

}

/// Write data to start of file, and set informational parameters
/// Data contains stamp, size, path, data
/// Sets streamCmd
void DataReceiver::FileStart( const char* data, size_t dataLen,
                              unsigned int flags, unsigned int stamp,
                              char* infoFilename, char* partFilename,
                              unsigned int& crc, size_t& size,
                              char* streamCmd, char* targetPath,
                              Logger& logger )
{
    FILE* infoFile = fopen( infoFilename, "wb" );
    fwrite( data, 1, 9, infoFile );
    crc = ( ( unsigned int )( unsigned char )data[5] )
          + ( ( ( unsigned int )( unsigned char )data[6] ) << 8 )
          + ( ( ( unsigned int )( unsigned char )data[7] ) << 16 )
          + ( ( ( unsigned int )( unsigned char )data[8] ) << 24 );
    size = ( size_t )( unsigned char )data[9]
           + ( ( size_t )( unsigned char )data[10] << 8 );
    unsigned int pos = 11;
    bool largeFile = flags & FILE_LARGE_OFFSET;
    bool mediumFile = flags & FILE_MEDIUM_OFFSET;
    if( mediumFile | largeFile )
    {
        size += ( size_t )( unsigned char )data[pos++] << 16;
    }
    if( largeFile )
    {
        size += ( size_t )( unsigned char )data[pos++] << 24;
    }
    unsigned char foo = size & 0xFF;
    fwrite( &foo, 1, 1, infoFile );
    foo = ( size >> 8 ) & 0xFF;
    fwrite( &foo, 1, 1, infoFile );
    foo = ( size >> 16 ) & 0xFF;
    fwrite( &foo, 1, 1, infoFile );
    foo = ( size >> 24 );
    fwrite( &foo, 1, 1, infoFile );
    int streamCmdPos = 0;
    for( ; pos < dataLen; ++pos )
    {
        streamCmd[streamCmdPos++] = data[pos];
        fwrite( data + pos, 1, 1, infoFile );
        if( data[pos] == 0 ) break;
    }
    ++pos;
    int targetPos = 0;
    for( ; pos < dataLen; ++pos )
    {
        targetPath[targetPos++] = data[pos];
        fwrite( data + pos, 1, 1, infoFile );
        if( data[pos] == 0 ) break;
    }
    ++pos;
    fclose( infoFile );

    FILE* partFile = fopen( partFilename, "r+b" );
    if( NULL == partFile )
    {
        partFile = fopen( partFilename, "w+b" );
    }
    logger.syslog( "Initialized file: ", targetPath, Syslog::IMPORTANT );
    size_t bytesToWrite = dataLen - pos;
    if( bytesToWrite > size )
    {
        bytesToWrite = size;
    }
    fwrite( data + pos, 1, bytesToWrite, partFile );
    fclose( partFile );
}

/// Write data to middle of file
/// Data contains stamp, offset, data
/// Sets streamCmd if possible
/// Returns true if file CAN BE COMPLETE
void DataReceiver::FilePart( const char* data, size_t dataLen,
                             unsigned int flags, unsigned int stamp,
                             char* infoFilename, char* partFilename,
                             unsigned int& crc, size_t& size,
                             char* streamCmd, char* targetPath,
                             Logger& logger )
{
    FILE* infoFile = fopen( infoFilename, "r+b" );
    if( NULL == infoFile )
    {
        infoFile = fopen( infoFilename, "w+b" );
    }
    bool ok = true;
    ok &= 0 != fwrite( data, 1, 5, infoFile );
    ok &= 0 == fseek( infoFile, 0, SEEK_END );
    int infoEndPos = ftell( infoFile );
    if( infoEndPos > 9 )
    {
        ok &= 0 == fseek( infoFile, 5, SEEK_SET );
        ok &= 0 != fread( &crc, 4, 1, infoFile );
        ok &= 0 != fread( &size, 4, 1, infoFile );
        int streamCmdPos = 0;
        while( ~feof( infoFile ) )
        {
            ok &= 0 != fread( streamCmd + streamCmdPos, 1, 1, infoFile );
            if( streamCmd[streamCmdPos] == 0 ) break;
            ++streamCmdPos;
        }
        int targetPathPos = 0;
        while( ~feof( infoFile ) )
        {
            ok &= 0 != fread( targetPath + targetPathPos, 1, 1, infoFile );
            if( targetPath[targetPathPos] == 0 ) break;
            ++targetPathPos;
        }
    }
    ok &= 0 == fclose( infoFile );

    FILE* partFile = fopen( partFilename, "r+b" );
    if( NULL == partFile )
    {
        partFile = fopen( partFilename, "w+b" );
    }
    size_t offset = ( size_t )( unsigned char )data[5]
                    + ( ( size_t )( unsigned char )data[6] << 8 );
    bool largeFile = flags & FILE_LARGE_OFFSET;
    bool mediumFile = flags & FILE_MEDIUM_OFFSET;
    unsigned int pos = 7;
    if( mediumFile || largeFile )
    {
        offset += ( size_t )( unsigned char )data[pos++] << 16;
    }
    if( largeFile )
    {
        offset += ( size_t )( unsigned char )data[pos++] << 24;
    }

    fseek( partFile, offset, SEEK_SET );
    size_t bytesToWrite( 0 );
    if( dataLen > pos )
    {
        bytesToWrite = dataLen - pos;
    }
    if( size > offset && bytesToWrite > size - offset )
    {
        bytesToWrite = size - offset;
    }

    fwrite( data + pos, 1, bytesToWrite, partFile );
    logger.syslog( "Added data to file: ", targetPath, Syslog::IMPORTANT );
    fclose( partFile );
}

/// Returns true if file is completely written and there are no long
/// strings of zeroes in the data
bool DataReceiver::FileDone( char* partFilename, unsigned int crc,
                             size_t size, char* targetPath,
                             Logger& logger )
{
    FILE* partFile = fopen( partFilename, "rb" );
    fseek( partFile, 0, SEEK_END );
    size_t endLoc = ftell( partFile );
    if( endLoc + 1 < size )
    {
        logger.syslog( "More data left to go, at position ", endLoc, Syslog::IMPORTANT );
        fclose( partFile );
        return false;
    }
    fseek( partFile, 0, SEEK_SET );
    const size_t bufSize( 1024 );
    char buffer[bufSize];
    int zeroes = 0;
    size_t totalBytesRead( 0 );
    unsigned long crcChk( 0 );
    while( !feof( partFile ) )
    {
        size_t bytesRead = fread( buffer, 1, bufSize, partFile );
        size_t bytesExamined( 0 );
        for( size_t i = 0; i < bytesRead && totalBytesRead < size; ++i )
        {
            if( 0 == buffer[i] )
            {
                ++zeroes;
                if( zeroes > MAX_ZEROES )
                {
                    logger.syslog( "Missing data starting at position ",
                                   totalBytesRead + i - zeroes + 1, Syslog::IMPORTANT );
                    fclose( partFile );
                    return false;
                }
            }
            else
            {
                zeroes = 0;
            }
            ++totalBytesRead;
            ++bytesExamined;
        }
        crcChk = crc32( crcChk, ( unsigned char* )buffer, bytesExamined );
    }
    fclose( partFile );

    if( crc != crcChk )
    {
        logger.syslog( Str( "CRC Error 0x" ) + Str( ( int )crc, 16 )
                       + " != computed 0x" + Str( ( int )crcChk, 16 ), Syslog::FAULT );
        return false;
    }

    return true;
}
