/*
 * Copyright (c) 2007, 2014 University of Washington
 *               2015 Universita' degli Studi di Napoli Federico II
 *
 * SPDX-License-Identifier: GPL-2.0-only
 */

#include "queue-disc.h"

#include "ns3/abort.h"
#include "ns3/log.h"
#include "ns3/net-device-queue-interface.h"
#include "ns3/object-vector.h"
#include "ns3/packet.h"
#include "ns3/pointer.h"
#include "ns3/queue.h"
#include "ns3/simulator.h"
#include "ns3/socket.h"
#include "ns3/uinteger.h"

namespace ns3
{

NS_LOG_COMPONENT_DEFINE("QueueDisc");

NS_OBJECT_ENSURE_REGISTERED(QueueDiscClass);

TypeId
QueueDiscClass::GetTypeId()
{
    static TypeId tid = TypeId("ns3::QueueDiscClass")
                            .SetParent<Object>()
                            .SetGroupName("TrafficControl")
                            .AddConstructor<QueueDiscClass>()
                            .AddAttribute("QueueDisc",
                                          "The queue disc attached to the class",
                                          PointerValue(),
                                          MakePointerAccessor(&QueueDiscClass::m_queueDisc),
                                          MakePointerChecker<QueueDisc>());
    return tid;
}

QueueDiscClass::QueueDiscClass()
{
    NS_LOG_FUNCTION(this);
}

QueueDiscClass::~QueueDiscClass()
{
    NS_LOG_FUNCTION(this);
}

void
QueueDiscClass::DoDispose()
{
    NS_LOG_FUNCTION(this);
    m_queueDisc = nullptr;
    Object::DoDispose();
}

Ptr<QueueDisc>
QueueDiscClass::GetQueueDisc() const
{
    NS_LOG_FUNCTION(this);
    return m_queueDisc;
}

void
QueueDiscClass::SetQueueDisc(Ptr<QueueDisc> qd)
{
    NS_LOG_FUNCTION(this);
    NS_ABORT_MSG_IF(m_queueDisc,
                    "Cannot set the queue disc on a class already having an attached queue disc");
    m_queueDisc = qd;
}

QueueDisc::Stats::Stats()
    : nTotalReceivedPackets(0),
      nTotalReceivedBytes(0),
      nTotalSentPackets(0),
      nTotalSentBytes(0),
      nTotalEnqueuedPackets(0),
      nTotalEnqueuedBytes(0),
      nTotalDequeuedPackets(0),
      nTotalDequeuedBytes(0),
      nTotalDroppedPackets(0),
      nTotalDroppedPacketsBeforeEnqueue(0),
      nTotalDroppedPacketsAfterDequeue(0),
      nTotalDroppedBytes(0),
      nTotalDroppedBytesBeforeEnqueue(0),
      nTotalDroppedBytesAfterDequeue(0),
      nTotalRequeuedPackets(0),
      nTotalRequeuedBytes(0),
      nTotalMarkedPackets(0),
      nTotalMarkedBytes(0)
{
}

uint32_t
QueueDisc::Stats::GetNDroppedPackets(std::string reason) const
{
    uint32_t count = 0;
    auto it = nDroppedPacketsBeforeEnqueue.find(reason);

    if (it != nDroppedPacketsBeforeEnqueue.end())
    {
        count += it->second;
    }

    it = nDroppedPacketsAfterDequeue.find(reason);

    if (it != nDroppedPacketsAfterDequeue.end())
    {
        count += it->second;
    }

    return count;
}

uint64_t
QueueDisc::Stats::GetNDroppedBytes(std::string reason) const
{
    uint64_t count = 0;
    auto it = nDroppedBytesBeforeEnqueue.find(reason);

    if (it != nDroppedBytesBeforeEnqueue.end())
    {
        count += it->second;
    }

    it = nDroppedBytesAfterDequeue.find(reason);

    if (it != nDroppedBytesAfterDequeue.end())
    {
        count += it->second;
    }

    return count;
}

uint32_t
QueueDisc::Stats::GetNMarkedPackets(std::string reason) const
{
    auto it = nMarkedPackets.find(reason);

    if (it != nMarkedPackets.end())
    {
        return it->second;
    }

    return 0;
}

uint64_t
QueueDisc::Stats::GetNMarkedBytes(std::string reason) const
{
    auto it = nMarkedBytes.find(reason);

    if (it != nMarkedBytes.end())
    {
        return it->second;
    }

    return 0;
}

void
QueueDisc::Stats::Print(std::ostream& os) const
{
    os << std::endl
       << "Packets/Bytes received: " << nTotalReceivedPackets << " / " << nTotalReceivedBytes
       << std::endl
       << "Packets/Bytes enqueued: " << nTotalEnqueuedPackets << " / " << nTotalEnqueuedBytes
       << std::endl
       << "Packets/Bytes dequeued: " << nTotalDequeuedPackets << " / " << nTotalDequeuedBytes
       << std::endl
       << "Packets/Bytes requeued: " << nTotalRequeuedPackets << " / " << nTotalRequeuedBytes
       << std::endl
       << "Packets/Bytes dropped: " << nTotalDroppedPackets << " / " << nTotalDroppedBytes
       << std::endl
       << "Packets/Bytes dropped before enqueue: " << nTotalDroppedPacketsBeforeEnqueue << " / "
       << nTotalDroppedBytesBeforeEnqueue;

    auto itp = nDroppedPacketsBeforeEnqueue.begin();
    auto itb = nDroppedBytesBeforeEnqueue.begin();

    while (itp != nDroppedPacketsBeforeEnqueue.end() && itb != nDroppedBytesBeforeEnqueue.end())
    {
        NS_ASSERT(itp->first == itb->first);
        os << std::endl << "  " << itp->first << ": " << itp->second << " / " << itb->second;
        itp++;
        itb++;
    }

    os << std::endl
       << "Packets/Bytes dropped after dequeue: " << nTotalDroppedPacketsAfterDequeue << " / "
       << nTotalDroppedBytesAfterDequeue;

    itp = nDroppedPacketsAfterDequeue.begin();
    itb = nDroppedBytesAfterDequeue.begin();

    while (itp != nDroppedPacketsAfterDequeue.end() && itb != nDroppedBytesAfterDequeue.end())
    {
        NS_ASSERT(itp->first == itb->first);
        os << std::endl << "  " << itp->first << ": " << itp->second << " / " << itb->second;
        itp++;
        itb++;
    }

    os << std::endl
       << "Packets/Bytes sent: " << nTotalSentPackets << " / " << nTotalSentBytes << std::endl
       << "Packets/Bytes marked: " << nTotalMarkedPackets << " / " << nTotalMarkedBytes;

    itp = nMarkedPackets.begin();
    itb = nMarkedBytes.begin();

    while (itp != nMarkedPackets.end() && itb != nMarkedBytes.end())
    {
        NS_ASSERT(itp->first == itb->first);
        os << std::endl << "  " << itp->first << ": " << itp->second << " / " << itb->second;
        itp++;
        itb++;
    }

    os << std::endl;
}

std::ostream&
operator<<(std::ostream& os, const QueueDisc::Stats& stats)
{
    stats.Print(os);
    return os;
}

NS_OBJECT_ENSURE_REGISTERED(QueueDisc);

TypeId
QueueDisc::GetTypeId()
{
    static TypeId tid =
        TypeId("ns3::QueueDisc")
            .SetParent<Object>()
            .SetGroupName("TrafficControl")
            .AddAttribute("Quota",
                          "The maximum number of packets dequeued in a qdisc run",
                          UintegerValue(DEFAULT_QUOTA),
                          MakeUintegerAccessor(&QueueDisc::SetQuota, &QueueDisc::GetQuota),
                          MakeUintegerChecker<uint32_t>())
            .AddAttribute("InternalQueueList",
                          "The list of internal queues.",
                          ObjectVectorValue(),
                          MakeObjectVectorAccessor(&QueueDisc::m_queues),
                          MakeObjectVectorChecker<InternalQueue>())
            .AddAttribute("PacketFilterList",
                          "The list of packet filters.",
                          ObjectVectorValue(),
                          MakeObjectVectorAccessor(&QueueDisc::m_filters),
                          MakeObjectVectorChecker<PacketFilter>())
            .AddAttribute("QueueDiscClassList",
                          "The list of queue disc classes.",
                          ObjectVectorValue(),
                          MakeObjectVectorAccessor(&QueueDisc::m_classes),
                          MakeObjectVectorChecker<QueueDiscClass>())
            .AddTraceSource("Enqueue",
                            "Enqueue a packet in the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_traceEnqueue),
                            "ns3::QueueDiscItem::TracedCallback")
            .AddTraceSource("Dequeue",
                            "Dequeue a packet from the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_traceDequeue),
                            "ns3::QueueDiscItem::TracedCallback")
            .AddTraceSource("Requeue",
                            "Requeue a packet in the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_traceRequeue),
                            "ns3::QueueDiscItem::TracedCallback")
            .AddTraceSource("Drop",
                            "Drop a packet stored in the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_traceDrop),
                            "ns3::QueueDiscItem::TracedCallback")
            .AddTraceSource("DropBeforeEnqueue",
                            "Drop a packet before enqueue",
                            MakeTraceSourceAccessor(&QueueDisc::m_traceDropBeforeEnqueue),
                            "ns3::QueueDiscItem::TracedCallback")
            .AddTraceSource("DropAfterDequeue",
                            "Drop a packet after dequeue",
                            MakeTraceSourceAccessor(&QueueDisc::m_traceDropAfterDequeue),
                            "ns3::QueueDiscItem::TracedCallback")
            .AddTraceSource("Mark",
                            "Mark a packet stored in the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_traceMark),
                            "ns3::QueueDiscItem::TracedCallback")
            .AddTraceSource("PacketsInQueue",
                            "Number of packets currently stored in the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_nPackets),
                            "ns3::TracedValueCallback::Uint32")
            .AddTraceSource("BytesInQueue",
                            "Number of bytes currently stored in the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_nBytes),
                            "ns3::TracedValueCallback::Uint32")
            .AddTraceSource("SojournTime",
                            "Sojourn time of the last packet dequeued from the queue disc",
                            MakeTraceSourceAccessor(&QueueDisc::m_sojourn),
                            "ns3::Time::TracedCallback");
    return tid;
}

QueueDisc::QueueDisc(QueueDiscSizePolicy policy)
    : m_nPackets(0),
      m_nBytes(0),
      m_maxSize(QueueSize("1p")), // to avoid that setting the mode at construction time is ignored
      m_running(false),
      m_peeked(false),
      m_sizePolicy(policy),
      m_prohibitChangeMode(false)
{
    NS_LOG_FUNCTION(this << (uint16_t)policy);

    // These lambdas call the DropBeforeEnqueue or DropAfterDequeue methods of this
    // QueueDisc object. Given that a callback to the operator() of these lambdas
    // is connected to the DropBeforeEnqueue and DropAfterDequeue traces of the
    // internal queues, the INTERNAL_QUEUE_DROP constant is passed as the reason
    // why the packet is dropped.
    m_internalQueueDbeFunctor = [this](Ptr<const QueueDiscItem> item) {
        return DropBeforeEnqueue(item, INTERNAL_QUEUE_DROP);
    };
    m_internalQueueDadFunctor = [this](Ptr<const QueueDiscItem> item) {
        return DropAfterDequeue(item, INTERNAL_QUEUE_DROP);
    };

    // These lambdas call the DropBeforeEnqueue or DropAfterDequeue methods of this
    // QueueDisc object. Given that a callback to the operator() of these lambdas
    // is connected to the DropBeforeEnqueue and DropAfterDequeue traces of the
    // child queue discs, the concatenation of the CHILD_QUEUE_DISC_DROP constant
    // and the second argument provided by such traces is passed as the reason why
    // the packet is dropped.
    m_childQueueDiscDbeFunctor = [this](Ptr<const QueueDiscItem> item, const char* r) {
        return DropBeforeEnqueue(
            item,
            m_childQueueDiscDropMsg.assign(CHILD_QUEUE_DISC_DROP).append(r).data());
    };
    m_childQueueDiscDadFunctor = [this](Ptr<const QueueDiscItem> item, const char* r) {
        return DropAfterDequeue(
            item,
            m_childQueueDiscDropMsg.assign(CHILD_QUEUE_DISC_DROP).append(r).data());
    };
    m_childQueueDiscMarkFunctor = [this](Ptr<const QueueDiscItem> item, const char* r) {
        return Mark(const_cast<QueueDiscItem*>(PeekPointer(item)),
                    m_childQueueDiscMarkMsg.assign(CHILD_QUEUE_DISC_MARK).append(r).data());
    };
}

QueueDisc::QueueDisc(QueueDiscSizePolicy policy, QueueSizeUnit unit)
    : QueueDisc(policy)
{
    m_maxSize = QueueSize(unit, 0);
    m_prohibitChangeMode = true;
}

QueueDisc::~QueueDisc()
{
    NS_LOG_FUNCTION(this);
}

void
QueueDisc::DoDispose()
{
    NS_LOG_FUNCTION(this);
    m_queues.clear();
    m_filters.clear();
    m_classes.clear();
    m_devQueueIface = nullptr;
    m_send = nullptr;
    m_requeued = nullptr;
    m_internalQueueDbeFunctor = nullptr;
    m_internalQueueDadFunctor = nullptr;
    m_childQueueDiscDbeFunctor = nullptr;
    m_childQueueDiscDadFunctor = nullptr;
    Object::DoDispose();
}

void
QueueDisc::DoInitialize()
{
    NS_LOG_FUNCTION(this);

    // Check the configuration and initialize the parameters of this queue disc
    bool ok [[maybe_unused]] = CheckConfig();
    NS_ASSERT_MSG(ok, "The queue disc configuration is not correct");
    InitializeParams();

    // Check the configuration and initialize the parameters of the child queue discs
    for (auto cl = m_classes.begin(); cl != m_classes.end(); cl++)
    {
        (*cl)->GetQueueDisc()->Initialize();
    }

    Object::DoInitialize();
}

const QueueDisc::Stats&
QueueDisc::GetStats()
{
    NS_ASSERT(m_stats.nTotalDroppedPackets ==
              m_stats.nTotalDroppedPacketsBeforeEnqueue + m_stats.nTotalDroppedPacketsAfterDequeue);
    NS_ASSERT(m_stats.nTotalDroppedBytes ==
              m_stats.nTotalDroppedBytesBeforeEnqueue + m_stats.nTotalDroppedBytesAfterDequeue);

    // the total number of sent packets is only updated here to avoid to increase it
    // after a dequeue and then having to decrease it if the packet is dropped after
    // dequeue or requeued
    m_stats.nTotalSentPackets = m_stats.nTotalDequeuedPackets - (m_requeued ? 1 : 0) -
                                m_stats.nTotalDroppedPacketsAfterDequeue;
    m_stats.nTotalSentBytes = m_stats.nTotalDequeuedBytes -
                              (m_requeued ? m_requeued->GetSize() : 0) -
                              m_stats.nTotalDroppedBytesAfterDequeue;

    return m_stats;
}

uint32_t
QueueDisc::GetNPackets() const
{
    NS_LOG_FUNCTION(this);
    return m_nPackets;
}

uint32_t
QueueDisc::GetNBytes() const
{
    NS_LOG_FUNCTION(this);
    return m_nBytes;
}

QueueSize
QueueDisc::GetMaxSize() const
{
    NS_LOG_FUNCTION(this);

    switch (m_sizePolicy)
    {
    case QueueDiscSizePolicy::NO_LIMITS:
        NS_FATAL_ERROR("The size of this queue disc is not limited");

    case QueueDiscSizePolicy::SINGLE_INTERNAL_QUEUE:
        if (GetNInternalQueues())
        {
            return GetInternalQueue(0)->GetMaxSize();
        }

    case QueueDiscSizePolicy::SINGLE_CHILD_QUEUE_DISC:
        if (GetNQueueDiscClasses())
        {
            return GetQueueDiscClass(0)->GetQueueDisc()->GetMaxSize();
        }

    case QueueDiscSizePolicy::MULTIPLE_QUEUES:
    default:
        return m_maxSize;
    }
}

bool
QueueDisc::SetMaxSize(QueueSize size)
{
    NS_LOG_FUNCTION(this << size);

    // do nothing if the limit is null
    if (!size.GetValue())
    {
        return false;
    }

    if (m_prohibitChangeMode && size.GetUnit() != m_maxSize.GetUnit())
    {
        NS_LOG_DEBUG("Changing the mode of this queue disc is prohibited");
        return false;
    }

    switch (m_sizePolicy)
    {
    case QueueDiscSizePolicy::NO_LIMITS:
        NS_FATAL_ERROR("The size of this queue disc is not limited");

    case QueueDiscSizePolicy::SINGLE_INTERNAL_QUEUE:
        if (GetNInternalQueues())
        {
            GetInternalQueue(0)->SetMaxSize(size);
        }

    case QueueDiscSizePolicy::SINGLE_CHILD_QUEUE_DISC:
        if (GetNQueueDiscClasses())
        {
            GetQueueDiscClass(0)->GetQueueDisc()->SetMaxSize(size);
        }

    case QueueDiscSizePolicy::MULTIPLE_QUEUES:
    default:
        m_maxSize = size;
    }
    return true;
}

QueueSize
QueueDisc::GetCurrentSize() const
{
    NS_LOG_FUNCTION(this);

    if (GetMaxSize().GetUnit() == QueueSizeUnit::PACKETS)
    {
        return QueueSize(QueueSizeUnit::PACKETS, m_nPackets);
    }
    if (GetMaxSize().GetUnit() == QueueSizeUnit::BYTES)
    {
        return QueueSize(QueueSizeUnit::BYTES, m_nBytes);
    }
    NS_ABORT_MSG("Unknown queue size unit");
}

void
QueueDisc::SetNetDeviceQueueInterface(Ptr<NetDeviceQueueInterface> ndqi)
{
    NS_LOG_FUNCTION(this << ndqi);
    m_devQueueIface = ndqi;
}

Ptr<NetDeviceQueueInterface>
QueueDisc::GetNetDeviceQueueInterface() const
{
    NS_LOG_FUNCTION(this);
    return m_devQueueIface;
}

void
QueueDisc::SetSendCallback(SendCallback func)
{
    NS_LOG_FUNCTION(this);
    m_send = func;
}

QueueDisc::SendCallback
QueueDisc::GetSendCallback() const
{
    NS_LOG_FUNCTION(this);
    return m_send;
}

void
QueueDisc::SetQuota(const uint32_t quota)
{
    NS_LOG_FUNCTION(this << quota);
    m_quota = quota;
}

uint32_t
QueueDisc::GetQuota() const
{
    NS_LOG_FUNCTION(this);
    return m_quota;
}

void
QueueDisc::AddInternalQueue(Ptr<InternalQueue> queue)
{
    NS_LOG_FUNCTION(this);

    // set various callbacks on the internal queue, so that the queue disc is
    // notified of packets enqueued, dequeued or dropped by the internal queue
    queue->TraceConnectWithoutContext("Enqueue", MakeCallback(&QueueDisc::PacketEnqueued, this));
    queue->TraceConnectWithoutContext("Dequeue", MakeCallback(&QueueDisc::PacketDequeued, this));
    queue->TraceConnectWithoutContext(
        "DropBeforeEnqueue",
        MakeCallback(&InternalQueueDropFunctor::operator(), &m_internalQueueDbeFunctor));
    queue->TraceConnectWithoutContext(
        "DropAfterDequeue",
        MakeCallback(&InternalQueueDropFunctor::operator(), &m_internalQueueDadFunctor));
    m_queues.push_back(queue);
}

Ptr<QueueDisc::InternalQueue>
QueueDisc::GetInternalQueue(std::size_t i) const
{
    NS_ASSERT(i < m_queues.size());
    return m_queues[i];
}

std::size_t
QueueDisc::GetNInternalQueues() const
{
    return m_queues.size();
}

void
QueueDisc::AddPacketFilter(Ptr<PacketFilter> filter)
{
    NS_LOG_FUNCTION(this);
    m_filters.push_back(filter);
}

Ptr<PacketFilter>
QueueDisc::GetPacketFilter(std::size_t i) const
{
    NS_ASSERT(i < m_filters.size());
    return m_filters[i];
}

std::size_t
QueueDisc::GetNPacketFilters() const
{
    return m_filters.size();
}

void
QueueDisc::AddQueueDiscClass(Ptr<QueueDiscClass> qdClass)
{
    NS_LOG_FUNCTION(this);
    NS_ABORT_MSG_IF(!qdClass->GetQueueDisc(), "Cannot add a class with no attached queue disc");
    // the child queue disc cannot be one with wake mode equal to WAKE_CHILD because
    // such queue discs do not implement the enqueue/dequeue methods
    NS_ABORT_MSG_IF(qdClass->GetQueueDisc()->GetWakeMode() == WAKE_CHILD,
                    "A queue disc with WAKE_CHILD as wake mode can only be a root queue disc");

    // set the parent callbacks on the child queue disc, so that it can notify
    // the parent queue disc of packets enqueued, dequeued, dropped, or marked
    qdClass->GetQueueDisc()->TraceConnectWithoutContext(
        "Enqueue",
        MakeCallback(&QueueDisc::PacketEnqueued, this));
    qdClass->GetQueueDisc()->TraceConnectWithoutContext(
        "Dequeue",
        MakeCallback(&QueueDisc::PacketDequeued, this));
    qdClass->GetQueueDisc()->TraceConnectWithoutContext(
        "DropBeforeEnqueue",
        MakeCallback(&ChildQueueDiscDropFunctor::operator(), &m_childQueueDiscDbeFunctor));
    qdClass->GetQueueDisc()->TraceConnectWithoutContext(
        "DropAfterDequeue",
        MakeCallback(&ChildQueueDiscDropFunctor::operator(), &m_childQueueDiscDadFunctor));
    qdClass->GetQueueDisc()->TraceConnectWithoutContext(
        "Mark",
        MakeCallback(&ChildQueueDiscMarkFunctor::operator(), &m_childQueueDiscMarkFunctor));
    m_classes.push_back(qdClass);
}

Ptr<QueueDiscClass>
QueueDisc::GetQueueDiscClass(std::size_t i) const
{
    NS_ASSERT(i < m_classes.size());
    return m_classes[i];
}

std::size_t
QueueDisc::GetNQueueDiscClasses() const
{
    return m_classes.size();
}

int32_t
QueueDisc::Classify(Ptr<QueueDiscItem> item)
{
    NS_LOG_FUNCTION(this << item);

    int32_t ret = PacketFilter::PF_NO_MATCH;
    for (auto f = m_filters.begin(); f != m_filters.end() && ret == PacketFilter::PF_NO_MATCH; f++)
    {
        ret = (*f)->Classify(item);
    }
    return ret;
}

QueueDisc::WakeMode
QueueDisc::GetWakeMode() const
{
    return WAKE_ROOT;
}

void
QueueDisc::PacketEnqueued(Ptr<const QueueDiscItem> item)
{
    m_nPackets++;
    m_nBytes += item->GetSize();
    m_stats.nTotalEnqueuedPackets++;
    m_stats.nTotalEnqueuedBytes += item->GetSize();

    NS_LOG_LOGIC("m_traceEnqueue (p)");
    m_traceEnqueue(item);
}

void
QueueDisc::PacketDequeued(Ptr<const QueueDiscItem> item)
{
    // If the queue disc asked the internal queue or the child queue disc to
    // dequeue a packet because a peek operation was requested, the packet is
    // still held by the queue disc, hence we do not need to update statistics
    // and fire the dequeue trace. This function will be explicitly called when
    // the packet will be actually dequeued.
    if (!m_peeked)
    {
        m_nPackets--;
        m_nBytes -= item->GetSize();
        m_stats.nTotalDequeuedPackets++;
        m_stats.nTotalDequeuedBytes += item->GetSize();

        m_sojourn(Simulator::Now() - item->GetTimeStamp());

        NS_LOG_LOGIC("m_traceDequeue (p)");
        m_traceDequeue(item);
    }
}

void
QueueDisc::DropBeforeEnqueue(Ptr<const QueueDiscItem> item, const char* reason)
{
    NS_LOG_FUNCTION(this << item << reason);

    m_stats.nTotalDroppedPackets++;
    m_stats.nTotalDroppedBytes += item->GetSize();
    m_stats.nTotalDroppedPacketsBeforeEnqueue++;
    m_stats.nTotalDroppedBytesBeforeEnqueue += item->GetSize();

    // update the number of packets dropped for the given reason
    auto itp = m_stats.nDroppedPacketsBeforeEnqueue.find(reason);
    if (itp != m_stats.nDroppedPacketsBeforeEnqueue.end())
    {
        itp->second++;
    }
    else
    {
        m_stats.nDroppedPacketsBeforeEnqueue[reason] = 1;
    }
    // update the amount of bytes dropped for the given reason
    auto itb = m_stats.nDroppedBytesBeforeEnqueue.find(reason);
    if (itb != m_stats.nDroppedBytesBeforeEnqueue.end())
    {
        itb->second += item->GetSize();
    }
    else
    {
        m_stats.nDroppedBytesBeforeEnqueue[reason] = item->GetSize();
    }

    NS_LOG_DEBUG("Total packets/bytes dropped before enqueue: "
                 << m_stats.nTotalDroppedPacketsBeforeEnqueue << " / "
                 << m_stats.nTotalDroppedBytesBeforeEnqueue);
    NS_LOG_LOGIC("m_traceDropBeforeEnqueue (p)");
    m_traceDrop(item);
    m_traceDropBeforeEnqueue(item, reason);
}

void
QueueDisc::DropAfterDequeue(Ptr<const QueueDiscItem> item, const char* reason)
{
    NS_LOG_FUNCTION(this << item << reason);

    m_stats.nTotalDroppedPackets++;
    m_stats.nTotalDroppedBytes += item->GetSize();
    m_stats.nTotalDroppedPacketsAfterDequeue++;
    m_stats.nTotalDroppedBytesAfterDequeue += item->GetSize();

    // update the number of packets dropped for the given reason
    auto itp = m_stats.nDroppedPacketsAfterDequeue.find(reason);
    if (itp != m_stats.nDroppedPacketsAfterDequeue.end())
    {
        itp->second++;
    }
    else
    {
        m_stats.nDroppedPacketsAfterDequeue[reason] = 1;
    }
    // update the amount of bytes dropped for the given reason
    auto itb = m_stats.nDroppedBytesAfterDequeue.find(reason);
    if (itb != m_stats.nDroppedBytesAfterDequeue.end())
    {
        itb->second += item->GetSize();
    }
    else
    {
        m_stats.nDroppedBytesAfterDequeue[reason] = item->GetSize();
    }

    // if in the context of a peek request a dequeued packet is dropped, we need
    // to update the statistics and fire the dequeue trace before firing the drop
    // after dequeue trace
    if (m_peeked)
    {
        // temporarily set m_peeked to false, otherwise PacketDequeued does nothing
        m_peeked = false;
        PacketDequeued(item);
        m_peeked = true;
    }

    NS_LOG_DEBUG("Total packets/bytes dropped after dequeue: "
                 << m_stats.nTotalDroppedPacketsAfterDequeue << " / "
                 << m_stats.nTotalDroppedBytesAfterDequeue);
    NS_LOG_LOGIC("m_traceDropAfterDequeue (p)");
    m_traceDrop(item);
    m_traceDropAfterDequeue(item, reason);
}

bool
QueueDisc::Mark(Ptr<QueueDiscItem> item, const char* reason)
{
    NS_LOG_FUNCTION(this << item << reason);

    bool retval = item->Mark();

    if (!retval)
    {
        return false;
    }

    m_stats.nTotalMarkedPackets++;
    m_stats.nTotalMarkedBytes += item->GetSize();

    // update the number of packets marked for the given reason
    auto itp = m_stats.nMarkedPackets.find(reason);
    if (itp != m_stats.nMarkedPackets.end())
    {
        itp->second++;
    }
    else
    {
        m_stats.nMarkedPackets[reason] = 1;
    }
    // update the amount of bytes marked for the given reason
    auto itb = m_stats.nMarkedBytes.find(reason);
    if (itb != m_stats.nMarkedBytes.end())
    {
        itb->second += item->GetSize();
    }
    else
    {
        m_stats.nMarkedBytes[reason] = item->GetSize();
    }

    NS_LOG_DEBUG("Total packets/bytes marked: " << m_stats.nTotalMarkedPackets << " / "
                                                << m_stats.nTotalMarkedBytes);
    m_traceMark(item, reason);
    return true;
}

bool
QueueDisc::Enqueue(Ptr<QueueDiscItem> item)
{
    NS_LOG_FUNCTION(this << item);

    m_stats.nTotalReceivedPackets++;
    m_stats.nTotalReceivedBytes += item->GetSize();

    bool retval = DoEnqueue(item);

    if (retval)
    {
        item->SetTimeStamp(Simulator::Now());
    }

    // DoEnqueue may return false because:
    // 1) the internal queue is full
    //    -> the DropBeforeEnqueue method of this queue disc is automatically called
    //       because QueueDisc::AddInternalQueue sets the trace callback
    // 2) the child queue disc dropped the packet
    //    -> the DropBeforeEnqueue method of this queue disc is automatically called
    //       because QueueDisc::AddQueueDiscClass sets the trace callback
    // 3) it dropped the packet
    //    -> DoEnqueue has to explicitly call DropBeforeEnqueue
    // Thus, we do not have to call DropBeforeEnqueue here.

    // check that the received packet was either enqueued or dropped
    NS_ASSERT(m_stats.nTotalReceivedPackets ==
              m_stats.nTotalDroppedPacketsBeforeEnqueue + m_stats.nTotalEnqueuedPackets);
    NS_ASSERT(m_stats.nTotalReceivedBytes ==
              m_stats.nTotalDroppedBytesBeforeEnqueue + m_stats.nTotalEnqueuedBytes);

    return retval;
}

Ptr<QueueDiscItem>
QueueDisc::Dequeue()
{
    NS_LOG_FUNCTION(this);

    // The QueueDisc::DoPeek method dequeues a packet and keeps it as a requeued
    // packet. Thus, first check whether a peeked packet exists. Otherwise, call
    // the private DoDequeue method.
    Ptr<QueueDiscItem> item = m_requeued;

    if (item)
    {
        m_requeued = nullptr;
        if (m_peeked)
        {
            // If the packet was requeued because a peek operation was requested
            // (which is the case here because DequeuePacket calls Dequeue only
            // when m_requeued is null), we need to explicitly call PacketDequeued
            // to update statistics about dequeued packets and fire the dequeue trace.
            m_peeked = false;
            PacketDequeued(item);
        }
    }
    else
    {
        item = DoDequeue();
    }

    NS_ASSERT(m_nPackets == m_stats.nTotalEnqueuedPackets - m_stats.nTotalDequeuedPackets);
    NS_ASSERT(m_nBytes == m_stats.nTotalEnqueuedBytes - m_stats.nTotalDequeuedBytes);

    return item;
}

Ptr<const QueueDiscItem>
QueueDisc::Peek()
{
    NS_LOG_FUNCTION(this);
    return DoPeek();
}

Ptr<const QueueDiscItem>
QueueDisc::DoPeek()
{
    NS_LOG_FUNCTION(this);

    if (!m_requeued)
    {
        m_peeked = true;
        m_requeued = Dequeue();
        // if no packet is returned, reset the m_peeked flag
        if (!m_requeued)
        {
            m_peeked = false;
        }
    }
    return m_requeued;
}

void
QueueDisc::Run()
{
    NS_LOG_FUNCTION(this);

    if (RunBegin())
    {
        uint32_t quota = m_quota;
        while (Restart())
        {
            quota -= 1;
            if (quota <= 0)
            {
                /// \todo netif_schedule (q);
                break;
            }
        }
        RunEnd();
    }
}

bool
QueueDisc::RunBegin()
{
    NS_LOG_FUNCTION(this);
    if (m_running)
    {
        return false;
    }

    m_running = true;
    return true;
}

void
QueueDisc::RunEnd()
{
    NS_LOG_FUNCTION(this);
    m_running = false;
}

bool
QueueDisc::Restart()
{
    NS_LOG_FUNCTION(this);
    Ptr<QueueDiscItem> item = DequeuePacket();
    if (!item)
    {
        NS_LOG_LOGIC("No packet to send");
        return false;
    }

    return Transmit(item);
}

Ptr<QueueDiscItem>
QueueDisc::DequeuePacket()
{
    NS_LOG_FUNCTION(this);

    Ptr<QueueDiscItem> item;

    // First check if there is a requeued packet
    if (m_requeued)
    {
        // If the queue where the requeued packet is destined to is not stopped, return
        // the requeued packet; otherwise, return an empty packet.
        // If the device does not support flow control, the device queue is never stopped
        if (!m_devQueueIface ||
            !m_devQueueIface->GetTxQueue(m_requeued->GetTxQueueIndex())->IsStopped())
        {
            item = m_requeued;
            m_requeued = nullptr;
            if (m_peeked)
            {
                // If the packet was requeued because a peek operation was requested
                // we need to explicitly call PacketDequeued to update statistics
                // about dequeued packets and fire the dequeue trace.
                m_peeked = false;
                PacketDequeued(item);
            }
        }
    }
    else
    {
        // If the device is multi-queue (actually, Linux checks if the queue disc has
        // multiple queues), ask the queue disc to dequeue a packet (a multi-queue aware
        // queue disc should try not to dequeue a packet destined to a stopped queue).
        // Otherwise, ask the queue disc to dequeue a packet only if the (unique) queue
        // is not stopped.
        if (!m_devQueueIface || m_devQueueIface->GetNTxQueues() > 1 ||
            !m_devQueueIface->GetTxQueue(0)->IsStopped())
        {
            item = Dequeue();
            // If the item is not null, add the header to the packet.
            if (item)
            {
                item->AddHeader();
            }
            // Here, Linux tries bulk dequeues
        }
    }
    return item;
}

void
QueueDisc::Requeue(Ptr<QueueDiscItem> item)
{
    NS_LOG_FUNCTION(this << item);
    m_requeued = item;
    /// \todo netif_schedule (q);

    m_stats.nTotalRequeuedPackets++;
    m_stats.nTotalRequeuedBytes += item->GetSize();

    NS_LOG_LOGIC("m_traceRequeue (p)");
    m_traceRequeue(item);
}

bool
QueueDisc::Transmit(Ptr<QueueDiscItem> item)
{
    NS_LOG_FUNCTION(this << item);

    // if the device queue is stopped, requeue the packet and return false.
    // Note that if the underlying device is tc-unaware, packets are never
    // requeued because the queues of tc-unaware devices are never stopped
    if (m_devQueueIface && m_devQueueIface->GetTxQueue(item->GetTxQueueIndex())->IsStopped())
    {
        Requeue(item);
        return false;
    }

    // a single queue device makes no use of the priority tag
    // a device that does not install a device queue interface likely makes no use of it as well
    if (!m_devQueueIface || m_devQueueIface->GetNTxQueues() == 1)
    {
        SocketPriorityTag priorityTag;
        item->GetPacket()->RemovePacketTag(priorityTag);
    }
    NS_ASSERT_MSG(m_send, "Send callback not set");
    m_send(item);

    // the behavior here slightly diverges from Linux. In Linux, it is advised that
    // the function called when a packet needs to be transmitted (ndo_start_xmit)
    // should always return NETDEV_TX_OK, which means that the packet is consumed by
    // the device driver and thus is not requeued. However, the ndo_start_xmit function
    // of the device driver is allowed to return NETDEV_TX_BUSY (and hence the packet
    // is requeued) when there is no room for the received packet in the device queue,
    // despite the queue is not stopped. This case is considered as a corner case or
    // an hard error, and should be avoided.
    // Here, we do not handle such corner case and always assume that the packet is
    // consumed by the netdevice. Thus, we ignore the value returned by Send and a
    // packet sent to a netdevice is never requeued. The reason is that the semantics
    // of the value returned by NetDevice::Send does not match that of the value
    // returned by ndo_start_xmit.

    // if the queue disc is empty or the device queue is now stopped, return false so
    // that the Run method does not attempt to dequeue other packets and exits
    return !(
        GetNPackets() == 0 ||
        (m_devQueueIface && m_devQueueIface->GetTxQueue(item->GetTxQueueIndex())->IsStopped()));
}

} // namespace ns3
