/* dag.cc
****************************************************************************************************************
****************************************************************************************************************

    Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2020, 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 <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <netinet/in.h>

#include "dag.h"
#include "endian64.h"
#include "erspan.h"

#if defined( HAVE_LIBDAG )
#include <dagapi.h>
#endif

using namespace trafficmeter;

using std::invalid_argument;
using std::logic_error;
using std::runtime_error;
using std::string;
using std::to_string;

constexpr const uint32_t    WAIT_FOR_N_BYTES_ = 8 << 20;    // 8 MiB

/**************************************************************************************************************/
Dag::Dag( const string& name ) :
    Source( name ),
    fd_( -1 ),
    stream_( 0 ),
    record_( nullptr ),
    recordsEnd_( nullptr )
{
    string::size_type   tokenpos = 0;
    string::size_type   tokenend = name_.find_first_of( ':' );

    device_ = "/dev/" + name_.substr( tokenpos, tokenend );
    tokenpos = tokenend + 1;

    if ( tokenend != string::npos )
    {
        if
        (
            tokenpos < name_.size() &&
            name_.find_first_not_of( "0123456789", tokenpos ) == string::npos
        )
        {
            stream_ = stoi( name_.substr(tokenpos) );
        }
        else
        {
            throw invalid_argument( "stream is invalid" );
        }
    }

#if defined( HAVE_LIBDAG )

    fd_ = dag_open( const_cast<char*>(device_.c_str()) );

    if ( fd_ < 0 )
    {
        throw runtime_error( "device is invalid or unavailable" );
    }
    else if ( dag_attach_stream(fd_, stream_, 0, 0) )
    {
        dag_close( fd_ );
        throw runtime_error( "stream is busy or unavailable" );
    }

    struct timeval  timeout = { .tv_sec = 0, .tv_usec = SOURCE_TIMEOUT * 1000 };

    if ( dag_set_stream_poll(fd_, stream_, WAIT_FOR_N_BYTES_, &timeout, &timeout) )
    {
        dag_detach_stream( fd_, stream_ );
        dag_close( fd_ );
        throw runtime_error( "cannot change poll parameters" );
    }

    info_ =
    (
        device_ + " " +
        "stream=" + to_string( stream_ ) + " " +
        ( shouldCheckErspan_? "erspan " : "" ) +
        "buf=" + to_string( dag_get_stream_buffer_size(fd_, stream_) >> 20 ) + "MiB " +
        __FILE__
    );

#else

    throw logic_error( "there is no libdag" );

#endif
}

/**************************************************************************************************************/
Dag::~Dag()
{
#if defined( HAVE_LIBDAG )

    if ( fd_ >= 0 )
    {
        dag_detach_stream( fd_, stream_ );
        dag_close( fd_ );
    }

#endif
}

/**************************************************************************************************************/
void
Dag::open()
{
#if defined( HAVE_LIBDAG )

    if ( dag_start_stream(fd_, stream_) != 0 )
    {
        dag_detach_stream( fd_, stream_ );
        dag_close( fd_ );
        throw runtime_error( "cannot start DAG stream: " + name_ + " - " + strerror(errno) );
    }

#endif
}

/**************************************************************************************************************/
void
Dag::close()
{
#if defined( HAVE_LIBDAG )

    if ( dag_stop_stream(fd_, stream_) != 0 )
    {
        dag_detach_stream( fd_, stream_ );
        dag_close( fd_ );
        throw runtime_error( "cannot stop DAG stream: " + name_ + " - " + strerror(errno) );
    }

#endif
}

/**************************************************************************************************************/
bool
Dag::checkForRecords()
{
#if defined( HAVE_LIBDAG )

    if
    (
        recordsEnd_ != nullptr &&
        record_ + dag_record_size < recordsEnd_ &&
        record_ + ntohs( reinterpret_cast<dag_record_t*>(record_)->rlen ) <= recordsEnd_
    )
    {
        return true;
    }

    recordsEnd_ = dag_advance_stream( fd_, stream_, &record_ );

#endif

    return recordsEnd_ != nullptr && record_ < recordsEnd_;
}

/**************************************************************************************************************/
bool
Dag::copyRecords( uint8_t** destination, const uint8_t* destinationEnd )
{
    unsigned    nBytesToCopy = 0;
    unsigned    nRecordsToCopy = 0;
    unsigned    nDropped = 0;
    uint8_t*    record = record_;
    bool        hasRecordsLeft = false;

#if defined( HAVE_LIBDAG )

    while ( recordsEnd_ != nullptr && record + dag_record_size < recordsEnd_ )
    {
        dag_record_t*   header = reinterpret_cast<dag_record_t*>( record );
        unsigned        recordSize = ntohs( header->rlen );

        if ( record + recordSize > recordsEnd_ )
        {
            break;
        }

        if ( (header->type & 0x7f) == TYPE_ETH )
        {
            nDropped += ntohs( header->lctr );
        }

        if ( *destination + nBytesToCopy + recordSize > destinationEnd )
        {
            hasRecordsLeft = true;
            break;
        }

        nBytesToCopy += recordSize;
        ++nRecordsToCopy;
        record += recordSize;
    }

    memcpy( *destination, record_, nBytesToCopy );
    nReceived_a_ += nRecordsToCopy;
    nDropped_a_ += nDropped;
    record_ = record;
    *destination += nBytesToCopy;

#endif

    return hasRecordsLeft;
}

/**************************************************************************************************************/
Source::Packet
Dag::getPacket()
{
    Source::Packet  packet = { Timestamp(0), nullptr, 0 };

#if defined( HAVE_LIBDAG )

    dag_record_t*   header = reinterpret_cast<dag_record_t*>( record_ );
    unsigned        recordSize = ntohs( header->rlen );
    unsigned        erfType = header->type & 0x7f;
    bool            hasExtension = header->type & 0x80;

    if
    (
        !header->flags.rxerror &&
        !header->flags.dserror &&
        ( erfType == TYPE_ETH || erfType == TYPE_DSM_COLOR_ETH )
    )
    {
        packet.timestamp = Timestamp( le64toh(header->ts) );
        packet.data2 = reinterpret_cast<uint8_t*>( &header->rec );
        packet.nBytes2 = ntohs( header->wlen );

        if ( hasExtension )
        {
            while ( *packet.data2 & 0x80 )
            {
                packet.data2 += 8;
            }
        }

        packet.data2 += 2;

        if ( shouldCheckErspan_ )
        {
            Erspan  erspan( packet.data2, packet.nBytes2 );

            packet.data2 = erspan.data2();
            packet.nBytes2 = erspan.nBytes2();
        }

        if ( erfType == TYPE_ETH )
        {
            nDropped_a_ += ntohs( header->lctr );
        }

        ++nReceived_a_;
    }

    record_ += recordSize;

#endif

    return packet;
}

/**************************************************************************************************************/
Source::Packet
Dag::getPacketFromCopy( uint8_t** record ) const
{
    Source::Packet  packet = { Timestamp(0), nullptr, 0 };

#if defined( HAVE_LIBDAG )

    dag_record_t*   header = reinterpret_cast<dag_record_t*>( *record );
    unsigned        recordSize = ntohs( header->rlen );
    unsigned        erfType = header->type & 0x7f;
    bool            hasExtension = header->type & 0x80;

    if
    (
        !header->flags.rxerror &&
        !header->flags.dserror &&
        ( erfType == TYPE_ETH || erfType == TYPE_DSM_COLOR_ETH )
    )
    {
        packet.timestamp = Timestamp( le64toh(header->ts) );
        packet.data2 = reinterpret_cast<uint8_t*>( &header->rec );
        packet.nBytes2 = ntohs( header->wlen );

        if ( hasExtension )
        {
            while ( *packet.data2 & 0x80 )
            {
                packet.data2 += 8;
            }
        }

        packet.data2 += 2;

        if ( shouldCheckErspan_ )
        {
            Erspan  erspan( packet.data2, packet.nBytes2 );

            packet.data2 = erspan.data2();
            packet.nBytes2 = erspan.nBytes2();
        }
    }

    *record += recordSize;

#endif

    return packet;
}

/**************************************************************************************************************/
bool
Dag::isEthernet() const
{
#if defined( HAVE_LIBDAG )

    uint8_t     erfs[ 255 ];
    int         erfcount = dag_get_stream_erf_types( fd_, stream_, erfs, 255 );

    for ( int i = 0; i < erfcount; i++ )
    {
        switch ( erfs[i] )
        {
            case TYPE_ETH:
            case TYPE_DSM_COLOR_ETH:
                return true;
        }
    }

#endif

    return false;
}
