/* pcapwriting.cc
****************************************************************************************************************
****************************************************************************************************************

    Copyright (C) 2021 Askar Almatov

    This file is part of Trafficmeter.

    Trafficmeter is free software: you can redistribute it and/or modify it under the terms of the GNU General
    Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
    option) any later version.

    Trafficmeter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
    implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
    License for more details.

    You should have received a copy of the GNU General Public License along with Trafficmeter. If not, see
    <http://www.gnu.org/licenses/>.
*/

#include <cstring>
#include <stdexcept>
#include <fcntl.h>
#include <unistd.h>

#include "pcapwriting.h"

using namespace trafficmeter;

using std::runtime_error;

constexpr const uint32_t    MAGIC_          = 0xa1b2c3d4;
constexpr const uint16_t    VERSION_MAJOR_  = 2;
constexpr const uint16_t    VERSION_MINOR_  = 4;
constexpr const unsigned    BUFFER_SIZE_    = 8U << 20;     // 8 MiB

struct FileHeader_
{
    uint32_t    magic;
    uint16_t    versionMajor;
    uint16_t    versionMinor;
    uint32_t    reserved1;
    uint32_t    reserved2;
    uint32_t    snapLen;
    uint32_t    linkType;
};

struct PacketHeader_
{
    uint32_t    sec;
    uint32_t    usec;
    uint32_t    capLen;
    uint32_t    len;
};

/**************************************************************************************************************/
PcapWriting::PcapWriting( const Path& outputFile )
{
    bool    shouldAppend = false;

    if ( outputFile == "STDOUT" )
    {
        fd_ = STDOUT_FILENO;
    }
    else if ( outputFile == "STDERR" )
    {
        fd_ = STDERR_FILENO;
    }
    else
    {
        fd_ = open( outputFile.c_str(), O_RDWR | O_CREAT, 0644 );

        if ( fd_ == -1 )
        {
            throw runtime_error( "cannot open: " + outputFile );
        }

        FileHeader_     oldHeader;

        if
        (
            read( fd_, &oldHeader, sizeof(oldHeader) ) == sizeof( oldHeader ) &&
            oldHeader.magic == MAGIC_ &&
            oldHeader.versionMajor == VERSION_MAJOR_ &&
            oldHeader.versionMinor == VERSION_MINOR_
        )
        {
            shouldAppend = true;
        }

        if ( lseek(fd_, 0, SEEK_END) > 0 && !shouldAppend )
        {
            close( fd_ );
            fd_ = -1;
            throw runtime_error( "incompatible pcap format: " + outputFile );
        }
    }

    buffer_ = cursor_ = new uint8_t[ BUFFER_SIZE_ ];

    if ( !shouldAppend )
    {
        static const FileHeader_    newHeader = { MAGIC_, VERSION_MAJOR_, VERSION_MINOR_, 0, 0, 65535, 1 };

        memcpy( cursor_, &newHeader, sizeof(newHeader) );
        cursor_ += sizeof( newHeader );
    }
}

/**************************************************************************************************************/
PcapWriting::~PcapWriting()
{
    flush();
    delete buffer_;

    if ( fd_ >= 0 && !isConsole() )
    {
        close( fd_ );
    }
}

/**************************************************************************************************************/
void
PcapWriting::writePacket( Timestamp timestamp, const uint8_t* data2, unsigned nBytes2 )
{
    if ( cursor_ + sizeof(PacketHeader_) + nBytes2 > buffer_ + BUFFER_SIZE_ )
    {
        flush();
    }

    PacketHeader_*  header = reinterpret_cast<PacketHeader_*>( cursor_ );
        
    header->sec = timestamp.sec();
    header->usec = timestamp.usec();
    header->capLen = header->len = nBytes2;
    memcpy( header + 1, data2, nBytes2 );
    cursor_ += sizeof( PacketHeader_ ) + nBytes2;
}

/**************************************************************************************************************/
void
PcapWriting::flush()
{
    if ( fd_ >= 0 )
    {
        if ( write(fd_, buffer_, cursor_ - buffer_) == -1 )
        {
            throw runtime_error( "pcap file write error" );
        }
    }

    cursor_ = buffer_;
}

/**************************************************************************************************************/
bool
PcapWriting::isConsole() const
{
    return fd_ == STDOUT_FILENO || fd_ == STDERR_FILENO;
}
