/*
 * Copyright (c) 2020 Universita' degli Studi di Napoli Federico II
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Author: Stefano Avallone <stavallo@unina.it>
 */

#include "he-frame-exchange-manager.h"

#include "he-configuration.h"
#include "he-phy.h"
#include "multi-user-scheduler.h"

#include "ns3/abort.h"
#include "ns3/ap-wifi-mac.h"
#include "ns3/erp-ofdm-phy.h"
#include "ns3/log.h"
#include "ns3/recipient-block-ack-agreement.h"
#include "ns3/snr-tag.h"
#include "ns3/sta-wifi-mac.h"
#include "ns3/wifi-mac-queue.h"
#include "ns3/wifi-mac-trailer.h"

#include <algorithm>
#include <functional>

#undef NS_LOG_APPEND_CONTEXT
#define NS_LOG_APPEND_CONTEXT WIFI_FEM_NS_LOG_APPEND_CONTEXT

namespace ns3
{

NS_LOG_COMPONENT_DEFINE("HeFrameExchangeManager");

NS_OBJECT_ENSURE_REGISTERED(HeFrameExchangeManager);

bool
IsTrigger(const WifiPsduMap& psduMap)
{
    return psduMap.size() == 1 && psduMap.cbegin()->first == SU_STA_ID &&
           psduMap.cbegin()->second->GetNMpdus() == 1 &&
           psduMap.cbegin()->second->GetHeader(0).IsTrigger();
}

bool
IsTrigger(const WifiConstPsduMap& psduMap)
{
    return psduMap.size() == 1 && psduMap.cbegin()->first == SU_STA_ID &&
           psduMap.cbegin()->second->GetNMpdus() == 1 &&
           psduMap.cbegin()->second->GetHeader(0).IsTrigger();
}

TypeId
HeFrameExchangeManager::GetTypeId()
{
    static TypeId tid =
        TypeId("ns3::HeFrameExchangeManager")
            .SetParent<VhtFrameExchangeManager>()
            .AddConstructor<HeFrameExchangeManager>()
            .SetGroupName("Wifi")
            .AddAttribute("ContinueTxopAfterBsrp",
                          "Whether to continue a TXOP a SIFS after the reception of responses "
                          "to a BSRP Trigger Frame when TXOP limit is zero.",
                          BooleanValue(false),
                          MakeBooleanAccessor(&HeFrameExchangeManager::m_continueTxopAfterBsrpTf),
                          MakeBooleanChecker());
    return tid;
}

HeFrameExchangeManager::HeFrameExchangeManager()
    : m_intraBssNavEnd(0),
      m_triggerFrameInAmpdu(false)
{
    NS_LOG_FUNCTION(this);
}

HeFrameExchangeManager::~HeFrameExchangeManager()
{
    NS_LOG_FUNCTION_NOARGS();
}

void
HeFrameExchangeManager::Reset()
{
    NS_LOG_FUNCTION(this);
    if (m_intraBssNavResetEvent.IsPending())
    {
        m_intraBssNavResetEvent.Cancel();
    }
    m_intraBssNavEnd = Simulator::Now();
    VhtFrameExchangeManager::Reset();
}

void
HeFrameExchangeManager::RxStartIndication(WifiTxVector txVector, Time psduDuration)
{
    NS_LOG_FUNCTION(this << txVector << psduDuration.As(Time::MS));
    VhtFrameExchangeManager::RxStartIndication(txVector, psduDuration);
    // Cancel intra-BSS NAV reset timer when receiving a frame from the PHY
    m_intraBssNavResetEvent.Cancel();
}

void
HeFrameExchangeManager::DoDispose()
{
    NS_LOG_FUNCTION(this);
    m_staMac = nullptr;
    m_psduMap.clear();
    m_txParams.Clear();
    m_muScheduler = nullptr;
    m_multiStaBaEvent.Cancel();
    VhtFrameExchangeManager::DoDispose();
}

void
HeFrameExchangeManager::SetMultiUserScheduler(const Ptr<MultiUserScheduler> muScheduler)
{
    NS_ASSERT(m_mac);
    NS_ABORT_MSG_IF(!m_apMac, "A Multi-User Scheduler can only be aggregated to an AP");
    NS_ABORT_MSG_IF(!m_apMac->GetHeConfiguration(),
                    "A Multi-User Scheduler can only be aggregated to an HE AP");
    m_muScheduler = muScheduler;
}

bool
HeFrameExchangeManager::StartFrameExchange(Ptr<QosTxop> edca, Time availableTime, bool initialFrame)
{
    NS_LOG_FUNCTION(this << edca << availableTime << initialFrame);

    MultiUserScheduler::TxFormat txFormat = MultiUserScheduler::SU_TX;
    Ptr<const WifiMpdu> mpdu;

    /*
     * We consult the Multi-user Scheduler (if available) to know the type of transmission to make
     * if:
     * - there is no pending BlockAckReq to transmit
     * - either the AC queue is empty (the scheduler might select an UL MU transmission)
     *   or the next frame in the AC queue is a non-broadcast QoS data frame addressed to
     *   a receiver with which a BA agreement has been already established
     */
    if (m_muScheduler && !GetBar(edca->GetAccessCategory()) &&
        (!(mpdu = edca->PeekNextMpdu(m_linkId)) ||
         (mpdu->GetHeader().IsQosData() && !mpdu->GetHeader().GetAddr1().IsGroup() &&
          m_mac->GetBaAgreementEstablishedAsOriginator(mpdu->GetHeader().GetAddr1(),
                                                       mpdu->GetHeader().GetQosTid()))))
    {
        txFormat = m_muScheduler->NotifyAccessGranted(edca,
                                                      availableTime,
                                                      initialFrame,
                                                      m_allowedWidth,
                                                      m_linkId);
    }

    if (txFormat == MultiUserScheduler::SU_TX)
    {
        return VhtFrameExchangeManager::StartFrameExchange(edca, availableTime, initialFrame);
    }

    if (txFormat == MultiUserScheduler::DL_MU_TX)
    {
        if (m_muScheduler->GetDlMuInfo(m_linkId).psduMap.empty())
        {
            NS_LOG_DEBUG(
                "The Multi-user Scheduler returned DL_MU_TX with empty psduMap, do not transmit");
            return false;
        }

        SendPsduMapWithProtection(m_muScheduler->GetDlMuInfo(m_linkId).psduMap,
                                  m_muScheduler->GetDlMuInfo(m_linkId).txParams);
        return true;
    }

    if (txFormat == MultiUserScheduler::UL_MU_TX)
    {
        auto packet = Create<Packet>();
        packet->AddHeader(m_muScheduler->GetUlMuInfo(m_linkId).trigger);
        auto trigger = Create<WifiMpdu>(packet, m_muScheduler->GetUlMuInfo(m_linkId).macHdr);
        SendPsduMapWithProtection(
            WifiPsduMap{
                {SU_STA_ID,
                 GetWifiPsdu(trigger, m_muScheduler->GetUlMuInfo(m_linkId).txParams.m_txVector)}},
            m_muScheduler->GetUlMuInfo(m_linkId).txParams);
        return true;
    }

    return false;
}

void
HeFrameExchangeManager::SendPsduMapWithProtection(WifiPsduMap psduMap, WifiTxParameters& txParams)
{
    NS_LOG_FUNCTION(this << &txParams);

    m_psduMap = std::move(psduMap);
    m_txParams = std::move(txParams);

    // Make sure that the acknowledgment time has been computed, so that SendMuRts()
    // can reuse this value.
    NS_ASSERT(m_txParams.m_acknowledgment);

    if (!m_txParams.m_acknowledgment->acknowledgmentTime.has_value())
    {
        CalculateAcknowledgmentTime(m_txParams.m_acknowledgment.get());
    }

    // in case we are sending a Trigger Frame, update the acknowledgment time so that
    // the Duration/ID of the MU-RTS is correctly computed
    if (!m_txParams.m_txVector.IsUlMu() && IsTrigger(m_psduMap))
    {
        NS_ASSERT(m_muScheduler);
        const auto& trigger = m_muScheduler->GetUlMuInfo(m_linkId).trigger;
        NS_ASSERT_MSG(!trigger.IsBasic() || m_txParams.m_acknowledgment->method ==
                                                WifiAcknowledgment::UL_MU_MULTI_STA_BA,
                      "Acknowledgment (" << m_txParams.m_acknowledgment.get()
                                         << ") incompatible with Basic Trigger Frame");
        NS_ASSERT_MSG(!trigger.IsBsrp() ||
                          m_txParams.m_acknowledgment->method == WifiAcknowledgment::NONE,
                      "Acknowledgment (" << m_txParams.m_acknowledgment.get()
                                         << ") incompatible with BSRP Trigger Frame");
        // Add a SIFS and the TB PPDU duration to the acknowledgment time of the Trigger Frame
        auto txVector = trigger.GetHeTbTxVector(trigger.begin()->GetAid12());
        *m_txParams.m_acknowledgment->acknowledgmentTime +=
            m_phy->GetSifs() + HePhy::ConvertLSigLengthToHeTbPpduDuration(trigger.GetUlLength(),
                                                                          txVector,
                                                                          m_phy->GetPhyBand());
    }

    // Set QoS Ack policy
    for (auto& psdu : m_psduMap)
    {
        WifiAckManager::SetQosAckPolicy(psdu.second, m_txParams.m_acknowledgment.get());
    }

    for (const auto& psdu : m_psduMap)
    {
        for (const auto& mpdu : *PeekPointer(psdu.second))
        {
            if (mpdu->IsQueued())
            {
                mpdu->SetInFlight(m_linkId);
            }
        }
    }

    StartProtection(m_txParams);
}

void
HeFrameExchangeManager::StartProtection(const WifiTxParameters& txParams)
{
    NS_LOG_FUNCTION(this << &txParams);

    NS_ABORT_MSG_IF(m_psduMap.size() > 1 &&
                        txParams.m_protection->method == WifiProtection::RTS_CTS,
                    "Cannot use RTS/CTS with MU PPDUs");
    if (txParams.m_protection->method == WifiProtection::MU_RTS_CTS)
    {
        auto protection = static_cast<WifiMuRtsCtsProtection*>(txParams.m_protection.get());

        NS_ASSERT(protection->muRts.IsMuRts());
        NS_ASSERT(m_sentRtsTo.empty());
        m_sentRtsTo = GetTfRecipients(protection->muRts);

        SendMuRts(txParams);
    }
    else
    {
        VhtFrameExchangeManager::StartProtection(txParams);
    }
}

std::set<Mac48Address>
HeFrameExchangeManager::GetTfRecipients(const CtrlTriggerHeader& trigger) const
{
    std::set<Mac48Address> recipients;
    NS_ASSERT_MSG(m_apMac, "APs only can send Trigger Frames");
    const auto& aidAddrMap = m_apMac->GetStaList(m_linkId);

    for (const auto& userInfo : trigger)
    {
        const auto addressIt = aidAddrMap.find(userInfo.GetAid12());
        NS_ASSERT_MSG(addressIt != aidAddrMap.end(), "AID not found");
        recipients.insert(addressIt->second);
    }

    return recipients;
}

void
HeFrameExchangeManager::ProtectionCompleted()
{
    NS_LOG_FUNCTION(this);
    if (!m_psduMap.empty())
    {
        m_protectedStas.merge(m_sentRtsTo);
        m_sentRtsTo.clear();
        if (m_muScheduler)
        {
            m_muScheduler->NotifyProtectionCompleted(m_linkId, m_psduMap, m_txParams);

            if (m_psduMap.empty())
            {
                NS_LOG_INFO("Multi-user scheduler aborted the transmission");
                NS_ASSERT(m_edca);
                if (m_edca->GetTxopLimit(m_linkId).IsStrictlyPositive())
                {
                    SendCfEndIfNeeded();
                    return;
                }
                NotifyChannelReleased(m_edca);
                m_edca = nullptr;
                return;
            }
        }
        if (m_txParams.m_protection->method == WifiProtection::NONE)
        {
            SendPsduMap();
        }
        else
        {
            Simulator::Schedule(m_phy->GetSifs(), &HeFrameExchangeManager::SendPsduMap, this);
        }
        return;
    }
    VhtFrameExchangeManager::ProtectionCompleted();
}

Time
HeFrameExchangeManager::GetMuRtsDurationId(uint32_t muRtsSize,
                                           const WifiTxVector& muRtsTxVector,
                                           Time txDuration,
                                           Time response) const
{
    NS_LOG_FUNCTION(this << muRtsSize << muRtsTxVector << txDuration << response);

    WifiTxVector txVector;
    txVector.SetMode(GetCtsModeAfterMuRts());
    const auto singleDurationId =
        VhtFrameExchangeManager::GetRtsDurationId(txVector, txDuration, response);

    if (m_edca->GetTxopLimit(m_linkId).IsZero())
    {
        return singleDurationId;
    }

    // under multiple protection settings, if the TXOP limit is not null, Duration/ID
    // is set to cover the remaining TXOP time (Sec. 9.2.5.2 of 802.11-2016).
    // The TXOP holder may exceed the TXOP limit in some situations (Sec. 10.22.2.8
    // of 802.11-2016)
    auto duration =
        std::max(m_edca->GetRemainingTxop(m_linkId) -
                     WifiPhy::CalculateTxDuration(muRtsSize, muRtsTxVector, m_phy->GetPhyBand()),
                 Seconds(0));

    if (m_protectSingleExchange)
    {
        duration = std::min(duration, singleDurationId + m_singleExchangeProtectionSurplus);
    }

    return duration;
}

void
HeFrameExchangeManager::SendMuRts(const WifiTxParameters& txParams)
{
    NS_LOG_FUNCTION(this << &txParams);
    WifiMacHeader hdr;
    hdr.SetType(WIFI_MAC_CTL_TRIGGER);
    hdr.SetAddr1(Mac48Address::GetBroadcast());
    hdr.SetAddr2(m_self);
    hdr.SetDsNotTo();
    hdr.SetDsNotFrom();
    hdr.SetNoRetry();
    hdr.SetNoMoreFragments();

    NS_ASSERT(txParams.m_protection && txParams.m_protection->method == WifiProtection::MU_RTS_CTS);
    auto protection = static_cast<WifiMuRtsCtsProtection*>(txParams.m_protection.get());

    NS_ASSERT(protection->muRts.IsMuRts());
    protection->muRts.SetCsRequired(true);
    Ptr<Packet> payload = Create<Packet>();
    payload->AddHeader(protection->muRts);

    auto mpdu = Create<WifiMpdu>(payload, hdr);

    NS_ASSERT(txParams.m_txDuration.has_value());
    NS_ASSERT(txParams.m_acknowledgment->acknowledgmentTime.has_value());
    mpdu->GetHeader().SetDuration(
        GetMuRtsDurationId(mpdu->GetSize(),
                           protection->muRtsTxVector,
                           *txParams.m_txDuration,
                           *txParams.m_acknowledgment->acknowledgmentTime));

    // Get the TXVECTOR used by one station to send the CTS response. This is used
    // to compute the preamble duration, so it does not matter which station we choose
    WifiTxVector ctsTxVector =
        GetCtsTxVectorAfterMuRts(protection->muRts, protection->muRts.begin()->GetAid12());

    // After transmitting an MU-RTS frame, the STA shall wait for a CTSTimeout interval of
    // aSIFSTime + aSlotTime + aRxPHYStartDelay (Sec. 27.2.5.2 of 802.11ax D3.0).
    // aRxPHYStartDelay equals the time to transmit the PHY header.
    Time timeout = WifiPhy::CalculateTxDuration(mpdu->GetSize(),
                                                protection->muRtsTxVector,
                                                m_phy->GetPhyBand()) +
                   m_phy->GetSifs() + m_phy->GetSlot() +
                   WifiPhy::CalculatePhyPreambleAndHeaderDuration(ctsTxVector);

    NS_ASSERT(!m_txTimer.IsRunning());
    m_txTimer.Set(WifiTxTimer::WAIT_CTS_AFTER_MU_RTS,
                  timeout,
                  m_sentRtsTo,
                  &HeFrameExchangeManager::CtsAfterMuRtsTimeout,
                  this,
                  mpdu,
                  protection->muRtsTxVector);
    m_channelAccessManager->NotifyCtsTimeoutStartNow(timeout);

    ForwardMpduDown(mpdu, protection->muRtsTxVector);
}

void
HeFrameExchangeManager::CtsAfterMuRtsTimeout(Ptr<WifiMpdu> muRts, const WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << *muRts << txVector);

    if (m_psduMap.empty())
    {
        // A CTS Timeout occurred when protecting a single PSDU that is not included
        // in a DL MU PPDU is handled by the parent classes
        VhtFrameExchangeManager::CtsTimeout(muRts, txVector);
        return;
    }

    DoCtsTimeout(m_psduMap);
    m_psduMap.clear();
}

Ptr<WifiPsdu>
HeFrameExchangeManager::GetPsduTo(Mac48Address to, const WifiPsduMap& psduMap)
{
    auto it = std::find_if(
        psduMap.begin(),
        psduMap.end(),
        [&to](std::pair<uint16_t, Ptr<WifiPsdu>> psdu) { return psdu.second->GetAddr1() == to; });
    if (it != psduMap.end())
    {
        return it->second;
    }
    return nullptr;
}

void
HeFrameExchangeManager::CtsTimeout(Ptr<WifiMpdu> rts, const WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << *rts << txVector);

    if (m_psduMap.empty())
    {
        // A CTS Timeout occurred when protecting a single PSDU that is not included
        // in a DL MU PPDU is handled by the parent classes
        VhtFrameExchangeManager::CtsTimeout(rts, txVector);
        return;
    }

    NS_ABORT_MSG_IF(m_psduMap.size() > 1, "RTS/CTS cannot be used to protect an MU PPDU");
    DoCtsTimeout(m_psduMap);
    m_psduMap.clear();
}

void
HeFrameExchangeManager::TransmissionSucceeded()
{
    NS_LOG_FUNCTION(this);

    // A multi-user transmission may succeed even if some stations did not respond.
    // Remove such stations from the set of stations for which protection is not needed
    // in the current TXOP.
    for (const auto& address : m_txTimer.GetStasExpectedToRespond())
    {
        NS_LOG_DEBUG(address << " did not respond, hence it is no longer protected");
        m_protectedStas.erase(address);
        m_sentFrameTo.erase(address);
    }

    if (m_continueTxopAfterBsrpTf && m_edca && m_edca->GetTxopLimit(m_linkId).IsZero() &&
        m_txTimer.IsRunning() &&
        m_txTimer.GetReason() == WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF &&
        (m_txNav > Simulator::Now() + m_phy->GetSifs()))
    {
        NS_LOG_DEBUG("Schedule another transmission in a SIFS after successful BSRP TF");
        Simulator::Schedule(m_phy->GetSifs(), [=, this]() {
            // TXOP limit is null, hence the txopDuration parameter is unused
            if (!StartTransmission(m_edca, Seconds(0)))
            {
                SendCfEndIfNeeded();
            }
        });
        if (m_protectedIfResponded)
        {
            m_protectedStas.merge(m_sentFrameTo);
        }
        m_sentFrameTo.clear();
    }
    else
    {
        VhtFrameExchangeManager::TransmissionSucceeded();
    }
}

void
HeFrameExchangeManager::SendPsduMap()
{
    NS_LOG_FUNCTION(this);

    NS_ASSERT(m_txParams.m_acknowledgment);
    NS_ASSERT(!m_txTimer.IsRunning());

    WifiTxTimer::Reason timerType = WifiTxTimer::NOT_RUNNING; // no timer
    WifiTxVector* responseTxVector = nullptr;
    Ptr<WifiMpdu> mpdu = nullptr;
    Ptr<WifiPsdu> psdu = nullptr;
    WifiTxVector txVector;
    std::set<Mac48Address> staExpectResponseFrom;

    // Compute the type of TX timer to set depending on the acknowledgment method

    /*
     * Acknowledgment via a sequence of BlockAckReq and BlockAck frames
     */
    if (m_txParams.m_acknowledgment->method == WifiAcknowledgment::DL_MU_BAR_BA_SEQUENCE)
    {
        auto acknowledgment =
            static_cast<WifiDlMuBarBaSequence*>(m_txParams.m_acknowledgment.get());

        // schedule the transmission of required BlockAckReq frames
        for (const auto& psdu : m_psduMap)
        {
            if (acknowledgment->stationsSendBlockAckReqTo.contains(psdu.second->GetAddr1()))
            {
                // the receiver of this PSDU will receive a BlockAckReq
                std::set<uint8_t> tids = psdu.second->GetTids();
                NS_ABORT_MSG_IF(tids.size() > 1,
                                "Acknowledgment method incompatible with a Multi-TID A-MPDU");
                uint8_t tid = *tids.begin();

                NS_ASSERT(m_edca);
                auto [reqHdr, hdr] =
                    m_mac->GetQosTxop(tid)->PrepareBlockAckRequest(psdu.second->GetAddr1(), tid);
                m_edca->GetBaManager()->ScheduleBar(reqHdr, hdr);
            }
        }

        if (!acknowledgment->stationsReplyingWithNormalAck.empty())
        {
            // a station will reply immediately with a Normal Ack
            timerType = WifiTxTimer::WAIT_NORMAL_ACK_AFTER_DL_MU_PPDU;
            responseTxVector =
                &acknowledgment->stationsReplyingWithNormalAck.begin()->second.ackTxVector;
            auto from = acknowledgment->stationsReplyingWithNormalAck.begin()->first;
            psdu = GetPsduTo(from, m_psduMap);
            NS_ASSERT(psdu->GetNMpdus() == 1);
            mpdu = *psdu->begin();
            staExpectResponseFrom.insert(from);
        }
        else if (!acknowledgment->stationsReplyingWithBlockAck.empty())
        {
            // a station will reply immediately with a Block Ack
            timerType = WifiTxTimer::WAIT_BLOCK_ACK;
            responseTxVector =
                &acknowledgment->stationsReplyingWithBlockAck.begin()->second.blockAckTxVector;
            auto from = acknowledgment->stationsReplyingWithBlockAck.begin()->first;
            psdu = GetPsduTo(from, m_psduMap);
            staExpectResponseFrom.insert(from);
        }
        // else no station will reply immediately
    }
    /*
     * Acknowledgment via a MU-BAR Trigger Frame sent as single user frame
     */
    else if (m_txParams.m_acknowledgment->method == WifiAcknowledgment::DL_MU_TF_MU_BAR)
    {
        auto acknowledgment = static_cast<WifiDlMuTfMuBar*>(m_txParams.m_acknowledgment.get());

        if (!m_triggerFrame)
        {
            // we are transmitting the DL MU PPDU and have to schedule the
            // transmission of a MU-BAR Trigger Frame.
            // Create a TRIGVECTOR by "merging" all the BlockAck TXVECTORs
            std::map<uint16_t, CtrlBAckRequestHeader> recipients;

            NS_ASSERT(!acknowledgment->stationsReplyingWithBlockAck.empty());
            auto staIt = acknowledgment->stationsReplyingWithBlockAck.begin();
            m_trigVector = staIt->second.blockAckTxVector;
            while (staIt != acknowledgment->stationsReplyingWithBlockAck.end())
            {
                NS_ASSERT(m_apMac);
                uint16_t staId = m_apMac->GetAssociationId(staIt->first, m_linkId);

                m_trigVector.SetHeMuUserInfo(staId,
                                             staIt->second.blockAckTxVector.GetHeMuUserInfo(staId));
                recipients.emplace(staId, staIt->second.barHeader);

                staIt++;
            }
            // set the Length field of the response TXVECTOR, which is needed to correctly
            // set the UL Length field of the MU-BAR Trigger Frame
            m_trigVector.SetLength(acknowledgment->ulLength);

            m_triggerFrame = PrepareMuBar(m_trigVector, recipients);
        }
        else
        {
            // we are transmitting the MU-BAR following the DL MU PPDU after a SIFS.
            // m_psduMap and m_txParams are still the same as when the DL MU PPDU was sent.
            // record the set of stations expected to send a BlockAck frame
            for (auto& station : acknowledgment->stationsReplyingWithBlockAck)
            {
                staExpectResponseFrom.insert(station.first);
            }

            Ptr<WifiPsdu> triggerPsdu = GetWifiPsdu(m_triggerFrame, acknowledgment->muBarTxVector);
            Time txDuration = WifiPhy::CalculateTxDuration(triggerPsdu->GetSize(),
                                                           acknowledgment->muBarTxVector,
                                                           m_phy->GetPhyBand());
            // update acknowledgmentTime to correctly set the Duration/ID
            *acknowledgment->acknowledgmentTime -= (m_phy->GetSifs() + txDuration);
            m_triggerFrame->GetHeader().SetDuration(GetPsduDurationId(txDuration, m_txParams));

            responseTxVector =
                &acknowledgment->stationsReplyingWithBlockAck.begin()->second.blockAckTxVector;
            Time timeout = txDuration + m_phy->GetSifs() + m_phy->GetSlot() +
                           WifiPhy::CalculatePhyPreambleAndHeaderDuration(*responseTxVector);

            m_txTimer.Set(WifiTxTimer::WAIT_BLOCK_ACKS_IN_TB_PPDU,
                          timeout,
                          staExpectResponseFrom,
                          &HeFrameExchangeManager::BlockAcksInTbPpduTimeout,
                          this,
                          &m_psduMap,
                          staExpectResponseFrom.size());
            m_channelAccessManager->NotifyAckTimeoutStartNow(timeout);

            ForwardPsduDown(triggerPsdu, acknowledgment->muBarTxVector);

            // Pass TRIGVECTOR to HE PHY (equivalent to PHY-TRIGGER.request primitive)
            auto hePhy = std::static_pointer_cast<HePhy>(
                m_phy->GetPhyEntity(responseTxVector->GetModulationClass()));
            hePhy->SetTrigVector(m_trigVector, timeout);

            return;
        }
    }
    /*
     * Acknowledgment requested by MU-BAR TFs aggregated to PSDUs in the DL MU PPDU
     */
    else if (m_txParams.m_acknowledgment->method == WifiAcknowledgment::DL_MU_AGGREGATE_TF)
    {
        auto acknowledgment = static_cast<WifiDlMuAggregateTf*>(m_txParams.m_acknowledgment.get());

        m_trigVector =
            acknowledgment->stationsReplyingWithBlockAck.begin()->second.blockAckTxVector;

        // record the set of stations expected to send a BlockAck frame
        for (auto& station : acknowledgment->stationsReplyingWithBlockAck)
        {
            staExpectResponseFrom.insert(station.first);
            // check that the station that is expected to send a BlockAck frame is
            // actually the receiver of a PSDU
            auto psduMapIt = std::find_if(m_psduMap.begin(),
                                          m_psduMap.end(),
                                          [&station](std::pair<uint16_t, Ptr<WifiPsdu>> psdu) {
                                              return psdu.second->GetAddr1() == station.first;
                                          });

            NS_ASSERT(psduMapIt != m_psduMap.end());
            // add a MU-BAR Trigger Frame to the PSDU
            std::vector<Ptr<WifiMpdu>> mpduList(psduMapIt->second->begin(),
                                                psduMapIt->second->end());
            NS_ASSERT(mpduList.size() == psduMapIt->second->GetNMpdus());
            // set the Length field of the response TXVECTOR, which is needed to correctly
            // set the UL Length field of the MU-BAR Trigger Frame
            station.second.blockAckTxVector.SetLength(acknowledgment->ulLength);
            mpduList.push_back(PrepareMuBar(station.second.blockAckTxVector,
                                            {{psduMapIt->first, station.second.barHeader}}));
            psduMapIt->second = Create<WifiPsdu>(std::move(mpduList));
            m_trigVector.SetHeMuUserInfo(
                psduMapIt->first,
                station.second.blockAckTxVector.GetHeMuUserInfo(psduMapIt->first));
        }

        timerType = WifiTxTimer::WAIT_BLOCK_ACKS_IN_TB_PPDU;
        responseTxVector =
            &acknowledgment->stationsReplyingWithBlockAck.begin()->second.blockAckTxVector;
        m_trigVector.SetLength(acknowledgment->ulLength);
    }
    /*
     * Basic Trigger Frame starting an UL MU transmission
     */
    else if (m_txParams.m_acknowledgment->method == WifiAcknowledgment::UL_MU_MULTI_STA_BA)
    {
        // the PSDU map being sent must contain a (Basic) Trigger Frame
        NS_ASSERT(IsTrigger(m_psduMap));
        mpdu = *m_psduMap.begin()->second->begin();

        auto acknowledgment = static_cast<WifiUlMuMultiStaBa*>(m_txParams.m_acknowledgment.get());

        // record the set of stations solicited by this Trigger Frame
        for (const auto& station : acknowledgment->stationsReceivingMultiStaBa)
        {
            staExpectResponseFrom.insert(station.first.first);
        }

        // Reset stationsReceivingMultiStaBa, which will be filled as soon as
        // TB PPDUs are received
        acknowledgment->stationsReceivingMultiStaBa.clear();
        acknowledgment->baType.m_bitmapLen.clear();

        timerType = WifiTxTimer::WAIT_TB_PPDU_AFTER_BASIC_TF;
        responseTxVector = &acknowledgment->tbPpduTxVector;
        m_trigVector = GetTrigVector(m_muScheduler->GetUlMuInfo(m_linkId).trigger);
    }
    /*
     * BSRP Trigger Frame
     */
    else if (m_txParams.m_acknowledgment->method == WifiAcknowledgment::NONE &&
             !m_txParams.m_txVector.IsUlMu() && IsTrigger(m_psduMap))
    {
        CtrlTriggerHeader& trigger = m_muScheduler->GetUlMuInfo(m_linkId).trigger;
        NS_ASSERT(trigger.IsBsrp());
        NS_ASSERT(m_apMac);

        // record the set of stations solicited by this Trigger Frame
        for (const auto& userInfo : trigger)
        {
            auto staIt = m_apMac->GetStaList(m_linkId).find(userInfo.GetAid12());
            NS_ASSERT(staIt != m_apMac->GetStaList(m_linkId).end());
            staExpectResponseFrom.insert(staIt->second);
        }

        timerType = WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF;
        txVector = trigger.GetHeTbTxVector(trigger.begin()->GetAid12());
        responseTxVector = &txVector;
        m_trigVector = GetTrigVector(m_muScheduler->GetUlMuInfo(m_linkId).trigger);
    }
    /*
     * TB PPDU solicited by a Basic Trigger Frame
     */
    else if (m_txParams.m_txVector.IsUlMu() &&
             m_txParams.m_acknowledgment->method == WifiAcknowledgment::ACK_AFTER_TB_PPDU)
    {
        NS_ASSERT(m_psduMap.size() == 1);
        timerType = WifiTxTimer::WAIT_BLOCK_ACK_AFTER_TB_PPDU;
        NS_ASSERT(m_staMac && m_staMac->IsAssociated());
        auto recv = m_psduMap.begin()->second->GetAddr1();
        txVector = GetWifiRemoteStationManager()->GetBlockAckTxVector(recv, m_txParams.m_txVector);
        responseTxVector = &txVector;
        staExpectResponseFrom.insert(recv);
    }
    /*
     * QoS Null frames solicited by a BSRP Trigger Frame
     */
    else if (m_txParams.m_txVector.IsUlMu() &&
             m_txParams.m_acknowledgment->method == WifiAcknowledgment::NONE)
    {
        // No response is expected, so do nothing.
    }
    else
    {
        NS_ABORT_MSG("Unable to handle the selected acknowledgment method ("
                     << m_txParams.m_acknowledgment.get() << ")");
    }

    // create a map of Ptr<const WifiPsdu>, as required by the PHY
    WifiConstPsduMap psduMap;
    for (const auto& psdu : m_psduMap)
    {
        psduMap.emplace(psdu.first, psdu.second);
    }

    Time txDuration;
    if (m_txParams.m_txVector.IsUlMu())
    {
        txDuration = HePhy::ConvertLSigLengthToHeTbPpduDuration(m_txParams.m_txVector.GetLength(),
                                                                m_txParams.m_txVector,
                                                                m_phy->GetPhyBand());
    }
    else
    {
        txDuration =
            WifiPhy::CalculateTxDuration(psduMap, m_txParams.m_txVector, m_phy->GetPhyBand());

        // Set Duration/ID
        Time durationId = GetPsduDurationId(txDuration, m_txParams);

        if (m_continueTxopAfterBsrpTf && m_edca && m_edca->GetTxopLimit(m_linkId).IsZero() &&
            timerType == WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF)
        {
            // add the duration of the following frame exchange to extend the NAV beyond the
            // responses to the BSRP TF
            durationId += m_muScheduler->GetExtraTimeForBsrpTfDurationId(m_linkId);
        }

        for (auto& psdu : m_psduMap)
        {
            psdu.second->SetDuration(durationId);
        }
    }

    if (timerType == WifiTxTimer::NOT_RUNNING)
    {
        if (m_triggerFrame)
        {
            NS_LOG_DEBUG("Scheduling MU-BAR " << *m_triggerFrame);
            Simulator::Schedule(txDuration + m_phy->GetSifs(),
                                &HeFrameExchangeManager::SendPsduMap,
                                this);
        }
        else if (!m_txParams.m_txVector.IsUlMu())
        {
            Simulator::Schedule(txDuration, &HeFrameExchangeManager::TransmissionSucceeded, this);
        }
    }
    else
    {
        Time timeout = txDuration + m_phy->GetSifs() + m_phy->GetSlot() +
                       WifiPhy::CalculatePhyPreambleAndHeaderDuration(*responseTxVector);
        m_channelAccessManager->NotifyAckTimeoutStartNow(timeout);

        // start timer
        switch (timerType)
        {
        case WifiTxTimer::WAIT_NORMAL_ACK_AFTER_DL_MU_PPDU:
            NS_ASSERT(mpdu);
            m_txTimer.Set(timerType,
                          timeout,
                          staExpectResponseFrom,
                          &HeFrameExchangeManager::NormalAckTimeout,
                          this,
                          mpdu,
                          m_txParams.m_txVector);
            break;
        case WifiTxTimer::WAIT_BLOCK_ACK:
            NS_ASSERT(psdu);
            m_txTimer.Set(timerType,
                          timeout,
                          staExpectResponseFrom,
                          &HeFrameExchangeManager::BlockAckTimeout,
                          this,
                          psdu,
                          m_txParams.m_txVector);
            break;
        case WifiTxTimer::WAIT_BLOCK_ACKS_IN_TB_PPDU:
            m_txTimer.Set(timerType,
                          timeout,
                          staExpectResponseFrom,
                          &HeFrameExchangeManager::BlockAcksInTbPpduTimeout,
                          this,
                          &m_psduMap,
                          staExpectResponseFrom.size());
            break;
        case WifiTxTimer::WAIT_TB_PPDU_AFTER_BASIC_TF:
        case WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF:
            m_txTimer.Set(timerType,
                          timeout,
                          staExpectResponseFrom,
                          &HeFrameExchangeManager::TbPpduTimeout,
                          this,
                          &m_psduMap,
                          staExpectResponseFrom.size());
            break;
        case WifiTxTimer::WAIT_BLOCK_ACK_AFTER_TB_PPDU:
            m_txTimer.Set(timerType,
                          timeout,
                          staExpectResponseFrom,
                          &HeFrameExchangeManager::BlockAckAfterTbPpduTimeout,
                          this,
                          m_psduMap.begin()->second,
                          m_txParams.m_txVector);
            break;
        default:
            NS_ABORT_MSG("Unknown timer type: " << timerType);
            break;
        }
    }

    // transmit the map of PSDUs
    ForwardPsduMapDown(psduMap, m_txParams.m_txVector);

    if (timerType == WifiTxTimer::WAIT_BLOCK_ACKS_IN_TB_PPDU ||
        timerType == WifiTxTimer::WAIT_TB_PPDU_AFTER_BASIC_TF ||
        timerType == WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF)
    {
        // Pass TRIGVECTOR to HE PHY (equivalent to PHY-TRIGGER.request primitive)
        auto hePhy = std::static_pointer_cast<HePhy>(
            m_phy->GetPhyEntity(responseTxVector->GetModulationClass()));
        hePhy->SetTrigVector(m_trigVector, m_txTimer.GetDelayLeft());
    }
    else if (timerType == WifiTxTimer::NOT_RUNNING &&
             (m_txParams.m_txVector.IsUlMu() ||
              m_txParams.m_acknowledgment->method == WifiAcknowledgment::DL_MU_BAR_BA_SEQUENCE))
    {
        // clear m_psduMap after sending QoS Null frames following a BSRP Trigger Frame or after
        // sending a DL MU PPDU with BAR-BA ack sequence and no immediate response is expected
        Simulator::Schedule(txDuration, &WifiPsduMap::clear, &m_psduMap);
    }

    if (m_txTimer.IsRunning() && timerType != WifiTxTimer::WAIT_BLOCK_ACK_AFTER_TB_PPDU)
    {
        NS_ASSERT(m_sentFrameTo.empty());

        // do not record that a frame is being sent to an EMLSR client unless the AP is sending a
        // BSRP TF or the EMLSR client is already protected
        for (const auto& address : staExpectResponseFrom)
        {
            if (!GetWifiRemoteStationManager()->GetEmlsrEnabled(address) ||
                timerType == WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF ||
                m_protectedStas.contains(address))
            {
                m_sentFrameTo.insert(address);
            }
        }
    }
}

void
HeFrameExchangeManager::ForwardPsduMapDown(WifiConstPsduMap psduMap, WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << psduMap << txVector);

    if (ns3::IsDlMu(txVector.GetPreambleType()))
    {
        auto hePhy =
            std::static_pointer_cast<HePhy>(m_phy->GetPhyEntity(txVector.GetModulationClass()));
        auto sigBMode = hePhy->GetSigBMode(txVector);
        txVector.SetSigBMode(sigBMode);
    }

    for (const auto& psdu : psduMap)
    {
        NS_LOG_DEBUG("Transmitting: [STAID=" << psdu.first << ", " << *psdu.second << "]");
    }
    NS_LOG_DEBUG("TXVECTOR: " << txVector);
    for (const auto& [staId, psdu] : psduMap)
    {
        FinalizeMacHeader(psdu);
        NotifyTxToEdca(psdu);
    }
    m_allowedWidth = std::min(m_allowedWidth, txVector.GetChannelWidth());

    if (psduMap.size() > 1 || psduMap.begin()->second->IsAggregate() ||
        psduMap.begin()->second->IsSingle())
    {
        txVector.SetAggregation(true);
    }

    const auto txDuration = WifiPhy::CalculateTxDuration(psduMap, txVector, m_phy->GetPhyBand());
    SetTxNav(*psduMap.cbegin()->second->begin(), txDuration);

    m_phy->Send(psduMap, txVector);
}

Ptr<WifiMpdu>
HeFrameExchangeManager::PrepareMuBar(const WifiTxVector& responseTxVector,
                                     std::map<uint16_t, CtrlBAckRequestHeader> recipients) const
{
    NS_LOG_FUNCTION(this << responseTxVector);
    NS_ASSERT(responseTxVector.GetHeMuUserInfoMap().size() == recipients.size());
    NS_ASSERT(!recipients.empty());

    CtrlTriggerHeader muBar(TriggerFrameType::MU_BAR_TRIGGER, responseTxVector);
    SetTargetRssi(muBar);
    // Set the CS Required subfield to true, unless the UL Length subfield is less
    // than or equal to 418 (see Section 26.5.2.5 of 802.11ax-2021)
    muBar.SetCsRequired(muBar.GetUlLength() > 418);

    // Add the Trigger Dependent User Info subfield to every User Info field
    for (auto& userInfo : muBar)
    {
        auto recipientIt = recipients.find(userInfo.GetAid12());
        NS_ASSERT(recipientIt != recipients.end());

        // Store the BAR in the Trigger Dependent User Info subfield
        userInfo.SetMuBarTriggerDepUserInfo(recipientIt->second);
    }

    Ptr<Packet> bar = Create<Packet>();
    bar->AddHeader(muBar);
    Mac48Address rxAddress;
    // "If the Trigger frame has one User Info field and the AID12 subfield of the
    // User Info contains the AID of a STA, then the RA field is set to the address
    // of that STA". Otherwise, it is set to the broadcast address (Sec. 9.3.1.23 -
    // 802.11ax amendment draft 3.0)
    if (muBar.GetNUserInfoFields() > 1)
    {
        rxAddress = Mac48Address::GetBroadcast();
    }
    else
    {
        NS_ASSERT(m_apMac);
        rxAddress = m_apMac->GetStaList(m_linkId).at(recipients.begin()->first);
    }

    WifiMacHeader hdr;
    hdr.SetType(WIFI_MAC_CTL_TRIGGER);
    hdr.SetAddr1(rxAddress);
    hdr.SetAddr2(m_self);
    hdr.SetDsNotTo();
    hdr.SetDsNotFrom();
    hdr.SetNoRetry();
    hdr.SetNoMoreFragments();

    return Create<WifiMpdu>(bar, hdr);
}

void
HeFrameExchangeManager::CalculateProtectionTime(WifiProtection* protection) const
{
    NS_LOG_FUNCTION(this << protection);
    NS_ASSERT(protection != nullptr);

    if (protection->method == WifiProtection::MU_RTS_CTS)
    {
        auto muRtsCtsProtection = static_cast<WifiMuRtsCtsProtection*>(protection);

        // Get the TXVECTOR used by one station to send the CTS response. This is used
        // to compute the TX duration, so it does not matter which station we choose
        WifiTxVector ctsTxVector =
            GetCtsTxVectorAfterMuRts(muRtsCtsProtection->muRts,
                                     muRtsCtsProtection->muRts.begin()->GetAid12());

        uint32_t muRtsSize = WifiMacHeader(WIFI_MAC_CTL_TRIGGER).GetSize() +
                             muRtsCtsProtection->muRts.GetSerializedSize() + WIFI_MAC_FCS_LENGTH;
        muRtsCtsProtection->protectionTime =
            WifiPhy::CalculateTxDuration(muRtsSize,
                                         muRtsCtsProtection->muRtsTxVector,
                                         m_phy->GetPhyBand()) +
            WifiPhy::CalculateTxDuration(GetCtsSize(), ctsTxVector, m_phy->GetPhyBand()) +
            2 * m_phy->GetSifs();
    }
    else
    {
        VhtFrameExchangeManager::CalculateProtectionTime(protection);
    }
}

void
HeFrameExchangeManager::CalculateAcknowledgmentTime(WifiAcknowledgment* acknowledgment) const
{
    NS_LOG_FUNCTION(this << acknowledgment);
    NS_ASSERT(acknowledgment);

    /*
     * Acknowledgment via a sequence of BlockAckReq and BlockAck frames
     */
    if (acknowledgment->method == WifiAcknowledgment::DL_MU_BAR_BA_SEQUENCE)
    {
        auto dlMuBarBaAcknowledgment = static_cast<WifiDlMuBarBaSequence*>(acknowledgment);

        Time duration;

        // normal ack or implicit BAR policy can be used for (no more than) one receiver
        NS_ABORT_IF(dlMuBarBaAcknowledgment->stationsReplyingWithNormalAck.size() +
                        dlMuBarBaAcknowledgment->stationsReplyingWithBlockAck.size() >
                    1);

        if (!dlMuBarBaAcknowledgment->stationsReplyingWithNormalAck.empty())
        {
            const auto& info =
                dlMuBarBaAcknowledgment->stationsReplyingWithNormalAck.begin()->second;
            duration +=
                m_phy->GetSifs() +
                WifiPhy::CalculateTxDuration(GetAckSize(), info.ackTxVector, m_phy->GetPhyBand());
        }

        if (!dlMuBarBaAcknowledgment->stationsReplyingWithBlockAck.empty())
        {
            const auto& info =
                dlMuBarBaAcknowledgment->stationsReplyingWithBlockAck.begin()->second;
            duration +=
                m_phy->GetSifs() + WifiPhy::CalculateTxDuration(GetBlockAckSize(info.baType),
                                                                info.blockAckTxVector,
                                                                m_phy->GetPhyBand());
        }

        for (const auto& stations : dlMuBarBaAcknowledgment->stationsSendBlockAckReqTo)
        {
            const auto& info = stations.second;
            duration += m_phy->GetSifs() +
                        WifiPhy::CalculateTxDuration(GetBlockAckRequestSize(info.barType),
                                                     info.blockAckReqTxVector,
                                                     m_phy->GetPhyBand()) +
                        m_phy->GetSifs() +
                        WifiPhy::CalculateTxDuration(GetBlockAckSize(info.baType),
                                                     info.blockAckTxVector,
                                                     m_phy->GetPhyBand());
        }

        dlMuBarBaAcknowledgment->acknowledgmentTime = duration;
    }
    /*
     * Acknowledgment via a MU-BAR Trigger Frame sent as single user frame
     */
    else if (acknowledgment->method == WifiAcknowledgment::DL_MU_TF_MU_BAR)
    {
        auto dlMuTfMuBarAcknowledgment = static_cast<WifiDlMuTfMuBar*>(acknowledgment);

        Time duration;

        for (const auto& stations : dlMuTfMuBarAcknowledgment->stationsReplyingWithBlockAck)
        {
            // compute the TX duration of the BlockAck response from this receiver.
            const auto& info = stations.second;
            NS_ASSERT(info.blockAckTxVector.GetHeMuUserInfoMap().size() == 1);
            uint16_t staId = info.blockAckTxVector.GetHeMuUserInfoMap().begin()->first;
            Time currBlockAckDuration = WifiPhy::CalculateTxDuration(GetBlockAckSize(info.baType),
                                                                     info.blockAckTxVector,
                                                                     m_phy->GetPhyBand(),
                                                                     staId);
            // update the max duration among all the Block Ack responses
            if (currBlockAckDuration > duration)
            {
                duration = currBlockAckDuration;
            }
        }

        // The computed duration may not be coded exactly in the L-SIG length, hence determine
        // the exact duration corresponding to the value that will be coded in this field.
        WifiTxVector& txVector = dlMuTfMuBarAcknowledgment->stationsReplyingWithBlockAck.begin()
                                     ->second.blockAckTxVector;
        std::tie(dlMuTfMuBarAcknowledgment->ulLength, duration) =
            HePhy::ConvertHeTbPpduDurationToLSigLength(duration, txVector, m_phy->GetPhyBand());

        const auto muBarVariant = (txVector.GetModulationClass() == WIFI_MOD_CLASS_HE)
                                      ? TriggerFrameVariant::HE
                                      : TriggerFrameVariant::EHT;
        uint32_t muBarSize =
            GetMuBarSize(muBarVariant,
                         dlMuTfMuBarAcknowledgment->muBarTxVector.GetChannelWidth(),
                         dlMuTfMuBarAcknowledgment->barTypes);
        if (dlMuTfMuBarAcknowledgment->muBarTxVector.GetModulationClass() >= WIFI_MOD_CLASS_VHT)
        {
            // MU-BAR TF will be sent as an S-MPDU
            muBarSize = MpduAggregator::GetSizeIfAggregated(muBarSize, 0);
        }
        dlMuTfMuBarAcknowledgment->acknowledgmentTime =
            m_phy->GetSifs() +
            WifiPhy::CalculateTxDuration(muBarSize,
                                         dlMuTfMuBarAcknowledgment->muBarTxVector,
                                         m_phy->GetPhyBand()) +
            m_phy->GetSifs() + duration;
    }
    /*
     * Acknowledgment requested by MU-BAR TFs aggregated to PSDUs in the DL MU PPDU
     */
    else if (acknowledgment->method == WifiAcknowledgment::DL_MU_AGGREGATE_TF)
    {
        auto dlMuAggrTfAcknowledgment = static_cast<WifiDlMuAggregateTf*>(acknowledgment);

        Time duration;

        for (const auto& stations : dlMuAggrTfAcknowledgment->stationsReplyingWithBlockAck)
        {
            // compute the TX duration of the BlockAck response from this receiver.
            const auto& info = stations.second;
            NS_ASSERT(info.blockAckTxVector.GetHeMuUserInfoMap().size() == 1);
            uint16_t staId = info.blockAckTxVector.GetHeMuUserInfoMap().begin()->first;
            Time currBlockAckDuration = WifiPhy::CalculateTxDuration(GetBlockAckSize(info.baType),
                                                                     info.blockAckTxVector,
                                                                     m_phy->GetPhyBand(),
                                                                     staId);
            // update the max duration among all the Block Ack responses
            if (currBlockAckDuration > duration)
            {
                duration = currBlockAckDuration;
            }
        }

        // The computed duration may not be coded exactly in the L-SIG length, hence determine
        // the exact duration corresponding to the value that will be coded in this field.
        WifiTxVector& txVector =
            dlMuAggrTfAcknowledgment->stationsReplyingWithBlockAck.begin()->second.blockAckTxVector;
        std::tie(dlMuAggrTfAcknowledgment->ulLength, duration) =
            HePhy::ConvertHeTbPpduDurationToLSigLength(duration, txVector, m_phy->GetPhyBand());
        dlMuAggrTfAcknowledgment->acknowledgmentTime = m_phy->GetSifs() + duration;
    }
    /*
     * Basic Trigger Frame starting an UL MU transmission
     */
    else if (acknowledgment->method == WifiAcknowledgment::UL_MU_MULTI_STA_BA)
    {
        auto ulMuMultiStaBa = static_cast<WifiUlMuMultiStaBa*>(acknowledgment);

        Time duration = WifiPhy::CalculateTxDuration(GetBlockAckSize(ulMuMultiStaBa->baType),
                                                     ulMuMultiStaBa->multiStaBaTxVector,
                                                     m_phy->GetPhyBand());
        ulMuMultiStaBa->acknowledgmentTime = m_phy->GetSifs() + duration;
    }
    /*
     * TB PPDU solicired by a Basic or BSRP Trigger Frame
     */
    else if (acknowledgment->method == WifiAcknowledgment::ACK_AFTER_TB_PPDU)
    {
        // The station solicited by the Trigger Frame does not have to account
        // for the actual acknowledgment time since it is given the PPDU duration
        // through the Trigger Frame
        acknowledgment->acknowledgmentTime = Seconds(0);
    }
    else
    {
        VhtFrameExchangeManager::CalculateAcknowledgmentTime(acknowledgment);
    }
}

WifiMode
HeFrameExchangeManager::GetCtsModeAfterMuRts() const
{
    // The CTS frame sent in response to an MU-RTS Trigger frame shall be carried in a non-HT or
    // non-HT duplicate PPDU (see Clause 17) with a 6 Mb/s rate (Sec. 26.2.6.3 of 802.11ax-2021)
    return m_phy->GetPhyBand() == WIFI_PHY_BAND_2_4GHZ ? ErpOfdmPhy::GetErpOfdmRate6Mbps()
                                                       : OfdmPhy::GetOfdmRate6Mbps();
}

WifiTxVector
HeFrameExchangeManager::GetCtsTxVectorAfterMuRts(const CtrlTriggerHeader& trigger,
                                                 uint16_t staId) const
{
    NS_LOG_FUNCTION(this << trigger << staId);

    auto userInfoIt = trigger.FindUserInfoWithAid(staId);
    NS_ASSERT_MSG(userInfoIt != trigger.end(), "User Info field for AID=" << staId << " not found");
    MHz_u bw{0};

    if (uint8_t ru = userInfoIt->GetMuRtsRuAllocation(); ru < 65)
    {
        bw = MHz_u{20};
    }
    else if (ru < 67)
    {
        bw = MHz_u{40};
    }
    else if (ru == 67)
    {
        bw = MHz_u{80};
    }
    else if (ru == 68)
    {
        bw = MHz_u{160};
    }
    else
    {
        NS_ASSERT(ru == 69);
        bw = MHz_u{320};
    }

    auto txVector = GetWifiRemoteStationManager()->GetCtsTxVector(m_bssid, GetCtsModeAfterMuRts());
    // set the channel width of the CTS TXVECTOR according to the allocated RU
    txVector.SetChannelWidth(bw);

    return txVector;
}

Time
HeFrameExchangeManager::GetTxDuration(uint32_t ppduPayloadSize,
                                      Mac48Address receiver,
                                      const WifiTxParameters& txParams) const
{
    if (!txParams.m_txVector.IsMu())
    {
        return VhtFrameExchangeManager::GetTxDuration(ppduPayloadSize, receiver, txParams);
    }

    NS_ASSERT_MSG(!txParams.m_txVector.IsDlMu() || m_apMac, "DL MU can be done by an AP");
    NS_ASSERT_MSG(!txParams.m_txVector.IsUlMu() || m_staMac, "UL MU can be done by a STA");

    if (txParams.m_acknowledgment &&
        txParams.m_acknowledgment->method == WifiAcknowledgment::DL_MU_AGGREGATE_TF)
    {
        // we need to account for the size of the aggregated MU-BAR Trigger Frame
        auto psduInfo = txParams.GetPsduInfo(receiver);
        NS_ASSERT_MSG(psduInfo, "No information for " << receiver << " in TX params");
        NS_ASSERT_MSG(!psduInfo->seqNumbers.empty(), "No sequence number for " << receiver);
        const auto tid = psduInfo->seqNumbers.cbegin()->first;

        const auto muBarVariant = (txParams.m_txVector.GetModulationClass() == WIFI_MOD_CLASS_HE)
                                      ? TriggerFrameVariant::HE
                                      : TriggerFrameVariant::EHT;
        ppduPayloadSize = MpduAggregator::GetSizeIfAggregated(
            GetMuBarSize(muBarVariant,
                         txParams.m_txVector.GetChannelWidth(),
                         {m_mac->GetBarTypeAsOriginator(receiver, tid)}),
            ppduPayloadSize);
    }

    uint16_t staId = (txParams.m_txVector.IsDlMu() ? m_apMac->GetAssociationId(receiver, m_linkId)
                                                   : m_staMac->GetAssociationId());
    Time psduDuration = WifiPhy::CalculateTxDuration(ppduPayloadSize,
                                                     txParams.m_txVector,
                                                     m_phy->GetPhyBand(),
                                                     staId);

    return txParams.m_txDuration ? std::max(psduDuration, *txParams.m_txDuration) : psduDuration;
}

void
HeFrameExchangeManager::TbPpduTimeout(WifiPsduMap* psduMap, std::size_t nSolicitedStations)
{
    NS_LOG_FUNCTION(this << psduMap << nSolicitedStations);
    DoTbPpduTimeout(psduMap, nSolicitedStations, true);
}

void
HeFrameExchangeManager::DoTbPpduTimeout(WifiPsduMap* psduMap,
                                        std::size_t nSolicitedStations,
                                        bool updateFailedCw)
{
    const auto& staMissedTbPpduFrom = m_txTimer.GetStasExpectedToRespond();
    NS_LOG_FUNCTION(this << psduMap << staMissedTbPpduFrom.size() << nSolicitedStations
                         << updateFailedCw);

    NS_ASSERT(psduMap);
    NS_ASSERT(IsTrigger(*psduMap));

    // This method is called if some station(s) did not send a TB PPDU
    NS_ASSERT(!staMissedTbPpduFrom.empty());
    NS_ASSERT(m_edca);

    if (staMissedTbPpduFrom.size() == nSolicitedStations)
    {
        // no station replied, the transmission failed
        CtrlTriggerHeader trigger;
        psduMap->cbegin()->second->GetPayload(0)->PeekHeader(trigger);

        if (m_continueTxopAfterBsrpTf && m_edca->GetTxopLimit(m_linkId).IsZero() &&
            trigger.IsBsrp())
        {
            SendCfEndIfNeeded();
        }

        TransmissionFailed(!updateFailedCw);
    }
    else if (!m_multiStaBaEvent.IsPending())
    {
        m_edca->ResetCw(m_linkId);
        TransmissionSucceeded();
    }
    else
    {
        // Stations that did not respond must be removed from the set of stations for which
        // protection is not needed in the current TXOP.
        for (const auto& address : staMissedTbPpduFrom)
        {
            NS_LOG_DEBUG(address << " did not respond, hence it is no longer protected");
            m_protectedStas.erase(address);
            m_sentFrameTo.erase(address);
        }
        if (m_protectedIfResponded)
        {
            m_protectedStas.merge(m_sentFrameTo);
        }
        m_sentFrameTo.clear();
    }

    m_psduMap.clear();
}

void
HeFrameExchangeManager::BlockAcksInTbPpduTimeout(WifiPsduMap* psduMap,
                                                 std::size_t nSolicitedStations)
{
    NS_LOG_FUNCTION(this << psduMap << nSolicitedStations);

    NS_ASSERT(psduMap);
    NS_ASSERT(m_txParams.m_acknowledgment &&
              (m_txParams.m_acknowledgment->method == WifiAcknowledgment::DL_MU_AGGREGATE_TF ||
               m_txParams.m_acknowledgment->method == WifiAcknowledgment::DL_MU_TF_MU_BAR));

    // This method is called if some station(s) did not send a BlockAck frame in a TB PPDU
    const auto& staMissedBlockAckFrom = m_txTimer.GetStasExpectedToRespond();
    NS_ASSERT(!staMissedBlockAckFrom.empty());

    if (staMissedBlockAckFrom.size() == nSolicitedStations)
    {
        // no station replied, the transmission failed
        GetWifiRemoteStationManager()->ReportDataFailed(*psduMap->begin()->second->begin());
    }

    if (m_triggerFrame)
    {
        // this is strictly needed for DL_MU_TF_MU_BAR only
        m_triggerFrame = nullptr;
    }

    for (const auto& sta : staMissedBlockAckFrom)
    {
        auto psdu = GetPsduTo(sta, *psduMap);
        NS_ASSERT(psdu);
        MissedBlockAck(psdu, m_txParams.m_txVector);
    }

    NS_ASSERT(m_edca);

    if (staMissedBlockAckFrom.size() == nSolicitedStations)
    {
        // no station replied, the transmission failed
        TransmissionFailed();
    }
    else
    {
        m_edca->ResetCw(m_linkId);
        TransmissionSucceeded();
    }
    m_psduMap.clear();
}

void
HeFrameExchangeManager::BlockAckAfterTbPpduTimeout(Ptr<WifiPsdu> psdu, const WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << *psdu << txVector);

    GetWifiRemoteStationManager()->ReportDataFailed(*psdu->begin());

    MissedBlockAck(psdu, m_txParams.m_txVector);

    // This is a PSDU sent in a TB PPDU. An HE STA resumes the EDCA backoff procedure
    // without modifying CW or the backoff counter for the associated EDCAF, after
    // transmission of an MPDU in a TB PPDU regardless of whether the STA has received
    // the corresponding acknowledgment frame in response to the MPDU sent in the TB PPDU
    // (Sec. 10.22.2.2 of 11ax Draft 3.0)
    m_psduMap.clear();
}

void
HeFrameExchangeManager::NormalAckTimeout(Ptr<WifiMpdu> mpdu, const WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << *mpdu << txVector);

    VhtFrameExchangeManager::NormalAckTimeout(mpdu, txVector);

    // If a Normal Ack is missed in response to a DL MU PPDU requiring acknowledgment
    // in SU format, we have to set the Retry flag for all transmitted MPDUs that have
    // not been acknowledged nor discarded and clear m_psduMap since the transmission failed.
    for (auto& psdu : m_psduMap)
    {
        for (auto& mpdu : *PeekPointer(psdu.second))
        {
            if (mpdu->IsQueued())
            {
                m_mac->GetTxopQueue(mpdu->GetQueueAc())->GetOriginal(mpdu)->GetHeader().SetRetry();
                mpdu->ResetInFlight(m_linkId);
            }
        }
    }
    m_psduMap.clear();
}

void
HeFrameExchangeManager::BlockAckTimeout(Ptr<WifiPsdu> psdu, const WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << *psdu << txVector);

    VhtFrameExchangeManager::BlockAckTimeout(psdu, txVector);

    // If a Block Ack is missed in response to a DL MU PPDU requiring acknowledgment
    // in SU format, we have to set the Retry flag for all transmitted MPDUs that have
    // not been acknowledged nor discarded and clear m_psduMap since the transmission failed.
    for (auto& psdu : m_psduMap)
    {
        for (auto& mpdu : *PeekPointer(psdu.second))
        {
            if (mpdu->IsQueued())
            {
                mpdu->GetHeader().SetRetry();
            }
        }
    }
    m_psduMap.clear();
}

WifiTxVector
HeFrameExchangeManager::GetTrigVector(const CtrlTriggerHeader& trigger) const
{
    WifiTxVector v;
    v.SetPreambleType(trigger.GetVariant() == TriggerFrameVariant::HE ? WIFI_PREAMBLE_HE_TB
                                                                      : WIFI_PREAMBLE_EHT_TB);
    v.SetChannelWidth(trigger.GetUlBandwidth());
    v.SetGuardInterval(trigger.GetGuardInterval());
    v.SetLength(trigger.GetUlLength());
    for (const auto& userInfoField : trigger)
    {
        v.SetHeMuUserInfo(
            userInfoField.GetAid12(),
            {userInfoField.GetRuAllocation(), userInfoField.GetUlMcs(), userInfoField.GetNss()});
    }
    return v;
}

WifiTxVector
HeFrameExchangeManager::GetHeTbTxVector(CtrlTriggerHeader trigger, Mac48Address triggerSender) const
{
    NS_ASSERT(triggerSender !=
              m_self); // TxPower information is used only by STAs, it is useless for the sending AP
                       // (which can directly use CtrlTriggerHeader::GetHeTbTxVector)
    NS_ASSERT(m_staMac);
    uint16_t staId = m_staMac->GetAssociationId();
    auto userInfoIt = trigger.FindUserInfoWithAid(staId);
    NS_ASSERT(userInfoIt != trigger.end());

    WifiTxVector v = trigger.GetHeTbTxVector(staId);

    Ptr<HeConfiguration> heConfiguration = m_mac->GetHeConfiguration();
    NS_ASSERT_MSG(heConfiguration, "This STA has to be an HE station to send an HE TB PPDU");
    v.SetBssColor(heConfiguration->m_bssColor);

    if (userInfoIt->IsUlTargetRxPowerMaxTxPower())
    {
        NS_LOG_LOGIC("AP requested using the max transmit power (" << m_phy->GetTxPowerEnd()
                                                                   << " dBm)");
        v.SetTxPowerLevel(m_phy->GetNTxPowerLevels());
        return v;
    }

    uint8_t powerLevel = GetWifiRemoteStationManager()->GetDefaultTxPowerLevel();
    /**
     * Get the transmit power to use for an HE TB PPDU
     * considering:
     * - the transmit power used by the AP to send the Trigger Frame (TF),
     *   obtained from the AP TX Power subfield of the Common Info field
     *   of the TF.
     * - the target uplink RSSI expected by the AP for the triggered HE TB PPDU,
     *   obtained from the UL Target RSSI subfield of the User Info field
     *   of the TF.
     * - the RSSI of the PPDU containing the TF, typically logged by the
     *   WifiRemoteStationManager upon reception of the TF from the AP.
     *
     * It is assumed that path loss is symmetric (i.e. uplink path loss is
     * equivalent to the measured downlink path loss);
     *
     * Refer to section 27.3.14.2 (Power pre-correction) of 802.11ax Draft 4.0 for more details.
     */
    auto optRssi = GetMostRecentRssi(triggerSender);
    NS_ASSERT(optRssi);
    int8_t pathLossDb =
        trigger.GetApTxPower() -
        static_cast<int8_t>(
            *optRssi); // cast RSSI to be on equal footing with AP Tx power information
    auto reqTxPower = dBm_u{static_cast<double>(userInfoIt->GetUlTargetRxPower() + pathLossDb)};

    // Convert the transmit power to a power level
    const auto numPowerLevels = m_phy->GetNTxPowerLevels();
    if (numPowerLevels > 1)
    {
        dBm_u step = (m_phy->GetTxPowerEnd() - m_phy->GetTxPowerStart()) / (numPowerLevels - 1);
        powerLevel = static_cast<uint8_t>(
            ceil((reqTxPower - m_phy->GetTxPowerStart()) /
                 step)); // better be slightly above so as to satisfy target UL RSSI
        if (powerLevel > numPowerLevels)
        {
            powerLevel = numPowerLevels; // capping will trigger warning below
        }
    }
    if (reqTxPower > m_phy->GetPower(powerLevel))
    {
        NS_LOG_WARN("The requested power level (" << reqTxPower << "dBm) cannot be satisfied (max: "
                                                  << m_phy->GetTxPowerEnd() << "dBm)");
    }
    v.SetTxPowerLevel(powerLevel);
    NS_LOG_LOGIC("UL power control: input "
                 << "{pathLoss=" << pathLossDb << "dB, reqTxPower=" << reqTxPower << "dBm}"
                 << " output "
                 << "{powerLevel=" << +powerLevel << " -> " << m_phy->GetPower(powerLevel) << "dBm}"
                 << " PHY power capa "
                 << "{min=" << m_phy->GetTxPowerStart() << "dBm, max=" << m_phy->GetTxPowerEnd()
                 << "dBm, levels:" << +numPowerLevels << "}");

    return v;
}

std::optional<dBm_u>
HeFrameExchangeManager::GetMostRecentRssi(const Mac48Address& address) const
{
    return GetWifiRemoteStationManager()->GetMostRecentRssi(address);
}

void
HeFrameExchangeManager::SetTargetRssi(CtrlTriggerHeader& trigger) const
{
    NS_LOG_FUNCTION(this);
    NS_ASSERT(m_apMac);

    trigger.SetApTxPower(static_cast<int8_t>(
        m_phy->GetPower(GetWifiRemoteStationManager()->GetDefaultTxPowerLevel())));
    for (auto& userInfo : trigger)
    {
        const auto staList = m_apMac->GetStaList(m_linkId);
        auto itAidAddr = staList.find(userInfo.GetAid12());
        NS_ASSERT(itAidAddr != staList.end());
        auto optRssi = GetMostRecentRssi(itAidAddr->second);
        if (!optRssi.has_value())
        {
            // This might happen after static setup where the AP has not received any
            // frame from the client yet.
            userInfo.SetUlTargetRxPowerMaxTxPower();
            continue;
        }

        NS_ASSERT(optRssi);
        auto rssi = static_cast<int8_t>(*optRssi);
        rssi = (rssi >= -20)
                   ? -20
                   : ((rssi <= -110) ? -110 : rssi); // cap so as to keep within [-110; -20] dBm
        userInfo.SetUlTargetRxPower(rssi);
    }
}

void
HeFrameExchangeManager::PostProcessFrame(Ptr<const WifiPsdu> psdu, const WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << psdu << txVector);

    auto txVectorCopy = txVector;

    if (psdu->GetNMpdus() == 1 && psdu->GetHeader(0).IsTrigger())
    {
        CtrlTriggerHeader trigger;
        psdu->GetPayload(0)->PeekHeader(trigger);
        if (trigger.IsMuRts())
        {
            const WifiMacHeader& muRts = psdu->GetHeader(0);
            // A station receiving an MU-RTS behaves just like as if it received an RTS.
            // Determine whether the MU-RTS is addressed to this station or not and
            // prepare an "equivalent" RTS frame so that we can reuse the UpdateNav()
            // and SetTxopHolder() methods of the parent classes
            WifiMacHeader rts;
            rts.SetType(WIFI_MAC_CTL_RTS);
            rts.SetDsNotFrom();
            rts.SetDsNotTo();
            rts.SetDuration(muRts.GetDuration());
            rts.SetAddr2(muRts.GetAddr2());
            if (m_staMac != nullptr && m_staMac->IsAssociated() &&
                muRts.GetAddr2() == m_bssid // sent by the AP this STA is associated with
                && trigger.FindUserInfoWithAid(m_staMac->GetAssociationId()) != trigger.end())
            {
                // the MU-RTS is addressed to this station
                rts.SetAddr1(m_self);
            }
            else
            {
                rts.SetAddr1(muRts.GetAddr2()); // an address different from that of this station
            }
            psdu = Create<const WifiPsdu>(Create<Packet>(), rts);
            // The duration of the NAV reset timeout has to take into account that the CTS
            // response is sent using the 6 Mbps data rate
            txVectorCopy =
                GetWifiRemoteStationManager()->GetCtsTxVector(m_bssid, GetCtsModeAfterMuRts());
        }
    }
    VhtFrameExchangeManager::PostProcessFrame(psdu, txVectorCopy);
}

void
HeFrameExchangeManager::SendCtsAfterMuRts(const WifiMacHeader& muRtsHdr,
                                          const CtrlTriggerHeader& trigger,
                                          double muRtsSnr)
{
    NS_LOG_FUNCTION(this << muRtsHdr << trigger << muRtsSnr);

    if (!UlMuCsMediumIdle(trigger))
    {
        NS_LOG_DEBUG("UL MU CS indicated medium busy, cannot send CTS");
        return;
    }

    NS_ASSERT(m_staMac != nullptr && m_staMac->IsAssociated());
    WifiTxVector ctsTxVector = GetCtsTxVectorAfterMuRts(trigger, m_staMac->GetAssociationId());
    ctsTxVector.SetTriggerResponding(true);

    DoSendCtsAfterRts(muRtsHdr, ctsTxVector, muRtsSnr);
}

void
HeFrameExchangeManager::SendMultiStaBlockAck(const WifiTxParameters& txParams, Time durationId)
{
    NS_LOG_FUNCTION(this << &txParams << durationId.As(Time::US));

    NS_ASSERT(m_apMac);
    NS_ASSERT(txParams.m_acknowledgment &&
              txParams.m_acknowledgment->method == WifiAcknowledgment::UL_MU_MULTI_STA_BA);
    auto acknowledgment = static_cast<WifiUlMuMultiStaBa*>(txParams.m_acknowledgment.get());

    NS_ASSERT(!acknowledgment->stationsReceivingMultiStaBa.empty());

    CtrlBAckResponseHeader blockAck;
    blockAck.SetType(acknowledgment->baType);

    Mac48Address receiver;

    for (const auto& staInfo : acknowledgment->stationsReceivingMultiStaBa)
    {
        receiver = staInfo.first.first;
        uint8_t tid = staInfo.first.second;
        std::size_t index = staInfo.second;

        blockAck.SetAid11(m_apMac->GetAssociationId(receiver, m_linkId), index);
        blockAck.SetTidInfo(tid, index);

        if (tid == 14)
        {
            // All-ack context
            NS_LOG_DEBUG("Multi-STA Block Ack: Sending All-ack to=" << receiver);
            blockAck.SetAckType(true, index);
            continue;
        }

        if (acknowledgment->baType.m_bitmapLen.at(index) == 0)
        {
            // Acknowledgment context
            NS_LOG_DEBUG("Multi-STA Block Ack: Sending Ack to=" << receiver);
            blockAck.SetAckType(true, index);
        }
        else
        {
            // Block acknowledgment context
            blockAck.SetAckType(false, index);

            auto agreement = m_mac->GetBaAgreementEstablishedAsRecipient(receiver, tid);
            NS_ASSERT(agreement);
            agreement->get().FillBlockAckBitmap(blockAck, index);
            NS_LOG_DEBUG("Multi-STA Block Ack: Sending Block Ack with seq="
                         << blockAck.GetStartingSequence(index) << " to=" << receiver
                         << " tid=" << +tid);
        }
    }

    WifiMacHeader hdr;
    hdr.SetType(WIFI_MAC_CTL_BACKRESP);
    hdr.SetAddr1(acknowledgment->stationsReceivingMultiStaBa.size() == 1
                     ? receiver
                     : Mac48Address::GetBroadcast());
    hdr.SetAddr2(m_self);
    hdr.SetDsNotFrom();
    hdr.SetDsNotTo();

    Ptr<Packet> packet = Create<Packet>();
    packet->AddHeader(blockAck);
    Ptr<WifiPsdu> psdu =
        GetWifiPsdu(Create<WifiMpdu>(packet, hdr), acknowledgment->multiStaBaTxVector);

    Time txDuration = WifiPhy::CalculateTxDuration(GetBlockAckSize(acknowledgment->baType),
                                                   acknowledgment->multiStaBaTxVector,
                                                   m_phy->GetPhyBand());
    /**
     * In a BlockAck frame transmitted in response to a frame carried in HE TB PPDU under
     * single protection settings, the Duration/ID field is set to the value obtained from
     * the Duration/ID field of the frame that elicited the response minus the time, in
     * microseconds between the end of the PPDU carrying the frame that elicited the response
     * and the end of the PPDU carrying the BlockAck frame.
     * Under multiple protection settings, the Duration/ID field in a BlockAck frame transmitted
     * in response to a frame carried in HE TB PPDU is set according to the multiple protection
     * settings defined in 9.2.5.2. (Sec. 9.2.5.7 of 802.11ax-2021)
     */
    NS_ASSERT(m_edca);
    const auto singleDurationId = Max(durationId - m_phy->GetSifs() - txDuration, Seconds(0));
    if (m_edca->GetTxopLimit(m_linkId).IsZero()) // single protection settings
    {
        psdu->SetDuration(singleDurationId);
    }
    else // multiple protection settings
    {
        auto duration = Max(m_edca->GetRemainingTxop(m_linkId) - txDuration, Seconds(0));
        if (m_protectSingleExchange)
        {
            duration = std::min(duration, singleDurationId + m_singleExchangeProtectionSurplus);
        }
        psdu->SetDuration(duration);
    }

    psdu->GetPayload(0)->AddPacketTag(m_muSnrTag);

    ForwardPsduDown(psdu, acknowledgment->multiStaBaTxVector);

    // continue with the TXOP if time remains
    m_psduMap.clear();
    m_edca->ResetCw(m_linkId);
    m_muSnrTag.Reset();
    Simulator::Schedule(txDuration, &HeFrameExchangeManager::TransmissionSucceeded, this);
}

void
HeFrameExchangeManager::ReceiveBasicTrigger(const CtrlTriggerHeader& trigger,
                                            const WifiMacHeader& hdr)
{
    NS_LOG_FUNCTION(this << trigger << hdr);
    NS_ASSERT(trigger.IsBasic());
    NS_ASSERT(m_staMac && m_staMac->IsAssociated());

    NS_LOG_DEBUG("Received a Trigger Frame (basic variant) soliciting a transmission");

    if (!UlMuCsMediumIdle(trigger))
    {
        return;
    }

    // Starting from the Preferred AC indicated in the Trigger Frame, check if there
    // is either a pending BlockAckReq frame or a data frame that can be transmitted
    // in the allocated time and is addressed to a station with which a Block Ack
    // agreement has been established.

    // create the sequence of TIDs to check
    std::vector<uint8_t> tids;
    uint16_t staId = m_staMac->GetAssociationId();
    AcIndex preferredAc = trigger.FindUserInfoWithAid(staId)->GetPreferredAc();
    auto acIt = wifiAcList.find(preferredAc);
    for (uint8_t i = 0; i < 4; i++)
    {
        NS_ASSERT(acIt != wifiAcList.end());
        tids.push_back(acIt->second.GetHighTid());
        tids.push_back(acIt->second.GetLowTid());

        acIt++;
        if (acIt == wifiAcList.end())
        {
            acIt = wifiAcList.begin();
        }
    }

    Ptr<WifiPsdu> psdu;
    WifiTxParameters txParams;
    WifiTxVector tbTxVector = GetHeTbTxVector(trigger, hdr.GetAddr2());
    Time ppduDuration = HePhy::ConvertLSigLengthToHeTbPpduDuration(trigger.GetUlLength(),
                                                                   tbTxVector,
                                                                   m_phy->GetPhyBand());

    for (const auto& tid : tids)
    {
        Ptr<QosTxop> edca = m_mac->GetQosTxop(tid);

        if (!m_mac->GetBaAgreementEstablishedAsOriginator(hdr.GetAddr2(), tid))
        {
            // no Block Ack agreement established for this TID
            continue;
        }

        txParams.Clear();
        txParams.m_txVector = tbTxVector;

        // first, check if there is a pending BlockAckReq frame
        if (auto mpdu = GetBar(edca->GetAccessCategory(), tid, hdr.GetAddr2());
            mpdu && TryAddMpdu(mpdu, txParams, ppduDuration))
        {
            NS_LOG_DEBUG("Sending a BAR within a TB PPDU");
            psdu = Create<WifiPsdu>(mpdu, true);
            break;
        }

        // otherwise, check if a suitable data frame is available
        auto receiver =
            GetWifiRemoteStationManager()->GetMldAddress(hdr.GetAddr2()).value_or(hdr.GetAddr2());
        if (auto mpdu = edca->PeekNextMpdu(m_linkId, tid, receiver))
        {
            mpdu = CreateAliasIfNeeded(mpdu);
            if (auto item = edca->GetNextMpdu(m_linkId, mpdu, txParams, ppduDuration, false))
            {
                // try A-MPDU aggregation
                std::vector<Ptr<WifiMpdu>> mpduList =
                    m_mpduAggregator->GetNextAmpdu(item, txParams, ppduDuration);
                psdu = (mpduList.size() > 1 ? Create<WifiPsdu>(std::move(mpduList))
                                            : Create<WifiPsdu>(item, true));
                break;
            }
        }
    }

    if (psdu)
    {
        psdu->SetDuration(hdr.GetDuration() - m_phy->GetSifs() - ppduDuration);
        SendPsduMapWithProtection(WifiPsduMap{{staId, psdu}}, txParams);
    }
    else
    {
        // send QoS Null frames
        SendQosNullFramesInTbPpdu(trigger, hdr);
    }
}

void
HeFrameExchangeManager::SendQosNullFramesInTbPpdu(const CtrlTriggerHeader& trigger,
                                                  const WifiMacHeader& hdr)
{
    NS_LOG_FUNCTION(this << trigger << hdr);
    NS_ASSERT(trigger.IsBasic() || trigger.IsBsrp());
    NS_ASSERT(m_staMac && m_staMac->IsAssociated());

    NS_LOG_DEBUG("Requested to send QoS Null frames");

    if (!UlMuCsMediumIdle(trigger))
    {
        return;
    }

    const auto addr1 =
        GetWifiRemoteStationManager()->GetMldAddress(hdr.GetAddr2()).value_or(hdr.GetAddr2());
    WifiMacHeader header(WIFI_MAC_QOSDATA_NULL);
    header.SetAddr1(addr1);
    header.SetAddr2(m_mac->GetAddress());
    header.SetAddr3(hdr.GetAddr2());
    header.SetDsTo();
    header.SetDsNotFrom();
    // TR3: Sequence numbers for transmitted QoS (+)Null frames may be set
    // to any value. (Table 10-3 of 802.11-2016)
    header.SetSequenceNumber(0);
    // Set the EOSP bit so that HtFEM::FinalizeMacHeader will add the Queue Size
    header.SetQosEosp();

    WifiTxParameters txParams;
    txParams.m_txVector = GetHeTbTxVector(trigger, hdr.GetAddr2());
    txParams.m_protection = std::unique_ptr<WifiProtection>(new WifiNoProtection);
    txParams.m_acknowledgment = std::unique_ptr<WifiAcknowledgment>(new WifiNoAck);

    Time ppduDuration = HePhy::ConvertLSigLengthToHeTbPpduDuration(trigger.GetUlLength(),
                                                                   txParams.m_txVector,
                                                                   m_phy->GetPhyBand());
    header.SetDuration(hdr.GetDuration() - m_phy->GetSifs() - ppduDuration);

    std::vector<Ptr<WifiMpdu>> mpduList;

    for (uint8_t tid = 0; tid < 8; ++tid)
    {
        if (!m_mac->GetBaAgreementEstablishedAsOriginator(hdr.GetAddr2(), tid))
        {
            NS_LOG_DEBUG("Skipping tid=" << +tid << " because no agreement established");
            continue;
        }

        // We could call TryAddMpdu instead of IsWithinSizeAndTimeLimits below in order to
        // get the TX parameters updated automatically. However, aggregating the QoS Null
        // frames might fail because MPDU aggregation is disabled by default for VO
        // and BK. Therefore, we skip the check on max A-MPDU size and only update the
        // TX parameters below.
        header.SetQosTid(tid);
        auto mpdu = Create<WifiMpdu>(Create<Packet>(), header);
        mpdu = CreateAliasIfNeeded(mpdu);
        txParams.AddMpdu(mpdu);
        UpdateTxDuration(header.GetAddr1(), txParams);

        if (!IsWithinSizeAndTimeLimits(txParams.GetSize(header.GetAddr1()),
                                       hdr.GetAddr2(),
                                       txParams,
                                       ppduDuration))
        {
            txParams.UndoAddMpdu();
            break;
        }

        NS_LOG_DEBUG("Aggregating a QoS Null frame with tid=" << +tid);
        txParams.m_acknowledgment = GetAckManager()->TryAddMpdu(mpdu, txParams);
        mpduList.push_back(mpdu);
    }

    if (mpduList.empty())
    {
        NS_LOG_DEBUG("Not enough time to send a QoS Null frame");
        return;
    }

    Ptr<WifiPsdu> psdu = (mpduList.size() > 1 ? Create<WifiPsdu>(std::move(mpduList))
                                              : Create<WifiPsdu>(mpduList.front(), true));
    uint16_t staId = m_staMac->GetAssociationId();
    SendPsduMapWithProtection(WifiPsduMap{{staId, psdu}}, txParams);
}

void
HeFrameExchangeManager::ReceiveMuBarTrigger(const CtrlTriggerHeader& trigger,
                                            uint8_t tid,
                                            Time durationId,
                                            double snr)
{
    NS_LOG_FUNCTION(this << trigger << tid << durationId.As(Time::US) << snr);

    auto agreement = m_mac->GetBaAgreementEstablishedAsRecipient(m_bssid, tid);

    if (!agreement)
    {
        NS_LOG_DEBUG("There's not a valid agreement for this BlockAckReq");
        return;
    }

    if (!UlMuCsMediumIdle(trigger))
    {
        return;
    }

    NS_LOG_DEBUG("Send Block Ack in TB PPDU");
    auto txVector = GetHeTbTxVector(trigger, m_bssid);
    SendBlockAck(*agreement, durationId, txVector, snr);
}

bool
HeFrameExchangeManager::IsIntraBssPpdu(const WifiMacHeader& hdr, const WifiTxVector& txVector) const
{
    NS_LOG_FUNCTION(this << hdr << txVector);

    // "If, based on the MAC address information of a frame carried in a received PPDU, the
    // received PPDU satisfies both intra-BSS and inter-BSS conditions, then the received PPDU is
    // classified as an intra-BSS PPDU." (Sec. 26.2.2 of 802.11ax-2021)
    // Hence, check first if the intra-BSS conditions using MAC address information are satisfied:
    // 1. "The PPDU carries a frame that has an RA, TA, or BSSID field value that is equal to
    //    the BSSID of the BSS in which the STA is associated"
    const auto ra = hdr.GetAddr1();
    auto ta = hdr.GetAddr2();
    const auto bssid = hdr.GetAddr3();
    const auto empty = Mac48Address();

    if (ra == m_bssid || ta == m_bssid || bssid == m_bssid)
    {
        return true;
    }

    // 2. "The PPDU carries a Control frame that does not have a TA field and that has an
    //    RA field value that matches the saved TXOP holder address of the BSS in which
    //    the STA is associated"
    if (hdr.IsCtl() && ta == empty && ra == m_txopHolder)
    {
        return true;
    }

    // If we get here, the intra-BSS conditions using MAC address information are not satisfied.
    // "If the received PPDU satisfies the intra-BSS conditions using the RXVECTOR parameter
    // BSS_COLOR and also satisfies the inter-BSS conditions using MAC address information of a
    // frame carried in the PPDU, then the classification made using the MAC address information
    // takes precedence."
    // Hence, if the inter-BSS conditions using MAC address information are satisfied, the frame
    // is classified as inter-BSS
    // 1. "The PPDU carries a frame that has a BSSID field, the value of which is not the BSSID
    //    of the BSS in which the STA is associated"
    if (bssid != empty && bssid != m_bssid)
    {
        return false;
    }

    // 2. The PPDU carries a frame that does not have a BSSID field but has both an RA field and
    //    TA field, neither value of which is equal to the BSSID of the BSS in which the STA is
    //    associated
    if (bssid == empty && ta != empty && ra != empty && ta != m_bssid && ra != m_bssid)
    {
        return false;
    }

    // If we get here, both intra-BSS and inter-bss conditions using MAC address information
    // are not satisfied. Hence, the frame is classified as intra-BSS if the intra-BSS conditions
    // using the RXVECTOR parameters are satisfied:
    // 1. The RXVECTOR parameter BSS_COLOR of the PPDU carrying the frame is the BSS color of the
    //    BSS of which the STA is a member
    // This condition is used if the BSS is not disabled ("If a STA determines that the BSS color
    // is disabled (see 26.17.3.3), then the RXVECTOR parameter BSS_COLOR of a PPDU shall not be
    // used to classify the PPDU")
    const auto bssColor = m_mac->GetHeConfiguration()->m_bssColor;

    // the other two conditions using the RXVECTOR parameter PARTIAL_AID are not implemented
    return bssColor != 0 && bssColor == txVector.GetBssColor();
}

void
HeFrameExchangeManager::UpdateNav(const WifiMacHeader& hdr,
                                  const WifiTxVector& txVector,
                                  const Time& surplus)
{
    NS_LOG_FUNCTION(this << hdr << txVector << surplus.As(Time::US));

    if (!hdr.HasNav())
    {
        return;
    }

    if (hdr.GetAddr1() == m_self)
    {
        // When the received frame's RA is equal to the STA's own MAC address, the STA
        // shall not update its NAV (IEEE 802.11-2020, sec. 10.3.2.4)
        return;
    }

    // The intra-BSS NAV is updated by an intra-BSS PPDU. The basic NAV is updated by an
    // inter-BSS PPDU or a PPDU that cannot be classified as intra-BSS or inter-BSS.
    // (Section 26.2.4 of 802.11ax-2021)
    if (!IsIntraBssPpdu(hdr, txVector))
    {
        NS_LOG_DEBUG("PPDU not classified as intra-BSS, update the basic NAV");
        VhtFrameExchangeManager::UpdateNav(hdr, txVector, surplus);
        return;
    }

    NS_LOG_DEBUG("PPDU classified as intra-BSS, update the intra-BSS NAV");
    Time duration = hdr.GetDuration();
    NS_LOG_DEBUG("Duration/ID=" << duration);
    duration += surplus;

    if (hdr.IsCfEnd())
    {
        // An HE STA that maintains two NAVs (see 26.2.4) and receives a CF-End frame should reset
        // the basic NAV if the received CF-End frame is carried in an inter-BSS PPDU and reset the
        // intra-BSS NAV if the received CF-End frame is carried in an intra-BSS PPDU. (Sec. 26.2.5
        // of 802.11ax-2021)
        NS_LOG_DEBUG("Received CF-End, resetting the intra-BSS NAV");
        IntraBssNavResetTimeout();
        return;
    }

    // For all other received frames the STA shall update its NAV when the received
    // Duration is greater than the STA's current NAV value (IEEE 802.11-2020 sec. 10.3.2.4)
    auto intraBssNavEnd = Simulator::Now() + duration;
    if (intraBssNavEnd > m_intraBssNavEnd)
    {
        m_intraBssNavEnd = intraBssNavEnd;
        NS_LOG_DEBUG("Updated intra-BSS NAV=" << m_intraBssNavEnd);

        // A STA that used information from an RTS frame as the most recent basis to update
        // its NAV setting is permitted to reset its NAV if no PHY-RXSTART.indication
        // primitive is received from the PHY during a NAVTimeout period starting when the
        // MAC receives a PHY-RXEND.indication primitive corresponding to the detection of
        // the RTS frame. NAVTimeout period is equal to:
        // (2 x aSIFSTime) + (CTS_Time) + aRxPHYStartDelay + (2 x aSlotTime)
        // The “CTS_Time” shall be calculated using the length of the CTS frame and the data
        // rate at which the RTS frame used for the most recent NAV update was received
        // (IEEE 802.11-2016 sec. 10.3.2.4)
        if (hdr.IsRts())
        {
            auto addr2 = hdr.GetAddr2();
            WifiTxVector ctsTxVector =
                GetWifiRemoteStationManager()->GetCtsTxVector(addr2, txVector.GetMode());
            auto navResetDelay =
                2 * m_phy->GetSifs() +
                WifiPhy::CalculateTxDuration(GetCtsSize(), ctsTxVector, m_phy->GetPhyBand()) +
                WifiPhy::CalculatePhyPreambleAndHeaderDuration(ctsTxVector) + 2 * m_phy->GetSlot();
            m_intraBssNavResetEvent.Cancel();
            m_intraBssNavResetEvent =
                Simulator::Schedule(navResetDelay,
                                    &HeFrameExchangeManager::IntraBssNavResetTimeout,
                                    this);
        }
    }
    NS_LOG_DEBUG("Current intra-BSS NAV=" << m_intraBssNavEnd);

    m_channelAccessManager->NotifyNavStartNow(duration);
}

void
HeFrameExchangeManager::ClearTxopHolderIfNeeded()
{
    NS_LOG_FUNCTION(this);
    if (m_intraBssNavEnd <= Simulator::Now())
    {
        m_txopHolder.reset();
    }
}

void
HeFrameExchangeManager::NavResetTimeout()
{
    NS_LOG_FUNCTION(this);
    m_navEnd = Simulator::Now();
    // Do not reset the TXOP holder because the basic NAV is updated by inter-BSS frames
    // The NAV seen by the ChannelAccessManager is now the intra-BSS NAV only
    Time intraBssNav = Simulator::GetDelayLeft(m_intraBssNavResetEvent);
    m_channelAccessManager->NotifyNavResetNow(intraBssNav);
}

void
HeFrameExchangeManager::IntraBssNavResetTimeout()
{
    NS_LOG_FUNCTION(this);
    m_intraBssNavEnd = Simulator::Now();
    ClearTxopHolderIfNeeded();
    // The NAV seen by the ChannelAccessManager is now the basic NAV only
    Time basicNav = Simulator::GetDelayLeft(m_navResetEvent);
    m_channelAccessManager->NotifyNavResetNow(basicNav);
}

std::optional<Mac48Address>
HeFrameExchangeManager::FindTxopHolder(const WifiMacHeader& hdr, const WifiTxVector& txVector)
{
    NS_LOG_FUNCTION(this << hdr << txVector);

    if (hdr.IsTrigger() && hdr.GetAddr2() == m_bssid)
    {
        return m_bssid;
    }
    if (!txVector.IsUlMu()) // the sender of a TB PPDU is not the TXOP holder
    {
        return VhtFrameExchangeManager::FindTxopHolder(hdr, txVector);
    }
    return std::nullopt;
}

bool
HeFrameExchangeManager::VirtualCsMediumIdle() const
{
    // For an HE STA maintaining two NAVs, if both the NAV timers are 0, the virtual CS indication
    // is that the medium is idle; if at least one of the two NAV timers is nonzero, the virtual CS
    // indication is that the medium is busy. (Sec. 26.2.4 of 802.11ax-2021)
    return m_navEnd <= Simulator::Now() && m_intraBssNavEnd <= Simulator::Now();
}

bool
HeFrameExchangeManager::UlMuCsMediumIdle(const CtrlTriggerHeader& trigger) const
{
    if (!trigger.GetCsRequired())
    {
        NS_LOG_DEBUG("CS not required");
        return true;
    }

    // A non-AP STA does not consider the intra-BSS NAV in determining whether to respond to a
    // Trigger frame sent by the AP with which the non-AP STA is associated.
    // A non-AP STA considers the basic NAV in determining whether to respond to a Trigger frame
    // sent by the AP with which the non-AP STA is associated. (Sec. 26.5.2.5 of 802.11ax-2021)
    const Time now = Simulator::Now();
    if (m_navEnd > now)
    {
        NS_LOG_DEBUG("Basic NAV indicates medium busy");
        return false;
    }

    NS_ASSERT_MSG(m_staMac, "UL MU CS is only performed by non-AP STAs");
    const auto userInfoIt = trigger.FindUserInfoWithAid(m_staMac->GetAssociationId());
    NS_ASSERT_MSG(userInfoIt != trigger.end(),
                  "No User Info field for STA (" << m_self
                                                 << ") AID=" << m_staMac->GetAssociationId());

    std::set<uint8_t> indices;

    if (trigger.IsMuRts())
    {
        auto ctsTxVector = GetCtsTxVectorAfterMuRts(trigger, m_staMac->GetAssociationId());
        auto bw = ctsTxVector.GetChannelWidth();
        indices = m_phy->GetOperatingChannel().GetAll20MHzChannelIndicesInPrimary(bw);
    }
    else
    {
        indices =
            m_phy->GetOperatingChannel().Get20MHzIndicesCoveringRu(userInfoIt->GetRuAllocation(),
                                                                   trigger.GetUlBandwidth());
    }
    return !m_channelAccessManager->GetPer20MHzBusy(indices);
}

void
HeFrameExchangeManager::ReceiveMpdu(Ptr<const WifiMpdu> mpdu,
                                    RxSignalInfo rxSignalInfo,
                                    const WifiTxVector& txVector,
                                    bool inAmpdu)
{
    NS_LOG_FUNCTION(this << *mpdu << rxSignalInfo << txVector << inAmpdu);

    // The received MPDU is either broadcast or addressed to this station
    NS_ASSERT(mpdu->GetHeader().GetAddr1().IsGroup() || mpdu->GetHeader().GetAddr1() == m_self);

    const WifiMacHeader& hdr = mpdu->GetHeader();

    if (txVector.IsUlMu() && m_txTimer.IsRunning() &&
        m_txTimer.GetReason() == WifiTxTimer::WAIT_TB_PPDU_AFTER_BASIC_TF)
    {
        Mac48Address sender = hdr.GetAddr2();
        NS_ASSERT(m_txParams.m_acknowledgment &&
                  m_txParams.m_acknowledgment->method == WifiAcknowledgment::UL_MU_MULTI_STA_BA);
        auto acknowledgment = static_cast<WifiUlMuMultiStaBa*>(m_txParams.m_acknowledgment.get());
        std::size_t index = acknowledgment->baType.m_bitmapLen.size();

        if (!m_txTimer.GetStasExpectedToRespond().contains(sender))
        {
            NS_LOG_WARN("Received a TB PPDU from an unexpected station: " << sender);
            return;
        }

        if (hdr.IsBlockAckReq())
        {
            NS_LOG_DEBUG("Received a BlockAckReq in a TB PPDU from " << sender);

            CtrlBAckRequestHeader blockAckReq;
            mpdu->GetPacket()->PeekHeader(blockAckReq);
            NS_ABORT_MSG_IF(blockAckReq.IsMultiTid(), "Multi-TID BlockAckReq not supported");
            uint8_t tid = blockAckReq.GetTidInfo();
            GetBaManager(tid)->NotifyGotBlockAckRequest(
                m_mac->GetMldAddress(sender).value_or(sender),
                tid,
                blockAckReq.GetStartingSequence());

            // Block Acknowledgment context
            acknowledgment->stationsReceivingMultiStaBa.emplace(std::make_pair(sender, tid), index);
            acknowledgment->baType.m_bitmapLen.push_back(
                m_mac->GetBaTypeAsRecipient(sender, tid).m_bitmapLen.at(0));
            uint16_t staId = txVector.GetHeMuUserInfoMap().begin()->first;
            m_muSnrTag.Set(staId, rxSignalInfo.snr);
        }
        else if (hdr.IsQosData() && !inAmpdu && hdr.GetQosAckPolicy() == WifiMacHeader::NORMAL_ACK)
        {
            NS_LOG_DEBUG("Received an S-MPDU in a TB PPDU from " << sender << " (" << *mpdu << ")");

            uint8_t tid = hdr.GetQosTid();
            GetBaManager(tid)->NotifyGotMpdu(mpdu);

            // Acknowledgment context of Multi-STA Block Acks
            acknowledgment->stationsReceivingMultiStaBa.emplace(std::make_pair(sender, tid), index);
            acknowledgment->baType.m_bitmapLen.push_back(0);
            uint16_t staId = txVector.GetHeMuUserInfoMap().begin()->first;
            m_muSnrTag.Set(staId, rxSignalInfo.snr);
        }
        else if (!(hdr.IsQosData() && !hdr.HasData() && !inAmpdu))
        {
            // The other case handled by this function is when we receive a QoS Null frame
            // that is not in an A-MPDU. For all other cases, the reception is handled by
            // parent classes. In particular, in case of a QoS data frame in A-MPDU, we
            // have to wait until the A-MPDU reception is completed, but we let the
            // parent classes notify the Block Ack agreement of the reception of this MPDU
            VhtFrameExchangeManager::ReceiveMpdu(mpdu, rxSignalInfo, txVector, inAmpdu);
            return;
        }

        // Schedule the transmission of a Multi-STA BlockAck frame if needed
        if (!acknowledgment->stationsReceivingMultiStaBa.empty() && !m_multiStaBaEvent.IsPending())
        {
            m_multiStaBaEvent = Simulator::Schedule(m_phy->GetSifs(),
                                                    &HeFrameExchangeManager::SendMultiStaBlockAck,
                                                    this,
                                                    std::cref(m_txParams),
                                                    mpdu->GetHeader().GetDuration());
        }

        // remove the sender from the set of stations that are expected to send a TB PPDU
        m_txTimer.GotResponseFrom(sender);

        if (m_txTimer.GetStasExpectedToRespond().empty())
        {
            // we do not expect any other BlockAck frame
            m_txTimer.Cancel();
            m_channelAccessManager->NotifyAckTimeoutResetNow();

            if (!m_multiStaBaEvent.IsPending())
            {
                // all of the stations that replied with a TB PPDU sent QoS Null frames.
                NS_LOG_DEBUG("Continue the TXOP");
                m_psduMap.clear();
                m_edca->ResetCw(m_linkId);
                TransmissionSucceeded();
            }
        }

        // the received TB PPDU has been processed
        return;
    }

    if (txVector.IsUlMu() && m_txTimer.IsRunning() &&
        m_txTimer.GetReason() == WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF &&
        !inAmpdu) // if in A-MPDU, processing is done at the end of A-MPDU reception
    {
        const auto& sender = hdr.GetAddr2();

        if (!m_txTimer.GetStasExpectedToRespond().contains(sender))
        {
            NS_LOG_WARN("Received a TB PPDU from an unexpected station: " << sender);
            return;
        }
        if (!(hdr.IsQosData() && !hdr.HasData()))
        {
            NS_LOG_WARN("No QoS Null frame in the received MPDU");
            return;
        }

        NS_LOG_DEBUG("Received a QoS Null frame in a TB PPDU from " << sender);
        ReceivedQosNullAfterBsrpTf(sender);

        // the received TB PPDU has been processed
        return;
    }

    if (hdr.IsCtl())
    {
        if (hdr.IsCts() && m_txTimer.IsRunning() &&
            m_txTimer.GetReason() == WifiTxTimer::WAIT_CTS && m_psduMap.size() == 1)
        {
            NS_ABORT_MSG_IF(inAmpdu, "Received CTS as part of an A-MPDU");
            NS_ASSERT(hdr.GetAddr1() == m_self);

            Mac48Address sender = m_psduMap.begin()->second->GetAddr1();
            NS_LOG_DEBUG("Received CTS from=" << sender);

            SnrTag tag;
            mpdu->GetPacket()->PeekPacketTag(tag);
            GetWifiRemoteStationManager()->ReportRxOk(sender, rxSignalInfo, txVector);
            GetWifiRemoteStationManager()->ReportRtsOk(m_psduMap.begin()->second->GetHeader(0),
                                                       rxSignalInfo.snr,
                                                       txVector.GetMode(),
                                                       tag.Get());

            m_txTimer.Cancel();
            m_channelAccessManager->NotifyCtsTimeoutResetNow();
            ProtectionCompleted();
        }
        else if (hdr.IsCts() && m_txTimer.IsRunning() &&
                 m_txTimer.GetReason() == WifiTxTimer::WAIT_CTS_AFTER_MU_RTS)
        {
            NS_ABORT_MSG_IF(inAmpdu, "Received CTS as part of an A-MPDU");
            NS_ASSERT(hdr.GetAddr1() == m_self);

            NS_LOG_DEBUG("Received a CTS frame in response to an MU-RTS");

            m_txTimer.Cancel();
            m_channelAccessManager->NotifyCtsTimeoutResetNow();
            ProtectionCompleted();
        }
        else if (hdr.IsAck() && m_txTimer.IsRunning() &&
                 m_txTimer.GetReason() == WifiTxTimer::WAIT_NORMAL_ACK_AFTER_DL_MU_PPDU)
        {
            NS_ASSERT(hdr.GetAddr1() == m_self);
            NS_ASSERT(m_txParams.m_acknowledgment);
            NS_ASSERT(m_txParams.m_acknowledgment->method ==
                      WifiAcknowledgment::DL_MU_BAR_BA_SEQUENCE);

            auto acknowledgment =
                static_cast<WifiDlMuBarBaSequence*>(m_txParams.m_acknowledgment.get());
            NS_ASSERT(acknowledgment->stationsReplyingWithNormalAck.size() == 1);
            NS_ASSERT(m_apMac);
            uint16_t staId = m_apMac->GetAssociationId(
                acknowledgment->stationsReplyingWithNormalAck.begin()->first,
                m_linkId);
            auto it = m_psduMap.find(staId);
            NS_ASSERT(it != m_psduMap.end());
            NS_ASSERT(it->second->GetAddr1() ==
                      acknowledgment->stationsReplyingWithNormalAck.begin()->first);
            SnrTag tag;
            mpdu->GetPacket()->PeekPacketTag(tag);
            ReceivedNormalAck(*it->second->begin(),
                              m_txParams.m_txVector,
                              txVector,
                              rxSignalInfo,
                              tag.Get());
            m_psduMap.clear();
        }
        // TODO the PHY should not pass us a non-TB PPDU if we are waiting for a
        // TB PPDU. However, processing the PHY header is done by the PHY entity
        // corresponding to the modulation class of the PPDU being received, hence
        // it is not possible to check if a valid TRIGVECTOR is stored when receiving
        // PPDUs of older modulation classes. Therefore, we check here that we are
        // actually receiving a TB PPDU.
        else if (hdr.IsBlockAck() && txVector.IsUlMu() && m_txTimer.IsRunning() &&
                 m_txTimer.GetReason() == WifiTxTimer::WAIT_BLOCK_ACKS_IN_TB_PPDU)
        {
            Mac48Address sender = hdr.GetAddr2();
            NS_LOG_DEBUG("Received BlockAck in TB PPDU from=" << sender);

            SnrTag tag;
            mpdu->GetPacket()->PeekPacketTag(tag);

            // notify the Block Ack Manager
            CtrlBAckResponseHeader blockAck;
            mpdu->GetPacket()->PeekHeader(blockAck);
            uint8_t tid = blockAck.GetTidInfo();
            std::pair<uint16_t, uint16_t> ret =
                GetBaManager(tid)->NotifyGotBlockAck(m_linkId,
                                                     blockAck,
                                                     m_mac->GetMldAddress(sender).value_or(sender),
                                                     {tid});
            GetWifiRemoteStationManager()->ReportAmpduTxStatus(sender,
                                                               ret.first,
                                                               ret.second,
                                                               rxSignalInfo.snr,
                                                               tag.Get(),
                                                               m_txParams.m_txVector);

            // remove the sender from the set of stations that are expected to send a BlockAck
            if (!m_txTimer.GetStasExpectedToRespond().contains(sender))
            {
                NS_LOG_WARN("Received a BlockAck from an unexpected stations: " << sender);
                return;
            }

            m_txTimer.GotResponseFrom(sender);

            if (m_txTimer.GetStasExpectedToRespond().empty())
            {
                // we do not expect any other BlockAck frame
                m_txTimer.Cancel();
                m_channelAccessManager->NotifyAckTimeoutResetNow();
                if (m_triggerFrame)
                {
                    // this is strictly needed for DL_MU_TF_MU_BAR only
                    m_triggerFrame = nullptr;
                }

                m_edca->ResetCw(m_linkId);
                m_psduMap.clear();
                TransmissionSucceeded();
            }
        }
        else if (hdr.IsBlockAck() && m_txTimer.IsRunning() &&
                 m_txTimer.GetReason() == WifiTxTimer::WAIT_BLOCK_ACK_AFTER_TB_PPDU)
        {
            CtrlBAckResponseHeader blockAck;
            mpdu->GetPacket()->PeekHeader(blockAck);

            NS_ABORT_MSG_IF(!blockAck.IsMultiSta(),
                            "A Multi-STA BlockAck is expected after a TB PPDU");
            NS_LOG_DEBUG("Received a Multi-STA BlockAck from=" << hdr.GetAddr2());
            m_txTimer.GotResponseFrom(hdr.GetAddr2());

            NS_ASSERT(m_staMac && m_staMac->IsAssociated());
            if (hdr.GetAddr2() != m_bssid)
            {
                NS_LOG_DEBUG("The sender is not the AP we are associated with");
                return;
            }

            uint16_t staId = m_staMac->GetAssociationId();
            std::vector<uint32_t> indices = blockAck.FindPerAidTidInfoWithAid(staId);

            if (indices.empty())
            {
                NS_LOG_DEBUG("No Per AID TID Info subfield intended for me");
                return;
            }

            MuSnrTag tag;
            mpdu->GetPacket()->PeekPacketTag(tag);

            // notify the Block Ack Manager
            for (const auto& index : indices)
            {
                uint8_t tid = blockAck.GetTidInfo(index);

                if (blockAck.GetAckType(index) && tid < 8)
                {
                    // Acknowledgment context
                    NS_ABORT_IF(m_psduMap.empty() || m_psduMap.begin()->first != staId);
                    GetBaManager(tid)->NotifyGotAck(m_linkId, *m_psduMap.at(staId)->begin());
                }
                else
                {
                    // Block Acknowledgment or All-ack context
                    if (blockAck.GetAckType(index) && tid == 14)
                    {
                        // All-ack context, we need to determine the actual TID(s) of the PSDU
                        NS_ASSERT(indices.size() == 1);
                        NS_ABORT_IF(m_psduMap.empty() || m_psduMap.begin()->first != staId);
                        std::set<uint8_t> tids = m_psduMap.at(staId)->GetTids();
                        NS_ABORT_MSG_IF(tids.size() > 1, "Multi-TID A-MPDUs not supported yet");
                        tid = *tids.begin();
                    }

                    std::pair<uint16_t, uint16_t> ret = GetBaManager(tid)->NotifyGotBlockAck(
                        m_linkId,
                        blockAck,
                        m_mac->GetMldAddress(hdr.GetAddr2()).value_or(hdr.GetAddr2()),
                        {tid},
                        index);
                    GetWifiRemoteStationManager()->ReportAmpduTxStatus(hdr.GetAddr2(),
                                                                       ret.first,
                                                                       ret.second,
                                                                       rxSignalInfo.snr,
                                                                       tag.Get(staId),
                                                                       m_txParams.m_txVector);
                }

                if (m_psduMap.at(staId)->GetHeader(0).IsQosData() &&
                    (blockAck.GetAckType(index) // Ack or All-ack context
                     || std::any_of(blockAck.GetBitmap(index).begin(),
                                    blockAck.GetBitmap(index).end(),
                                    [](uint8_t b) { return b != 0; })))
                {
                    NS_ASSERT(m_psduMap.at(staId)->GetHeader(0).HasData());
                    NS_ASSERT(m_psduMap.at(staId)->GetHeader(0).GetQosTid() == tid);
                    // the station has received a response from the AP for the HE TB PPDU
                    // transmitted in response to a Basic Trigger Frame and at least one
                    // MPDU was acknowledged. Therefore, it needs to update the access
                    // parameters if it received an MU EDCA Parameter Set element.
                    m_mac->GetQosTxop(tid)->StartMuEdcaTimerNow(m_linkId);
                }
            }

            // cancel the timer
            m_txTimer.Cancel();
            m_channelAccessManager->NotifyAckTimeoutResetNow();
            // dequeue BlockAckReq frames included in acknowledged TB PPDUs (if any)
            for (const auto& [staId, psdu] : m_psduMap)
            {
                if (psdu->GetNMpdus() == 1 && psdu->GetHeader(0).IsBlockAckReq())
                {
                    DequeuePsdu(psdu);
                }
            }
            m_psduMap.clear();
        }
        else if (hdr.IsBlockAck() && m_txTimer.IsRunning() &&
                 m_txTimer.GetReason() == WifiTxTimer::WAIT_BLOCK_ACK)
        {
            // this BlockAck frame may have been sent in response to a DL MU PPDU with
            // acknowledgment in SU format or one of the consequent BlockAckReq frames.
            // We clear the PSDU map and let parent classes continue processing this frame.
            m_psduMap.clear();
            VhtFrameExchangeManager::ReceiveMpdu(mpdu, rxSignalInfo, txVector, inAmpdu);
        }
        else if (hdr.IsTrigger())
        {
            // Trigger Frames are only processed by STAs
            if (!m_staMac)
            {
                return;
            }

            // A Trigger Frame in an A-MPDU is processed when the A-MPDU is fully received
            if (inAmpdu)
            {
                m_triggerFrameInAmpdu = true;
                return;
            }

            CtrlTriggerHeader trigger;
            mpdu->GetPacket()->PeekHeader(trigger);

            if (hdr.GetAddr1() != m_self &&
                (!hdr.GetAddr1().IsBroadcast() || !m_staMac->IsAssociated() ||
                 hdr.GetAddr2() != m_bssid // not sent by the AP this STA is associated with
                 || trigger.FindUserInfoWithAid(m_staMac->GetAssociationId()) == trigger.end()))
            {
                // not addressed to us
                return;
            }

            uint16_t staId = m_staMac->GetAssociationId();

            if (trigger.IsMuRts())
            {
                Mac48Address sender = hdr.GetAddr2();
                NS_LOG_DEBUG("Received MU-RTS Trigger Frame from=" << sender);
                GetWifiRemoteStationManager()->ReportRxOk(sender, rxSignalInfo, txVector);

                // If a non-AP STA receives an MU-RTS Trigger frame, the non-AP STA shall commence
                // the transmission of a CTS frame response at the SIFS time boundary after
                // the end of a received PPDU when all the following conditions are met:
                // - The MU-RTS Trigger frame has one of the User Info fields addressed to
                //   the non-AP STA (this is guaranteed if we get here)
                // - The UL MU CS condition indicates that the medium is idle
                // (Sec. 26.2.6.3 of 802.11ax-2021)
                NS_LOG_DEBUG("Schedule CTS");
                m_sendCtsEvent = Simulator::Schedule(m_phy->GetSifs(),
                                                     &HeFrameExchangeManager::SendCtsAfterMuRts,
                                                     this,
                                                     hdr,
                                                     trigger,
                                                     rxSignalInfo.snr);
            }
            else if (trigger.IsMuBar())
            {
                Mac48Address sender = hdr.GetAddr2();
                NS_LOG_DEBUG("Received MU-BAR Trigger Frame from=" << sender);
                GetWifiRemoteStationManager()->ReportRxOk(sender, rxSignalInfo, txVector);

                auto userInfoIt = trigger.FindUserInfoWithAid(staId);
                NS_ASSERT(userInfoIt != trigger.end());
                CtrlBAckRequestHeader blockAckReq = userInfoIt->GetMuBarTriggerDepUserInfo();
                NS_ABORT_MSG_IF(blockAckReq.IsMultiTid(), "Multi-TID BlockAckReq not supported");
                uint8_t tid = blockAckReq.GetTidInfo();

                GetBaManager(tid)->NotifyGotBlockAckRequest(
                    m_mac->GetMldAddress(sender).value_or(sender),
                    tid,
                    blockAckReq.GetStartingSequence());

                Simulator::Schedule(m_phy->GetSifs(),
                                    &HeFrameExchangeManager::ReceiveMuBarTrigger,
                                    this,
                                    trigger,
                                    tid,
                                    hdr.GetDuration(),
                                    rxSignalInfo.snr);
            }
            else if (trigger.IsBasic())
            {
                Simulator::Schedule(m_phy->GetSifs(),
                                    &HeFrameExchangeManager::ReceiveBasicTrigger,
                                    this,
                                    trigger,
                                    hdr);
            }
            else if (trigger.IsBsrp())
            {
                Simulator::Schedule(m_phy->GetSifs(),
                                    &HeFrameExchangeManager::SendQosNullFramesInTbPpdu,
                                    this,
                                    trigger,
                                    hdr);
            }
        }
        else
        {
            // the received control frame cannot be handled here
            VhtFrameExchangeManager::ReceiveMpdu(mpdu, rxSignalInfo, txVector, inAmpdu);
        }

        // the received control frame has been processed
        return;
    }

    // the received frame cannot be handled here
    VhtFrameExchangeManager::ReceiveMpdu(mpdu, rxSignalInfo, txVector, inAmpdu);
    ;
}

void
HeFrameExchangeManager::EndReceiveAmpdu(Ptr<const WifiPsdu> psdu,
                                        const RxSignalInfo& rxSignalInfo,
                                        const WifiTxVector& txVector,
                                        const std::vector<bool>& perMpduStatus)
{
    std::set<uint8_t> tids = psdu->GetTids();

    if (txVector.IsUlMu() && m_txTimer.IsRunning() &&
        m_txTimer.GetReason() == WifiTxTimer::WAIT_TB_PPDU_AFTER_BASIC_TF)
    {
        Mac48Address sender = psdu->GetAddr2();
        NS_ASSERT(m_txParams.m_acknowledgment &&
                  m_txParams.m_acknowledgment->method == WifiAcknowledgment::UL_MU_MULTI_STA_BA);
        auto acknowledgment = static_cast<WifiUlMuMultiStaBa*>(m_txParams.m_acknowledgment.get());
        std::size_t index = acknowledgment->baType.m_bitmapLen.size();

        if (!m_txTimer.GetStasExpectedToRespond().contains(sender))
        {
            NS_LOG_WARN("Received a TB PPDU from an unexpected station: " << sender);
            return;
        }

        NS_LOG_DEBUG("Received an A-MPDU in a TB PPDU from " << sender << " (" << *psdu << ")");

        if (std::any_of(tids.begin(), tids.end(), [&psdu](uint8_t tid) {
                return psdu->GetAckPolicyForTid(tid) == WifiMacHeader::NORMAL_ACK;
            }))
        {
            if (std::all_of(perMpduStatus.cbegin(), perMpduStatus.cend(), [](bool v) { return v; }))
            {
                // All-ack context
                acknowledgment->stationsReceivingMultiStaBa.emplace(std::make_pair(sender, 14),
                                                                    index);
                acknowledgment->baType.m_bitmapLen.push_back(0);
            }
            else
            {
                // Block Acknowledgment context
                std::size_t i = 0;
                for (const auto& tid : tids)
                {
                    acknowledgment->stationsReceivingMultiStaBa.emplace(std::make_pair(sender, tid),
                                                                        index + i++);
                    acknowledgment->baType.m_bitmapLen.push_back(
                        m_mac->GetBaTypeAsRecipient(sender, tid).m_bitmapLen.at(0));
                }
            }
            uint16_t staId = txVector.GetHeMuUserInfoMap().begin()->first;
            m_muSnrTag.Set(staId, rxSignalInfo.snr);
        }

        // Schedule the transmission of a Multi-STA BlockAck frame if needed
        if (!acknowledgment->stationsReceivingMultiStaBa.empty() && !m_multiStaBaEvent.IsPending())
        {
            m_multiStaBaEvent = Simulator::Schedule(m_phy->GetSifs(),
                                                    &HeFrameExchangeManager::SendMultiStaBlockAck,
                                                    this,
                                                    std::cref(m_txParams),
                                                    psdu->GetDuration());
        }

        // remove the sender from the set of stations that are expected to send a TB PPDU
        m_txTimer.GotResponseFrom(sender);

        if (m_txTimer.GetStasExpectedToRespond().empty())
        {
            // we do not expect any other BlockAck frame
            m_txTimer.Cancel();
            m_channelAccessManager->NotifyAckTimeoutResetNow();

            if (!m_multiStaBaEvent.IsPending())
            {
                // all of the stations that replied with a TB PPDU sent QoS Null frames.
                NS_LOG_DEBUG("Continue the TXOP");
                m_psduMap.clear();
                m_edca->ResetCw(m_linkId);
                TransmissionSucceeded();
            }
        }

        // the received TB PPDU has been processed
        return;
    }

    if (txVector.IsUlMu() && m_txTimer.IsRunning() &&
        m_txTimer.GetReason() == WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF)
    {
        Mac48Address sender = psdu->GetAddr2();

        if (!m_txTimer.GetStasExpectedToRespond().contains(sender))
        {
            NS_LOG_WARN("Received a TB PPDU from an unexpected station: " << sender);
            return;
        }
        if (std::none_of(psdu->begin(), psdu->end(), [](Ptr<WifiMpdu> mpdu) {
                return mpdu->GetHeader().IsQosData() && !mpdu->GetHeader().HasData();
            }))
        {
            NS_LOG_WARN("No QoS Null frame in the received PSDU");
            return;
        }

        NS_LOG_DEBUG("Received QoS Null frames in a TB PPDU from " << sender);
        ReceivedQosNullAfterBsrpTf(sender);

        // the received TB PPDU has been processed
        return;
    }

    if (m_triggerFrameInAmpdu)
    {
        // the received A-MPDU contains a Trigger Frame. It is now time to handle it.
        auto psduIt = psdu->begin();
        while (psduIt != psdu->end())
        {
            if ((*psduIt)->GetHeader().IsTrigger())
            {
                ReceiveMpdu(*psduIt, rxSignalInfo, txVector, false);
            }
            psduIt++;
        }

        m_triggerFrameInAmpdu = false;
        return;
    }

    // the received frame cannot be handled here
    VhtFrameExchangeManager::EndReceiveAmpdu(psdu, rxSignalInfo, txVector, perMpduStatus);
}

void
HeFrameExchangeManager::ReceivedQosNullAfterBsrpTf(Mac48Address sender)
{
    NS_LOG_FUNCTION(this << sender);

    NS_ASSERT(m_txTimer.IsRunning() &&
              m_txTimer.GetReason() == WifiTxTimer::WAIT_QOS_NULL_AFTER_BSRP_TF);

    // remove the sender from the set of stations that are expected to send a TB PPDU
    m_txTimer.GotResponseFrom(sender);

    if (m_txTimer.GetStasExpectedToRespond().empty())
    {
        // we do not expect any other response
        m_channelAccessManager->NotifyAckTimeoutResetNow();

        NS_ASSERT(m_edca);
        m_psduMap.clear();
        m_edca->ResetCw(m_linkId);
        TransmissionSucceeded();
        // we reset the TX timer after calling TransmissionSucceeded, so that the latter can
        // check whether the reason for the last timer is WAIT_QOS_NULL_AFTER_BSRP_TF
        m_txTimer.Cancel();
    }
}

} // namespace ns3
