/*
 *  Copyright (c) 2016, The OpenThread Authors.
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @file
 *   This file implements mesh forwarding of IPv6/6LoWPAN messages.
 */

#include "mesh_forwarder.hpp"

#include "common/code_utils.hpp"
#include "common/debug.hpp"
#include "common/encoding.hpp"
#include "common/instance.hpp"
#include "common/locator_getters.hpp"
#include "common/message.hpp"
#include "common/random.hpp"
#include "common/time_ticker.hpp"
#include "net/ip6.hpp"
#include "net/ip6_filter.hpp"
#include "net/netif.hpp"
#include "net/tcp6.hpp"
#include "net/udp6.hpp"
#include "radio/radio.hpp"
#include "thread/mle.hpp"
#include "thread/mle_router.hpp"
#include "thread/thread_netif.hpp"

namespace ot {

RegisterLogModule("MeshForwarder");

void ThreadLinkInfo::SetFrom(const Mac::RxFrame &aFrame)
{
    Clear();

    if (kErrorNone != aFrame.GetSrcPanId(mPanId))
    {
        IgnoreError(aFrame.GetDstPanId(mPanId));
    }

    {
        Mac::PanId dstPanId;

        if (kErrorNone != aFrame.GetDstPanId(dstPanId))
        {
            dstPanId = mPanId;
        }

        mIsDstPanIdBroadcast = (dstPanId == Mac::kPanIdBroadcast);
    }

    mChannel      = aFrame.GetChannel();
    mRss          = aFrame.GetRssi();
    mLqi          = aFrame.GetLqi();
    mLinkSecurity = aFrame.GetSecurityEnabled();
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (aFrame.GetTimeIe() != nullptr)
    {
        mNetworkTimeOffset = aFrame.ComputeNetworkTimeOffset();
        mTimeSyncSeq       = aFrame.ReadTimeSyncSeq();
    }
#endif
#if OPENTHREAD_CONFIG_MULTI_RADIO
    mRadioType = static_cast<uint8_t>(aFrame.GetRadioType());
#endif
}

MeshForwarder::MeshForwarder(Instance &aInstance)
    : InstanceLocator(aInstance)
    , mMessageNextOffset(0)
    , mSendMessage(nullptr)
    , mMeshSource()
    , mMeshDest()
    , mAddMeshHeader(false)
    , mEnabled(false)
    , mTxPaused(false)
    , mSendBusy(false)
#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
    , mDelayNextTx(false)
    , mTxDelayTimer(aInstance, HandleTxDelayTimer)
#endif
    , mScheduleTransmissionTask(aInstance, MeshForwarder::ScheduleTransmissionTask)
#if OPENTHREAD_FTD
    , mIndirectSender(aInstance)
#endif
    , mDataPollSender(aInstance)
{
    mFragTag = Random::NonCrypto::GetUint16();

    ResetCounters();

#if OPENTHREAD_FTD
    mFragmentPriorityList.Clear();
#endif
}

void MeshForwarder::Start(void)
{
    if (!mEnabled)
    {
        Get<Mac::Mac>().SetRxOnWhenIdle(true);
#if OPENTHREAD_FTD
        mIndirectSender.Start();
#endif

        mEnabled = true;
    }
}

void MeshForwarder::Stop(void)
{
    VerifyOrExit(mEnabled);

    mDataPollSender.StopPolling();
    Get<TimeTicker>().UnregisterReceiver(TimeTicker::kMeshForwarder);
    Get<Mle::DiscoverScanner>().Stop();

    mSendQueue.DequeueAndFreeAll();
    mReassemblyList.DequeueAndFreeAll();

#if OPENTHREAD_FTD
    mIndirectSender.Stop();
    mFragmentPriorityList.Clear();
#endif

#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
    mTxDelayTimer.Stop();
    mDelayNextTx = false;
#endif

    mEnabled     = false;
    mSendMessage = nullptr;
    Get<Mac::Mac>().SetRxOnWhenIdle(false);

exit:
    return;
}

void MeshForwarder::PrepareEmptyFrame(Mac::TxFrame &aFrame, const Mac::Address &aMacDest, bool aAckRequest)
{
    uint16_t fcf       = 0;
    bool     iePresent = CalcIePresent(nullptr);

    Mac::Address macSource;
    macSource.SetShort(Get<Mac::Mac>().GetShortAddress());

    if (macSource.IsShortAddrInvalid() || aMacDest.IsExtended())
    {
        macSource.SetExtended(Get<Mac::Mac>().GetExtAddress());
    }

    fcf = Mac::Frame::kFcfFrameData | Mac::Frame::kFcfPanidCompression | Mac::Frame::kFcfSecurityEnabled;

    if (iePresent)
    {
        fcf |= Mac::Frame::kFcfIePresent;
    }

    fcf |= CalcFrameVersion(Get<NeighborTable>().FindNeighbor(aMacDest), iePresent);

    if (aAckRequest)
    {
        fcf |= Mac::Frame::kFcfAckRequest;
    }

    fcf |= (aMacDest.IsShort()) ? Mac::Frame::kFcfDstAddrShort : Mac::Frame::kFcfDstAddrExt;
    fcf |= (macSource.IsShort()) ? Mac::Frame::kFcfSrcAddrShort : Mac::Frame::kFcfSrcAddrExt;

    aFrame.InitMacHeader(fcf, Mac::Frame::kKeyIdMode1 | Mac::Frame::kSecEncMic32);

    if (aFrame.IsDstPanIdPresent())
    {
        aFrame.SetDstPanId(Get<Mac::Mac>().GetPanId());
    }
    IgnoreError(aFrame.SetSrcPanId(Get<Mac::Mac>().GetPanId()));

    aFrame.SetDstAddr(aMacDest);
    aFrame.SetSrcAddr(macSource);
    aFrame.SetFramePending(false);
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
    if (iePresent)
    {
        AppendHeaderIe(nullptr, aFrame);
    }
#endif
    aFrame.SetPayloadLength(0);
}

void MeshForwarder::RemoveMessage(Message &aMessage)
{
    PriorityQueue *queue = aMessage.GetPriorityQueue();

    OT_ASSERT(queue != nullptr);

    if (queue == &mSendQueue)
    {
#if OPENTHREAD_FTD
        for (Child &child : Get<ChildTable>().Iterate(Child::kInStateAnyExceptInvalid))
        {
            IgnoreError(mIndirectSender.RemoveMessageFromSleepyChild(aMessage, child));
        }
#endif

        if (mSendMessage == &aMessage)
        {
            mSendMessage = nullptr;
        }
    }

    LogMessage(kMessageEvict, aMessage, kErrorNoBufs);
    queue->DequeueAndFree(aMessage);
}

void MeshForwarder::ResumeMessageTransmissions(void)
{
    if (mTxPaused)
    {
        mTxPaused = false;
        mScheduleTransmissionTask.Post();
    }
}

#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
void MeshForwarder::HandleTxDelayTimer(Timer &aTimer)
{
    aTimer.Get<MeshForwarder>().HandleTxDelayTimer();
}

void MeshForwarder::HandleTxDelayTimer(void)
{
    mDelayNextTx = false;
    mScheduleTransmissionTask.Post();
    LogDebg("Tx delay timer expired");
}
#endif

#if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE

Error MeshForwarder::UpdateEcnOrDrop(Message &aMessage, bool aPreparingToSend)
{
    // This method performs delay-aware active queue management for
    // direct message transmission. It parses the IPv6 header from
    // `aMessage` to determine if message is  ECN-capable. This is
    // then used along with the message's time-in-queue to decide
    // whether to keep the message as is, change the ECN field to
    // mark congestion, or drop the message. If the message is to be
    // dropped, this method clears the direct tx flag on `aMessage`
    // and removes it from the send queue (if no pending indirect tx)
    // and returns `kErrorDrop`. This method returns `kErrorNone`
    // when the message is kept as is or ECN field is updated.

    Error    error         = kErrorNone;
    uint32_t timeInQueue   = TimerMilli::GetNow() - aMessage.GetTimestamp();
    bool     shouldMarkEcn = (timeInQueue >= kTimeInQueueMarkEcn);
    bool     isEcnCapable  = false;

    VerifyOrExit(aMessage.IsDirectTransmission() && (aMessage.GetOffset() == 0));

    if (aMessage.GetType() == Message::kTypeIp6)
    {
        Ip6::Header ip6Header;

        IgnoreError(aMessage.Read(0, ip6Header));

        VerifyOrExit(!Get<ThreadNetif>().HasUnicastAddress(ip6Header.GetSource()));

        isEcnCapable = (ip6Header.GetEcn() != Ip6::kEcnNotCapable);

        if ((shouldMarkEcn && !isEcnCapable) || (timeInQueue >= kTimeInQueueDropMsg))
        {
            ExitNow(error = kErrorDrop);
        }

        if (shouldMarkEcn)
        {
            switch (ip6Header.GetEcn())
            {
            case Ip6::kEcnCapable0:
            case Ip6::kEcnCapable1:
                ip6Header.SetEcn(Ip6::kEcnMarked);
                aMessage.Write(0, ip6Header);
                LogMessage(kMessageMarkEcn, aMessage);
                break;

            case Ip6::kEcnMarked:
            case Ip6::kEcnNotCapable:
                break;
            }
        }
    }
#if OPENTHREAD_FTD
    else if (aMessage.GetType() == Message::kType6lowpan)
    {
        uint16_t               headerLength = 0;
        uint16_t               offset;
        bool                   hasFragmentHeader = false;
        Lowpan::FragmentHeader fragmentHeader;
        Lowpan::MeshHeader     meshHeader;

        IgnoreError(meshHeader.ParseFrom(aMessage, headerLength));

        offset = headerLength;

        if (fragmentHeader.ParseFrom(aMessage, offset, headerLength) == kErrorNone)
        {
            hasFragmentHeader = true;
            offset += headerLength;
        }

        if (!hasFragmentHeader || (fragmentHeader.GetDatagramOffset() == 0))
        {
            Ip6::Ecn ecn = Get<Lowpan::Lowpan>().DecompressEcn(aMessage, offset);

            isEcnCapable = (ecn != Ip6::kEcnNotCapable);

            if ((shouldMarkEcn && !isEcnCapable) || (timeInQueue >= kTimeInQueueDropMsg))
            {
                FragmentPriorityList::Entry *entry;

                entry = mFragmentPriorityList.FindEntry(meshHeader.GetSource(), fragmentHeader.GetDatagramTag());

                if (entry != nullptr)
                {
                    entry->MarkToDrop();
                    entry->ResetLifetime();
                }

                ExitNow(error = kErrorDrop);
            }

            if (shouldMarkEcn)
            {
                switch (ecn)
                {
                case Ip6::kEcnCapable0:
                case Ip6::kEcnCapable1:
                    Get<Lowpan::Lowpan>().MarkCompressedEcn(aMessage, offset);
                    LogMessage(kMessageMarkEcn, aMessage);
                    break;

                case Ip6::kEcnMarked:
                case Ip6::kEcnNotCapable:
                    break;
                }
            }
        }
        else if (hasFragmentHeader)
        {
            FragmentPriorityList::Entry *entry;

            entry = mFragmentPriorityList.FindEntry(meshHeader.GetSource(), fragmentHeader.GetDatagramTag());
            VerifyOrExit(entry != nullptr);

            if (entry->ShouldDrop())
            {
                error = kErrorDrop;
            }

            // We can clear the entry if it is the last fragment and
            // only if the message is being prepared to be sent out.
            if (aPreparingToSend && (fragmentHeader.GetDatagramOffset() + aMessage.GetLength() - offset >=
                                     fragmentHeader.GetDatagramSize()))
            {
                entry->Clear();
            }
        }
    }
#else
    OT_UNUSED_VARIABLE(aPreparingToSend);
#endif // OPENTHREAD_FTD

exit:
    if (error == kErrorDrop)
    {
        LogMessage(kMessageQueueMgmtDrop, aMessage);
        aMessage.ClearDirectTransmission();
        RemoveMessageIfNoPendingTx(aMessage);
    }

    return error;
}

Error MeshForwarder::RemoveAgedMessages(void)
{
    // This method goes through all messages in the send queue and
    // removes all aged messages determined based on the delay-aware
    // active queue management rules. It may also mark ECN on some
    // messages. It returns `kErrorNone` if at least one message was
    // removed, or `kErrorNotFound` if none was removed.

    Error    error = kErrorNotFound;
    Message *nextMessage;

    for (Message *message = mSendQueue.GetHead(); message != nullptr; message = nextMessage)
    {
        nextMessage = message->GetNext();

        // Exclude the current message being sent `mSendMessage`.
        if ((message == mSendMessage) || !message->IsDirectTransmission())
        {
            continue;
        }

        if (UpdateEcnOrDrop(*message, /* aPreparingToSend */ false) == kErrorDrop)
        {
            error = kErrorNone;
        }
    }

    return error;
}

#endif // OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE

#if (OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE > 0)

bool MeshForwarder::IsDirectTxQueueOverMaxFrameThreshold(void) const
{
    uint16_t frameCount = 0;

    for (const Message &message : mSendQueue)
    {
        if (!message.IsDirectTransmission() || (&message == mSendMessage))
        {
            continue;
        }

        switch (message.GetType())
        {
        case Message::kTypeIp6:
        {
            // If it is an IPv6 message, we estimate the number of
            // fragment frames assuming typical header sizes and lowpan
            // compression. Since this estimate is only used for queue
            // management, we lean towards an under estimate in sense
            // that we may allow few more frames in the tx queue over
            // threshold in some rare cases.
            //
            // The constants below are derived as follows: Typical MAC
            // header (15 bytes) and MAC footer (6 bytes) leave 106
            // bytes for MAC payload. Next fragment header is 5 bytes
            // leaving 96 for next fragment payload. Lowpan compression
            // on average compresses 40 bytes IPv6 header into about 19
            // bytes leaving 87 bytes for the IPv6 payload, so the first
            // fragment can fit 87 + 40 = 127 bytes.

            static constexpr uint16_t kFirstFragmentMaxLength = 127;
            static constexpr uint16_t kNextFragmentSize       = 96;

            uint16_t length = message.GetLength();

            frameCount++;

            if (length > kFirstFragmentMaxLength)
            {
                frameCount += (length - kFirstFragmentMaxLength) / kNextFragmentSize;
            }

            break;
        }

        case Message::kType6lowpan:
        case Message::kTypeMacEmptyData:
            frameCount++;
            break;

        case Message::kTypeSupervision:
        default:
            break;
        }
    }

    return (frameCount > OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE);
}

void MeshForwarder::ApplyDirectTxQueueLimit(Message &aMessage)
{
    VerifyOrExit(aMessage.IsDirectTransmission());
    VerifyOrExit(IsDirectTxQueueOverMaxFrameThreshold());

#if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
    if (RemoveAgedMessages() == kErrorNone)
    {
        VerifyOrExit(IsDirectTxQueueOverMaxFrameThreshold());
    }
#endif

    LogMessage(kMessageFullQueueDrop, aMessage);
    aMessage.ClearDirectTransmission();
    RemoveMessageIfNoPendingTx(aMessage);

exit:
    return;
}

#endif // (OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE > 0)

void MeshForwarder::ScheduleTransmissionTask(Tasklet &aTasklet)
{
    aTasklet.Get<MeshForwarder>().ScheduleTransmissionTask();
}

void MeshForwarder::ScheduleTransmissionTask(void)
{
    VerifyOrExit(!mSendBusy && !mTxPaused);

#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
    VerifyOrExit(!mDelayNextTx);
#endif

    mSendMessage = PrepareNextDirectTransmission();
    VerifyOrExit(mSendMessage != nullptr);

    if (mSendMessage->GetOffset() == 0)
    {
        mSendMessage->SetTxSuccess(true);
    }

    Get<Mac::Mac>().RequestDirectFrameTransmission();

exit:
    return;
}

Message *MeshForwarder::PrepareNextDirectTransmission(void)
{
    Message *curMessage, *nextMessage;
    Error    error = kErrorNone;

    for (curMessage = mSendQueue.GetHead(); curMessage; curMessage = nextMessage)
    {
        // We set the `nextMessage` here but it can be updated again
        // after the `switch(message.GetType())` since it may be
        // evicted during message processing (e.g., from the call to
        // `UpdateIp6Route()` due to Address Solicit).

        nextMessage = curMessage->GetNext();

        if (!curMessage->IsDirectTransmission() || curMessage->IsResolvingAddress())
        {
            continue;
        }

#if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
        if (UpdateEcnOrDrop(*curMessage) == kErrorDrop)
        {
            continue;
        }
#endif
        curMessage->SetDoNotEvict(true);

        switch (curMessage->GetType())
        {
        case Message::kTypeIp6:
            error = UpdateIp6Route(*curMessage);
            break;

#if OPENTHREAD_FTD

        case Message::kType6lowpan:
            error = UpdateMeshRoute(*curMessage);
            break;

#endif

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
        case Message::kTypeMacEmptyData:
            error = kErrorNone;
            break;
#endif

        default:
            error = kErrorDrop;
            break;
        }

        curMessage->SetDoNotEvict(false);

        // the next message may have been evicted during processing (e.g. due to Address Solicit)
        nextMessage = curMessage->GetNext();

        switch (error)
        {
        case kErrorNone:
            ExitNow();

#if OPENTHREAD_FTD
        case kErrorAddressQuery:
            curMessage->SetResolvingAddress(true);
            continue;
#endif

        default:
            LogMessage(kMessageDrop, *curMessage, error);
            mSendQueue.DequeueAndFree(*curMessage);
            continue;
        }
    }

exit:
    return curMessage;
}

Error MeshForwarder::UpdateIp6Route(Message &aMessage)
{
    Mle::MleRouter &mle   = Get<Mle::MleRouter>();
    Error           error = kErrorNone;
    Ip6::Header     ip6Header;

    mAddMeshHeader = false;

    IgnoreError(aMessage.Read(0, ip6Header));

    VerifyOrExit(!ip6Header.GetSource().IsMulticast(), error = kErrorDrop);

    GetMacSourceAddress(ip6Header.GetSource(), mMacAddrs.mSource);

    if (mle.IsDisabled() || mle.IsDetached())
    {
        if (ip6Header.GetDestination().IsLinkLocal() || ip6Header.GetDestination().IsLinkLocalMulticast())
        {
            GetMacDestinationAddress(ip6Header.GetDestination(), mMacAddrs.mDestination);
        }
        else
        {
            error = kErrorDrop;
        }

        ExitNow();
    }

    if (ip6Header.GetDestination().IsMulticast())
    {
        // With the exception of MLE multicasts and any other message
        // with link security disabled, an End Device transmits
        // multicasts, as IEEE 802.15.4 unicasts to its parent.

        if (mle.IsChild() && aMessage.IsLinkSecurityEnabled() && !aMessage.IsSubTypeMle())
        {
            mMacAddrs.mDestination.SetShort(mle.GetNextHop(Mac::kShortAddrBroadcast));
        }
        else
        {
            mMacAddrs.mDestination.SetShort(Mac::kShortAddrBroadcast);
        }
    }
    else if (ip6Header.GetDestination().IsLinkLocal())
    {
        GetMacDestinationAddress(ip6Header.GetDestination(), mMacAddrs.mDestination);
    }
    else if (mle.IsMinimalEndDevice())
    {
        mMacAddrs.mDestination.SetShort(mle.GetNextHop(Mac::kShortAddrBroadcast));
    }
    else
    {
#if OPENTHREAD_FTD
        error = UpdateIp6RouteFtd(ip6Header, aMessage);
#else
        OT_ASSERT(false);
#endif
    }

exit:
    return error;
}

bool MeshForwarder::GetRxOnWhenIdle(void) const
{
    return Get<Mac::Mac>().GetRxOnWhenIdle();
}

void MeshForwarder::SetRxOnWhenIdle(bool aRxOnWhenIdle)
{
    Get<Mac::Mac>().SetRxOnWhenIdle(aRxOnWhenIdle);

    if (aRxOnWhenIdle)
    {
        mDataPollSender.StopPolling();
#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
        Get<Utils::SupervisionListener>().Stop();
#endif
    }
    else
    {
        mDataPollSender.StartPolling();
#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
        Get<Utils::SupervisionListener>().Start();
#endif
    }
}

void MeshForwarder::GetMacSourceAddress(const Ip6::Address &aIp6Addr, Mac::Address &aMacAddr)
{
    aIp6Addr.GetIid().ConvertToMacAddress(aMacAddr);

    if (aMacAddr.GetExtended() != Get<Mac::Mac>().GetExtAddress())
    {
        aMacAddr.SetShort(Get<Mac::Mac>().GetShortAddress());
    }
}

void MeshForwarder::GetMacDestinationAddress(const Ip6::Address &aIp6Addr, Mac::Address &aMacAddr)
{
    if (aIp6Addr.IsMulticast())
    {
        aMacAddr.SetShort(Mac::kShortAddrBroadcast);
    }
    else if (Get<Mle::MleRouter>().IsRoutingLocator(aIp6Addr))
    {
        aMacAddr.SetShort(aIp6Addr.GetIid().GetLocator());
    }
    else
    {
        aIp6Addr.GetIid().ConvertToMacAddress(aMacAddr);
    }
}

Mac::TxFrame *MeshForwarder::HandleFrameRequest(Mac::TxFrames &aTxFrames)
{
    Mac::TxFrame *frame         = nullptr;
    bool          addFragHeader = false;

    VerifyOrExit(mEnabled && (mSendMessage != nullptr));

#if OPENTHREAD_CONFIG_MULTI_RADIO
    frame = &Get<RadioSelector>().SelectRadio(*mSendMessage, mMacAddrs.mDestination, aTxFrames);

    // If multi-radio link is supported, when sending frame with link
    // security enabled, Fragment Header is always included (even if
    // the message is small and does not require 6LoWPAN fragmentation).
    // This allows the Fragment Header's tag to be used to detect and
    // suppress duplicate received frames over different radio links.

    if (mSendMessage->IsLinkSecurityEnabled())
    {
        addFragHeader = true;
    }
#else
    frame = &aTxFrames.GetTxFrame();
#endif

    mSendBusy = true;

    switch (mSendMessage->GetType())
    {
    case Message::kTypeIp6:
        if (mSendMessage->GetSubType() == Message::kSubTypeMleDiscoverRequest)
        {
            frame = Get<Mle::DiscoverScanner>().PrepareDiscoveryRequestFrame(*frame);
            VerifyOrExit(frame != nullptr);
        }
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
        if (Get<Mac::Mac>().IsCslEnabled() && mSendMessage->IsSubTypeMle())
        {
            mSendMessage->SetLinkSecurityEnabled(true);
        }
#endif
        mMessageNextOffset =
            PrepareDataFrame(*frame, *mSendMessage, mMacAddrs, mAddMeshHeader, mMeshSource, mMeshDest, addFragHeader);

        if ((mSendMessage->GetSubType() == Message::kSubTypeMleChildIdRequest) && mSendMessage->IsLinkSecurityEnabled())
        {
            LogNote("Child ID Request requires fragmentation, aborting tx");
            mMessageNextOffset = mSendMessage->GetLength();
            ExitNow(frame = nullptr);
        }

        break;

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    case Message::kTypeMacEmptyData:
    {
        Mac::Address macDestAddr;

        macDestAddr.SetShort(Get<Mle::MleRouter>().GetParent().GetRloc16());
        PrepareEmptyFrame(*frame, macDestAddr, /* aAckRequest */ true);
    }
    break;
#endif

#if OPENTHREAD_FTD

    case Message::kType6lowpan:
        SendMesh(*mSendMessage, *frame);
        break;

    case Message::kTypeSupervision:
        // A direct supervision message is possible in the case where
        // a sleepy child switches its mode (becomes non-sleepy) while
        // there is a pending indirect supervision message in the send
        // queue for it. The message would be then converted to a
        // direct tx.

        OT_FALL_THROUGH;
#endif

    default:
        mMessageNextOffset = mSendMessage->GetLength();
        ExitNow(frame = nullptr);
    }

    frame->SetIsARetransmission(false);

exit:
    return frame;
}

// This method constructs a MAC data from from a given IPv6 message.
//
// This method handles generation of MAC header, mesh header (if
// requested), lowpan compression of IPv6 header, lowpan fragmentation
// header (if message requires fragmentation or if it is explicitly
// requested by setting `aAddFragHeader` to `true`) It uses the
// message offset to construct next fragments. This method enables
// link security when message is MLE type and requires fragmentation.
// It returns the next offset into the message after the prepared
// frame.
//
uint16_t MeshForwarder::PrepareDataFrame(Mac::TxFrame &        aFrame,
                                         Message &             aMessage,
                                         const Mac::Addresses &aMacAddrs,
                                         bool                  aAddMeshHeader,
                                         uint16_t              aMeshSource,
                                         uint16_t              aMeshDest,
                                         bool                  aAddFragHeader)
{
    uint16_t     fcf;
    uint16_t     payloadLength;
    uint16_t     dstpan;
    uint8_t      secCtl;
    uint16_t     origMsgOffset;
    uint16_t     nextOffset;
    FrameBuilder frameBuilder;
    bool         iePresent = CalcIePresent(&aMessage);

start:

    // Initialize MAC header
    fcf = Mac::Frame::kFcfFrameData;

    fcf |= (aMacAddrs.mDestination.IsShort()) ? Mac::Frame::kFcfDstAddrShort : Mac::Frame::kFcfDstAddrExt;
    fcf |= (aMacAddrs.mSource.IsShort()) ? Mac::Frame::kFcfSrcAddrShort : Mac::Frame::kFcfSrcAddrExt;

    if (iePresent)
    {
        fcf |= Mac::Frame::kFcfIePresent;
    }

    fcf |= CalcFrameVersion(Get<NeighborTable>().FindNeighbor(aMacAddrs.mDestination), iePresent);

    // All unicast frames request ACK
    if (aMacAddrs.mDestination.IsExtended() || !aMacAddrs.mDestination.IsBroadcast())
    {
        fcf |= Mac::Frame::kFcfAckRequest;
    }

    if (aMessage.IsLinkSecurityEnabled())
    {
        fcf |= Mac::Frame::kFcfSecurityEnabled;

        switch (aMessage.GetSubType())
        {
        case Message::kSubTypeJoinerEntrust:
            secCtl = static_cast<uint8_t>(Mac::Frame::kKeyIdMode0);
            break;

        case Message::kSubTypeMleAnnounce:
            secCtl = static_cast<uint8_t>(Mac::Frame::kKeyIdMode2);
            break;

        default:
            secCtl = static_cast<uint8_t>(Mac::Frame::kKeyIdMode1);
            break;
        }

        secCtl |= Mac::Frame::kSecEncMic32;
    }
    else
    {
        secCtl = Mac::Frame::kSecNone;
    }

    dstpan = Get<Mac::Mac>().GetPanId();

    switch (aMessage.GetSubType())
    {
    case Message::kSubTypeMleAnnounce:
        aFrame.SetChannel(aMessage.GetChannel());
        dstpan = Mac::kPanIdBroadcast;
        break;

    case Message::kSubTypeMleDiscoverRequest:
    case Message::kSubTypeMleDiscoverResponse:
        dstpan = aMessage.GetPanId();
        break;

    default:
        break;
    }

    // Handle special case in 15.4-2015:
    //  Dest Address: Extended
    //  Source Address: Extended
    //  Dest PanId: Present
    //  Src Panid: Not Present
    //  Pan ID Compression: 0
    if (dstpan == Get<Mac::Mac>().GetPanId() &&
        ((fcf & Mac::Frame::kFcfFrameVersionMask) == Mac::Frame::kFcfFrameVersion2006 ||
         (fcf & Mac::Frame::kFcfDstAddrMask) != Mac::Frame::kFcfDstAddrExt ||
         (fcf & Mac::Frame::kFcfSrcAddrMask) != Mac::Frame::kFcfSrcAddrExt))
    {
#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
        // Handle a special case in IEEE 802.15.4-2015, when Pan ID Compression is 0, but Src Pan ID is not present:
        //  Dest Address:       Extended
        //  Src Address:        Extended
        //  Dest Pan ID:        Present
        //  Src Pan ID:         Not Present
        //  Pan ID Compression: 0

        if ((fcf & Mac::Frame::kFcfFrameVersionMask) != Mac::Frame::kFcfFrameVersion2015 ||
            (fcf & Mac::Frame::kFcfDstAddrMask) != Mac::Frame::kFcfDstAddrExt ||
            (fcf & Mac::Frame::kFcfSrcAddrMask) != Mac::Frame::kFcfSrcAddrExt)
#endif
        {
            fcf |= Mac::Frame::kFcfPanidCompression;
        }
    }

    aFrame.InitMacHeader(fcf, secCtl);

    if (aFrame.IsDstPanIdPresent())
    {
        aFrame.SetDstPanId(dstpan);
    }

    IgnoreError(aFrame.SetSrcPanId(Get<Mac::Mac>().GetPanId()));
    aFrame.SetDstAddr(aMacAddrs.mDestination);
    aFrame.SetSrcAddr(aMacAddrs.mSource);

#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
    if (iePresent)
    {
        AppendHeaderIe(&aMessage, aFrame);
    }
#endif

    frameBuilder.Init(aFrame.GetPayload(), aFrame.GetMaxPayloadLength());

#if OPENTHREAD_FTD

    // Initialize Mesh header
    if (aAddMeshHeader)
    {
        Mle::MleRouter &   mle = Get<Mle::MleRouter>();
        Lowpan::MeshHeader meshHeader;
        uint8_t            hopsLeft;
        uint16_t           maxPayloadLength;

        // Mesh Header frames are forwarded by routers over multiple
        // hops to reach a final destination. The forwarding path can
        // have routers supporting different radio links with varying
        // MTU sizes. Since the originator of the frame does not know the
        // path and the MTU sizes of supported radio links by the routers
        // in the path, we limit the max payload length of a Mesh Header
        // frame to a fixed minimum value (derived from 15.4 radio)
        // ensuring it can be handled by any radio link.
        //
        // Maximum payload length is calculated by subtracting the frame
        // header and footer lengths from the MTU size. The footer
        // length is derived by removing the `aFrame.GetFcsSize()` and
        // then adding the fixed `kMeshHeaderFrameFcsSize` instead
        // (updating the FCS size in the calculation of footer length).

        maxPayloadLength = kMeshHeaderFrameMtu - aFrame.GetHeaderLength() -
                           (aFrame.GetFooterLength() - aFrame.GetFcsSize() + kMeshHeaderFrameFcsSize);

        frameBuilder.Init(aFrame.GetPayload(), maxPayloadLength);

        if (mle.IsChild())
        {
            // REED sets hopsLeft to max (16) + 1. It does not know the route cost.
            hopsLeft = Mle::kMaxRouteCost + 1;
        }
        else
        {
            // Calculate the number of predicted hops.
            hopsLeft = mle.GetRouteCost(aMeshDest);

            if (hopsLeft != Mle::kMaxRouteCost)
            {
                hopsLeft += mle.GetLinkCost(Mle::Mle::RouterIdFromRloc16(mle.GetNextHop(aMeshDest)));
            }
            else
            {
                // In case there is no route to the destination router (only link).
                hopsLeft = mle.GetLinkCost(Mle::Mle::RouterIdFromRloc16(aMeshDest));
            }
        }

        // The hopsLft field MUST be incremented by one if the
        // destination RLOC16 is not that of an active Router.
        if (!Mle::Mle::IsActiveRouter(aMeshDest))
        {
            hopsLeft += 1;
        }

        meshHeader.Init(aMeshSource, aMeshDest, hopsLeft + Lowpan::MeshHeader::kAdditionalHopsLeft);

        IgnoreError(meshHeader.AppendTo(frameBuilder));
    }

#endif // OPENTHREAD_FTD

    // While performing lowpan compression, the message offset may be
    // changed to skip over the compressed IPv6 headers, we save the
    // original offset and set it back on `aMessage` at the end
    // before returning.

    origMsgOffset = aMessage.GetOffset();

    // Compress IPv6 Header
    if (aMessage.GetOffset() == 0)
    {
        uint16_t       fragHeaderOffset;
        uint16_t       maxFrameLength;
        Mac::Addresses macAddrs;

        // Before performing lowpan header compression, we reduce the
        // max length on `frameBuilder` to reserve bytes for first
        // fragment header. This ensures that lowpan compression will
        // leave room for a first fragment header. After the lowpan
        // header compression is done, we reclaim the reserved bytes
        // by setting the max length back to its original value.

        fragHeaderOffset = frameBuilder.GetLength();
        maxFrameLength   = frameBuilder.GetMaxLength();
        frameBuilder.SetMaxLength(maxFrameLength - sizeof(Lowpan::FragmentHeader::FirstFrag));

        if (aAddMeshHeader)
        {
            macAddrs.mSource.SetShort(aMeshSource);
            macAddrs.mDestination.SetShort(aMeshDest);
        }
        else
        {
            macAddrs = aMacAddrs;
        }

        SuccessOrAssert(Get<Lowpan::Lowpan>().Compress(aMessage, macAddrs, frameBuilder));

        frameBuilder.SetMaxLength(maxFrameLength);

        payloadLength = aMessage.GetLength() - aMessage.GetOffset();

        if (aAddFragHeader || (payloadLength > frameBuilder.GetRemainingLength()))
        {
            Lowpan::FragmentHeader::FirstFrag firstFragHeader;

            if ((!aMessage.IsLinkSecurityEnabled()) && aMessage.IsSubTypeMle())
            {
                // MLE messages that require fragmentation MUST use
                // link-layer security. We enable security and try
                // constructing the frame again.

                aMessage.SetOffset(0);
                aMessage.SetLinkSecurityEnabled(true);
                goto start;
            }

            // Insert Fragment header
            if (aMessage.GetDatagramTag() == 0)
            {
                // Avoid using datagram tag value 0, which indicates the tag has not been set
                if (mFragTag == 0)
                {
                    mFragTag++;
                }

                aMessage.SetDatagramTag(mFragTag++);
            }

            firstFragHeader.Init(aMessage.GetLength(), static_cast<uint16_t>(aMessage.GetDatagramTag()));
            SuccessOrAssert(frameBuilder.Insert(fragHeaderOffset, firstFragHeader));
        }
    }
    else
    {
        Lowpan::FragmentHeader::NextFrag nextFragHeader;

        nextFragHeader.Init(aMessage.GetLength(), static_cast<uint16_t>(aMessage.GetDatagramTag()),
                            aMessage.GetOffset());
        SuccessOrAssert(frameBuilder.Append(nextFragHeader));

        payloadLength = aMessage.GetLength() - aMessage.GetOffset();
    }

    if (payloadLength > frameBuilder.GetRemainingLength())
    {
        payloadLength = (frameBuilder.GetRemainingLength() & ~0x7);
    }

    // Copy IPv6 Payload
    SuccessOrAssert(frameBuilder.AppendBytesFromMessage(aMessage, aMessage.GetOffset(), payloadLength));
    aFrame.SetPayloadLength(frameBuilder.GetLength());

    nextOffset = aMessage.GetOffset() + payloadLength;

    if (nextOffset < aMessage.GetLength())
    {
        aFrame.SetFramePending(true);
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
        aMessage.SetTimeSync(false);
#endif
    }

    aMessage.SetOffset(origMsgOffset);

    return nextOffset;
}

Neighbor *MeshForwarder::UpdateNeighborOnSentFrame(Mac::TxFrame &      aFrame,
                                                   Error               aError,
                                                   const Mac::Address &aMacDest,
                                                   bool                aIsDataPoll)
{
    OT_UNUSED_VARIABLE(aIsDataPoll);

    Neighbor *neighbor = nullptr;

    VerifyOrExit(mEnabled);

    neighbor = Get<NeighborTable>().FindNeighbor(aMacDest);
    VerifyOrExit(neighbor != nullptr);

    VerifyOrExit(aFrame.GetAckRequest());

#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
    // TREL radio link uses deferred ack model. We ignore
    // `SendDone` event from `Mac` layer with success status and
    // wait for deferred ack callback instead.
#if OPENTHREAD_CONFIG_MULTI_RADIO
    if (aFrame.GetRadioType() == Mac::kRadioTypeTrel)
#endif
    {
        VerifyOrExit(aError != kErrorNone);
    }
#endif // OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE

#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
    if ((aFrame.GetHeaderIe(Mac::CslIe::kHeaderIeId) != nullptr) && aIsDataPoll)
    {
        UpdateNeighborLinkFailures(*neighbor, aError, /* aAllowNeighborRemove */ true,
                                   /* aFailLimit */ Mle::kFailedCslDataPollTransmissions);
    }
    else
#endif
    {
        UpdateNeighborLinkFailures(*neighbor, aError, /* aAllowNeighborRemove */ true);
    }

exit:
    return neighbor;
}

void MeshForwarder::UpdateNeighborLinkFailures(Neighbor &aNeighbor,
                                               Error     aError,
                                               bool      aAllowNeighborRemove,
                                               uint8_t   aFailLimit)
{
    // Update neighbor `LinkFailures` counter on ack error.

    if (aError == kErrorNone)
    {
        aNeighbor.ResetLinkFailures();
    }
    else if (aError == kErrorNoAck)
    {
        aNeighbor.IncrementLinkFailures();

        if (aAllowNeighborRemove && (Mle::Mle::IsActiveRouter(aNeighbor.GetRloc16())) &&
            (aNeighbor.GetLinkFailures() >= aFailLimit))
        {
            Get<Mle::MleRouter>().RemoveRouterLink(static_cast<Router &>(aNeighbor));
        }
    }
}

#if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE
void MeshForwarder::HandleDeferredAck(Neighbor &aNeighbor, Error aError)
{
    bool allowNeighborRemove = true;

    VerifyOrExit(mEnabled);

    if (aError == kErrorNoAck)
    {
        LogInfo("Deferred ack timeout on trel for neighbor %s rloc16:0x%04x",
                aNeighbor.GetExtAddress().ToString().AsCString(), aNeighbor.GetRloc16());
    }

#if OPENTHREAD_CONFIG_MULTI_RADIO
    // In multi radio mode, `RadioSelector` will update the neighbor's
    // link failure counter and removes the neighbor if required.
    Get<RadioSelector>().UpdateOnDeferredAck(aNeighbor, aError, allowNeighborRemove);
#else
    UpdateNeighborLinkFailures(aNeighbor, aError, allowNeighborRemove);
#endif

exit:
    return;
}
#endif // #if OPENTHREAD_CONFIG_RADIO_LINK_TREL_ENABLE

void MeshForwarder::HandleSentFrame(Mac::TxFrame &aFrame, Error aError)
{
    Neighbor *   neighbor = nullptr;
    Mac::Address macDest;

    OT_ASSERT((aError == kErrorNone) || (aError == kErrorChannelAccessFailure) || (aError == kErrorAbort) ||
              (aError == kErrorNoAck));

    mSendBusy = false;

    VerifyOrExit(mEnabled);

#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_COLLISION_AVOIDANCE_DELAY_ENABLE
    if (mDelayNextTx && (aError == kErrorNone))
    {
        mTxDelayTimer.Start(kTxDelayInterval);
        LogDebg("Start tx delay timer for %u msec", kTxDelayInterval);
    }
    else
    {
        mDelayNextTx = false;
    }
#endif

    if (!aFrame.IsEmpty())
    {
        IgnoreError(aFrame.GetDstAddr(macDest));
        neighbor = UpdateNeighborOnSentFrame(aFrame, aError, macDest);
    }

    UpdateSendMessage(aError, macDest, neighbor);

exit:
    return;
}

void MeshForwarder::UpdateSendMessage(Error aFrameTxError, Mac::Address &aMacDest, Neighbor *aNeighbor)
{
    Error txError = aFrameTxError;

    VerifyOrExit(mSendMessage != nullptr);

    OT_ASSERT(mSendMessage->IsDirectTransmission());

    if (aFrameTxError != kErrorNone)
    {
        // If the transmission of any fragment frame fails,
        // the overall message transmission is considered
        // as failed

        mSendMessage->SetTxSuccess(false);

#if OPENTHREAD_CONFIG_DROP_MESSAGE_ON_FRAGMENT_TX_FAILURE

        // We set the NextOffset to end of message to avoid sending
        // any remaining fragments in the message.

        mMessageNextOffset = mSendMessage->GetLength();
#endif
    }

    if (mMessageNextOffset < mSendMessage->GetLength())
    {
        mSendMessage->SetOffset(mMessageNextOffset);
        ExitNow();
    }

    txError = aFrameTxError;

    mSendMessage->ClearDirectTransmission();
    mSendMessage->SetOffset(0);

    if (aNeighbor != nullptr)
    {
        aNeighbor->GetLinkInfo().AddMessageTxStatus(mSendMessage->GetTxSuccess());
    }

#if !OPENTHREAD_CONFIG_DROP_MESSAGE_ON_FRAGMENT_TX_FAILURE

    // When `CONFIG_DROP_MESSAGE_ON_FRAGMENT_TX_FAILURE` is
    // disabled, all fragment frames of a larger message are
    // sent even if the transmission of an earlier fragment fail.
    // Note that `GetTxSuccess() tracks the tx success of the
    // entire message, while `aFrameTxError` represents the error
    // status of the last fragment frame transmission.

    if (!mSendMessage->GetTxSuccess() && (txError == kErrorNone))
    {
        txError = kErrorFailed;
    }
#endif

#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
    Get<Utils::HistoryTracker>().RecordTxMessage(*mSendMessage, aMacDest);
#endif

    LogMessage(kMessageTransmit, *mSendMessage, txError, &aMacDest);

    if (mSendMessage->GetType() == Message::kTypeIp6)
    {
        if (mSendMessage->GetTxSuccess())
        {
            mIpCounters.mTxSuccess++;
        }
        else
        {
            mIpCounters.mTxFailure++;
        }
    }

    switch (mSendMessage->GetSubType())
    {
    case Message::kSubTypeMleDiscoverRequest:
        // Note that `HandleDiscoveryRequestFrameTxDone()` may update
        // `mSendMessage` and mark it again for direct transmission.
        Get<Mle::DiscoverScanner>().HandleDiscoveryRequestFrameTxDone(*mSendMessage);
        break;

    case Message::kSubTypeMleChildIdRequest:
        if (mSendMessage->IsLinkSecurityEnabled())
        {
            // If the Child ID Request requires fragmentation and therefore
            // link layer security, the frame transmission will be aborted.
            // When the message is being freed, we signal to MLE to prepare a
            // shorter Child ID Request message (by only including mesh-local
            // address in the Address Registration TLV).

            LogInfo("Requesting shorter `Child ID Request`");
            Get<Mle::Mle>().RequestShorterChildIdRequest();
        }

        break;

    default:
        break;
    }

    RemoveMessageIfNoPendingTx(*mSendMessage);

exit:
    mScheduleTransmissionTask.Post();
}

void MeshForwarder::RemoveMessageIfNoPendingTx(Message &aMessage)
{
    VerifyOrExit(!aMessage.IsDirectTransmission() && !aMessage.IsChildPending());

    if (mSendMessage == &aMessage)
    {
        mSendMessage       = nullptr;
        mMessageNextOffset = 0;
    }

    mSendQueue.DequeueAndFree(aMessage);

exit:
    return;
}

void MeshForwarder::HandleReceivedFrame(Mac::RxFrame &aFrame)
{
    ThreadLinkInfo linkInfo;
    Mac::Addresses macAddrs;
    FrameData      frameData;
    Error          error = kErrorNone;

    VerifyOrExit(mEnabled, error = kErrorInvalidState);

    SuccessOrExit(error = aFrame.GetSrcAddr(macAddrs.mSource));
    SuccessOrExit(error = aFrame.GetDstAddr(macAddrs.mDestination));

    linkInfo.SetFrom(aFrame);

    frameData.Init(aFrame.GetPayload(), aFrame.GetPayloadLength());

#if OPENTHREAD_CONFIG_CHILD_SUPERVISION_ENABLE
    Get<Utils::SupervisionListener>().UpdateOnReceive(macAddrs.mSource, linkInfo.IsLinkSecurityEnabled());
#endif

    switch (aFrame.GetType())
    {
    case Mac::Frame::kFcfFrameData:
        if (Lowpan::MeshHeader::IsMeshHeader(frameData))
        {
#if OPENTHREAD_FTD
            HandleMesh(frameData, macAddrs.mSource, linkInfo);
#endif
        }
        else if (Lowpan::FragmentHeader::IsFragmentHeader(frameData))
        {
            HandleFragment(frameData, macAddrs, linkInfo);
        }
        else if (Lowpan::Lowpan::IsLowpanHc(frameData))
        {
            HandleLowpanHC(frameData, macAddrs, linkInfo);
        }
        else
        {
            VerifyOrExit(frameData.GetLength() == 0, error = kErrorNotLowpanDataFrame);

            LogFrame("Received empty payload frame", aFrame, kErrorNone);
        }

        break;

    case Mac::Frame::kFcfFrameBeacon:
        break;

    default:
        error = kErrorDrop;
        break;
    }

exit:

    if (error != kErrorNone)
    {
        LogFrame("Dropping rx frame", aFrame, error);
    }
}

void MeshForwarder::HandleFragment(FrameData &           aFrameData,
                                   const Mac::Addresses &aMacAddrs,
                                   const ThreadLinkInfo &aLinkInfo)
{
    Error                  error = kErrorNone;
    Lowpan::FragmentHeader fragmentHeader;
    Message *              message = nullptr;

    SuccessOrExit(error = fragmentHeader.ParseFrom(aFrameData));

#if OPENTHREAD_CONFIG_MULTI_RADIO

    if (aLinkInfo.mLinkSecurity)
    {
        Neighbor *neighbor = Get<NeighborTable>().FindNeighbor(aMacAddrs.mSource, Neighbor::kInStateAnyExceptInvalid);

        if (neighbor != nullptr)
        {
            uint16_t tag = fragmentHeader.GetDatagramTag();

            if (neighbor->IsLastRxFragmentTagSet())
            {
                VerifyOrExit(!neighbor->IsLastRxFragmentTagAfter(tag), error = kErrorDuplicated);

                if (neighbor->GetLastRxFragmentTag() == tag)
                {
                    VerifyOrExit(fragmentHeader.GetDatagramOffset() != 0, error = kErrorDuplicated);

                    // Duplication suppression for a "next fragment" is handled
                    // by the code below where the the datagram offset is
                    // checked against the offset of the corresponding message
                    // (same datagram tag and size) in Reassembly List. Note
                    // that if there is no matching message in the Reassembly
                    // List (e.g., in case the message is already fully
                    // assembled) the received "next fragment" frame would be
                    // dropped.
                }
            }

            neighbor->SetLastRxFragmentTag(tag);
        }
    }

#endif // OPENTHREAD_CONFIG_MULTI_RADIO

    if (fragmentHeader.GetDatagramOffset() == 0)
    {
        uint16_t datagramSize = fragmentHeader.GetDatagramSize();

#if OPENTHREAD_FTD
        UpdateRoutes(aFrameData, aMacAddrs);
#endif

        SuccessOrExit(error = FrameToMessage(aFrameData, datagramSize, aMacAddrs, message));

        VerifyOrExit(datagramSize >= message->GetLength(), error = kErrorParse);
        SuccessOrExit(error = message->SetLength(datagramSize));

        message->SetDatagramTag(fragmentHeader.GetDatagramTag());
        message->SetTimestampToNow();
        message->SetLinkInfo(aLinkInfo);

        VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);

#if OPENTHREAD_FTD
        SendIcmpErrorIfDstUnreach(*message, aMacAddrs);
#endif

        // Allow re-assembly of only one message at a time on a SED by clearing
        // any remaining fragments in reassembly list upon receiving of a new
        // (secure) first fragment.

        if (!GetRxOnWhenIdle() && message->IsLinkSecurityEnabled())
        {
            ClearReassemblyList();
        }

        mReassemblyList.Enqueue(*message);

        Get<TimeTicker>().RegisterReceiver(TimeTicker::kMeshForwarder);
    }
    else // Received frame is a "next fragment".
    {
        for (Message &msg : mReassemblyList)
        {
            // Security Check: only consider reassembly buffers that had the same Security Enabled setting.
            if (msg.GetLength() == fragmentHeader.GetDatagramSize() &&
                msg.GetDatagramTag() == fragmentHeader.GetDatagramTag() &&
                msg.GetOffset() == fragmentHeader.GetDatagramOffset() &&
                msg.GetOffset() + aFrameData.GetLength() <= fragmentHeader.GetDatagramSize() &&
                msg.IsLinkSecurityEnabled() == aLinkInfo.IsLinkSecurityEnabled())
            {
                message = &msg;
                break;
            }
        }

        // For a sleepy-end-device, if we receive a new (secure) next fragment
        // with a non-matching fragmentation offset or tag, it indicates that
        // we have either missed a fragment, or the parent has moved to a new
        // message with a new tag. In either case, we can safely clear any
        // remaining fragments stored in the reassembly list.

        if (!GetRxOnWhenIdle() && (message == nullptr) && aLinkInfo.IsLinkSecurityEnabled())
        {
            ClearReassemblyList();
        }

        VerifyOrExit(message != nullptr, error = kErrorDrop);

        message->WriteData(message->GetOffset(), aFrameData);
        message->MoveOffset(aFrameData.GetLength());
        message->AddRss(aLinkInfo.GetRss());
#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
        message->AddLqi(aLinkInfo.GetLqi());
#endif
        message->SetTimestampToNow();
    }

exit:

    if (error == kErrorNone)
    {
        if (message->GetOffset() >= message->GetLength())
        {
            mReassemblyList.Dequeue(*message);
            IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
        }
    }
    else
    {
        LogFragmentFrameDrop(error, aFrameData.GetLength(), aMacAddrs, fragmentHeader,
                             aLinkInfo.IsLinkSecurityEnabled());
        FreeMessage(message);
    }
}

void MeshForwarder::ClearReassemblyList(void)
{
    for (Message &message : mReassemblyList)
    {
        LogMessage(kMessageReassemblyDrop, message, kErrorNoFrameReceived);

        if (message.GetType() == Message::kTypeIp6)
        {
            mIpCounters.mRxFailure++;
        }

        mReassemblyList.DequeueAndFree(message);
    }
}

void MeshForwarder::HandleTimeTick(void)
{
    bool contineRxingTicks = false;

#if OPENTHREAD_FTD
    contineRxingTicks = mFragmentPriorityList.UpdateOnTimeTick();
#endif

    contineRxingTicks = UpdateReassemblyList() || contineRxingTicks;

    if (!contineRxingTicks)
    {
        Get<TimeTicker>().UnregisterReceiver(TimeTicker::kMeshForwarder);
    }
}

bool MeshForwarder::UpdateReassemblyList(void)
{
    TimeMilli now = TimerMilli::GetNow();

    for (Message &message : mReassemblyList)
    {
        if (now - message.GetTimestamp() >= TimeMilli::SecToMsec(kReassemblyTimeout))
        {
            LogMessage(kMessageReassemblyDrop, message, kErrorReassemblyTimeout);

            if (message.GetType() == Message::kTypeIp6)
            {
                mIpCounters.mRxFailure++;
            }

            mReassemblyList.DequeueAndFree(message);
        }
    }

    return mReassemblyList.GetHead() != nullptr;
}

Error MeshForwarder::FrameToMessage(const FrameData &     aFrameData,
                                    uint16_t              aDatagramSize,
                                    const Mac::Addresses &aMacAddrs,
                                    Message *&            aMessage)
{
    Error             error     = kErrorNone;
    FrameData         frameData = aFrameData;
    Message::Priority priority;

    SuccessOrExit(error = GetFramePriority(frameData, aMacAddrs, priority));

    aMessage = Get<MessagePool>().Allocate(Message::kTypeIp6, /* aReserveHeader */ 0, Message::Settings(priority));
    VerifyOrExit(aMessage, error = kErrorNoBufs);

    SuccessOrExit(error = Get<Lowpan::Lowpan>().Decompress(*aMessage, aMacAddrs, frameData, aDatagramSize));

    SuccessOrExit(error = aMessage->AppendData(frameData));
    aMessage->MoveOffset(frameData.GetLength());

exit:
    return error;
}

void MeshForwarder::HandleLowpanHC(const FrameData &     aFrameData,
                                   const Mac::Addresses &aMacAddrs,
                                   const ThreadLinkInfo &aLinkInfo)
{
    Error    error   = kErrorNone;
    Message *message = nullptr;

#if OPENTHREAD_FTD
    UpdateRoutes(aFrameData, aMacAddrs);
#endif

    SuccessOrExit(error = FrameToMessage(aFrameData, 0, aMacAddrs, message));

    message->SetLinkInfo(aLinkInfo);

    VerifyOrExit(Get<Ip6::Filter>().Accept(*message), error = kErrorDrop);

#if OPENTHREAD_FTD
    SendIcmpErrorIfDstUnreach(*message, aMacAddrs);
#endif

exit:

    if (error == kErrorNone)
    {
        IgnoreError(HandleDatagram(*message, aLinkInfo, aMacAddrs.mSource));
    }
    else
    {
        LogLowpanHcFrameDrop(error, aFrameData.GetLength(), aMacAddrs, aLinkInfo.IsLinkSecurityEnabled());
        FreeMessage(message);
    }
}

Error MeshForwarder::HandleDatagram(Message &aMessage, const ThreadLinkInfo &aLinkInfo, const Mac::Address &aMacSource)
{
    ThreadNetif &netif = Get<ThreadNetif>();

#if OPENTHREAD_CONFIG_HISTORY_TRACKER_ENABLE
    Get<Utils::HistoryTracker>().RecordRxMessage(aMessage, aMacSource);
#endif

    LogMessage(kMessageReceive, aMessage, kErrorNone, &aMacSource);

    if (aMessage.GetType() == Message::kTypeIp6)
    {
        mIpCounters.mRxSuccess++;
    }

    return Get<Ip6::Ip6>().HandleDatagram(aMessage, &netif, &aLinkInfo, false);
}

Error MeshForwarder::GetFramePriority(const FrameData &     aFrameData,
                                      const Mac::Addresses &aMacAddrs,
                                      Message::Priority &   aPriority)
{
    Error        error = kErrorNone;
    Ip6::Headers headers;

    SuccessOrExit(error = headers.DecompressFrom(aFrameData, aMacAddrs, GetInstance()));

    aPriority = Ip6::Ip6::DscpToPriority(headers.GetIp6Header().GetDscp());

    // Only ICMPv6 error messages are prioritized.
    if (headers.IsIcmp6() && headers.GetIcmpHeader().IsError())
    {
        aPriority = Message::kPriorityNet;
    }

    if (headers.IsUdp())
    {
        uint16_t destPort = headers.GetUdpHeader().GetDestinationPort();

        if ((destPort == Mle::kUdpPort) || (destPort == Tmf::kUdpPort))
        {
            aPriority = Message::kPriorityNet;
        }
    }

exit:
    return error;
}

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
Error MeshForwarder::SendEmptyMessage(void)
{
    Error    error   = kErrorNone;
    Message *message = nullptr;

    VerifyOrExit(mEnabled && !Get<Mac::Mac>().GetRxOnWhenIdle() &&
                     Get<Mle::MleRouter>().GetParent().IsStateValidOrRestoring(),
                 error = kErrorInvalidState);

    message = Get<MessagePool>().Allocate(Message::kTypeMacEmptyData);
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    SuccessOrExit(error = SendMessage(*message));

exit:
    FreeMessageOnError(message, error);
    LogDebg("Send empty message, error:%s", ErrorToString(error));
    return error;
}
#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE

bool MeshForwarder::CalcIePresent(const Message *aMessage)
{
    bool iePresent = false;

    OT_UNUSED_VARIABLE(aMessage);

#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    iePresent |= (aMessage != nullptr && aMessage->IsTimeSync());
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
    iePresent |= Get<Mac::Mac>().IsCslEnabled();
#endif
#endif

    return iePresent;
}

#if OPENTHREAD_CONFIG_MAC_HEADER_IE_SUPPORT
void MeshForwarder::AppendHeaderIe(const Message *aMessage, Mac::TxFrame &aFrame)
{
    uint8_t index     = 0;
    bool    iePresent = false;
    bool    payloadPresent =
        (aFrame.GetType() == Mac::Frame::kFcfFrameMacCmd) || (aMessage != nullptr && aMessage->GetLength() != 0);

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (aMessage != nullptr && aMessage->IsTimeSync())
    {
        IgnoreError(aFrame.AppendHeaderIeAt<Mac::TimeIe>(index));
        iePresent = true;
    }
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
    if (Get<Mac::Mac>().IsCslEnabled())
    {
        IgnoreError(aFrame.AppendHeaderIeAt<Mac::CslIe>(index));
        aFrame.mInfo.mTxInfo.mCslPresent = true;
        iePresent                        = true;
    }
    else
    {
        aFrame.mInfo.mTxInfo.mCslPresent = false;
    }
#endif

    if (iePresent && payloadPresent)
    {
        // Assume no Payload IE in current implementation
        IgnoreError(aFrame.AppendHeaderIeAt<Mac::Termination2Ie>(index));
    }
}
#endif

uint16_t MeshForwarder::CalcFrameVersion(const Neighbor *aNeighbor, bool aIePresent)
{
    uint16_t version = Mac::Frame::kFcfFrameVersion2006;
    OT_UNUSED_VARIABLE(aNeighbor);

    if (aIePresent)
    {
        version = Mac::Frame::kFcfFrameVersion2015;
    }
#if OPENTHREAD_FTD && OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
    else if (aNeighbor != nullptr && !Mle::MleRouter::IsActiveRouter(aNeighbor->GetRloc16()) &&
             static_cast<const Child *>(aNeighbor)->IsCslSynchronized())
    {
        version = Mac::Frame::kFcfFrameVersion2015;
    }
#endif
#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE
    else if (aNeighbor != nullptr && aNeighbor->IsEnhAckProbingActive())
    {
        version = Mac::Frame::kFcfFrameVersion2015; ///< Set version to 2015 to fetch Link Metrics data in Enh-ACK.
    }
#endif

    return version;
}

// LCOV_EXCL_START

#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_NOTE)

const char *MeshForwarder::MessageActionToString(MessageAction aAction, Error aError)
{
    static const char *const kMessageActionStrings[] = {
        "Received",                    // (0) kMessageReceive
        "Sent",                        // (1) kMessageTransmit
        "Prepping indir tx",           // (2) kMessagePrepareIndirect
        "Dropping",                    // (3) kMessageDrop
        "Dropping (reassembly queue)", // (4) kMessageReassemblyDrop
        "Evicting",                    // (5) kMessageEvict
#if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
        "Marked ECN",            // (6) kMessageMarkEcn
        "Dropping (queue mgmt)", // (7) kMessageQueueMgmtDrop
#endif
#if (OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE > 0)
        "Dropping (dir queue full)", // (8) kMessageFullQueueDrop
#endif
    };

    const char *string = kMessageActionStrings[aAction];

    static_assert(kMessageReceive == 0, "kMessageReceive value is incorrect");
    static_assert(kMessageTransmit == 1, "kMessageTransmit value is incorrect");
    static_assert(kMessagePrepareIndirect == 2, "kMessagePrepareIndirect value is incorrect");
    static_assert(kMessageDrop == 3, "kMessageDrop value is incorrect");
    static_assert(kMessageReassemblyDrop == 4, "kMessageReassemblyDrop value is incorrect");
    static_assert(kMessageEvict == 5, "kMessageEvict value is incorrect");
#if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
    static_assert(kMessageMarkEcn == 6, "kMessageMarkEcn is incorrect");
    static_assert(kMessageQueueMgmtDrop == 7, "kMessageQueueMgmtDrop is incorrect");
#if (OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE > 0)
    static_assert(kMessageFullQueueDrop == 8, "kMessageFullQueueDrop is incorrect");
#endif
#else
#if (OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE > 0)
    static_assert(kMessageFullQueueDrop == 6, "kMessageFullQueueDrop is incorrect");
#endif
#endif

    if ((aAction == kMessageTransmit) && (aError != kErrorNone))
    {
        string = "Failed to send";
    }

    return string;
}

const char *MeshForwarder::MessagePriorityToString(const Message &aMessage)
{
    return Message::PriorityToString(aMessage.GetPriority());
}

#if OPENTHREAD_CONFIG_LOG_SRC_DST_IP_ADDRESSES
void MeshForwarder::LogIp6SourceDestAddresses(const Ip6::Headers &aHeaders, LogLevel aLogLevel)
{
    uint16_t srcPort = aHeaders.GetSourcePort();
    uint16_t dstPort = aHeaders.GetDestinationPort();

    if (srcPort != 0)
    {
        LogAt(aLogLevel, "    src:[%s]:%d", aHeaders.GetSourceAddress().ToString().AsCString(), srcPort);
    }
    else
    {
        LogAt(aLogLevel, "    src:[%s]", aHeaders.GetSourceAddress().ToString().AsCString());
    }

    if (dstPort != 0)
    {
        LogAt(aLogLevel, "    dst:[%s]:%d", aHeaders.GetDestinationAddress().ToString().AsCString(), dstPort);
    }
    else
    {
        LogAt(aLogLevel, "    dst:[%s]", aHeaders.GetDestinationAddress().ToString().AsCString());
    }
}
#else
void MeshForwarder::LogIp6SourceDestAddresses(const Ip6::Headers &, LogLevel)
{
}
#endif

void MeshForwarder::LogIp6Message(MessageAction       aAction,
                                  const Message &     aMessage,
                                  const Mac::Address *aMacAddress,
                                  Error               aError,
                                  LogLevel            aLogLevel)
{
    Ip6::Headers headers;
    bool         shouldLogRss;
    bool         shouldLogRadio = false;
    const char * radioString    = "";

    SuccessOrExit(headers.ParseFrom(aMessage));

    shouldLogRss = (aAction == kMessageReceive) || (aAction == kMessageReassemblyDrop);

#if OPENTHREAD_CONFIG_MULTI_RADIO
    shouldLogRadio = true;
    radioString    = aMessage.IsRadioTypeSet() ? RadioTypeToString(aMessage.GetRadioType()) : "all";
#endif

    LogAt(aLogLevel, "%s IPv6 %s msg, len:%d, chksum:%04x, ecn:%s%s%s, sec:%s%s%s, prio:%s%s%s%s%s",
          MessageActionToString(aAction, aError), Ip6::Ip6::IpProtoToString(headers.GetIpProto()), aMessage.GetLength(),
          headers.GetChecksum(), Ip6::Ip6::EcnToString(headers.GetEcn()),
          (aMacAddress == nullptr) ? "" : ((aAction == kMessageReceive) ? ", from:" : ", to:"),
          (aMacAddress == nullptr) ? "" : aMacAddress->ToString().AsCString(),
          ToYesNo(aMessage.IsLinkSecurityEnabled()),
          (aError == kErrorNone) ? "" : ", error:", (aError == kErrorNone) ? "" : ErrorToString(aError),
          MessagePriorityToString(aMessage), shouldLogRss ? ", rss:" : "",
          shouldLogRss ? aMessage.GetRssAverager().ToString().AsCString() : "", shouldLogRadio ? ", radio:" : "",
          radioString);

    if (aAction != kMessagePrepareIndirect)
    {
        LogIp6SourceDestAddresses(headers, aLogLevel);
    }

exit:
    return;
}

void MeshForwarder::LogMessage(MessageAction       aAction,
                               const Message &     aMessage,
                               Error               aError,
                               const Mac::Address *aMacAddress)

{
    LogLevel logLevel = kLogLevelInfo;

    switch (aAction)
    {
    case kMessageReceive:
    case kMessageTransmit:
    case kMessagePrepareIndirect:
#if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
    case kMessageMarkEcn:
#endif
        logLevel = (aError == kErrorNone) ? kLogLevelInfo : kLogLevelNote;
        break;

    case kMessageDrop:
    case kMessageReassemblyDrop:
    case kMessageEvict:
#if OPENTHREAD_CONFIG_DELAY_AWARE_QUEUE_MANAGEMENT_ENABLE
    case kMessageQueueMgmtDrop:
#endif
#if (OPENTHREAD_CONFIG_MAX_FRAMES_IN_DIRECT_TX_QUEUE > 0)
    case kMessageFullQueueDrop:
#endif
        logLevel = kLogLevelNote;
        break;
    }

    VerifyOrExit(Instance::GetLogLevel() >= logLevel);

    switch (aMessage.GetType())
    {
    case Message::kTypeIp6:
        LogIp6Message(aAction, aMessage, aMacAddress, aError, logLevel);
        break;

#if OPENTHREAD_FTD
    case Message::kType6lowpan:
        LogMeshMessage(aAction, aMessage, aMacAddress, aError, logLevel);
        break;
#endif

    default:
        break;
    }

exit:
    return;
}

void MeshForwarder::LogFrame(const char *aActionText, const Mac::Frame &aFrame, Error aError)
{
    if (aError != kErrorNone)
    {
        LogNote("%s, aError:%s, %s", aActionText, ErrorToString(aError), aFrame.ToInfoString().AsCString());
    }
    else
    {
        LogInfo("%s, %s", aActionText, aFrame.ToInfoString().AsCString());
    }
}

void MeshForwarder::LogFragmentFrameDrop(Error                         aError,
                                         uint16_t                      aFrameLength,
                                         const Mac::Addresses &        aMacAddrs,
                                         const Lowpan::FragmentHeader &aFragmentHeader,
                                         bool                          aIsSecure)
{
    LogNote("Dropping rx frag frame, error:%s, len:%d, src:%s, dst:%s, tag:%d, offset:%d, dglen:%d, sec:%s",
            ErrorToString(aError), aFrameLength, aMacAddrs.mSource.ToString().AsCString(),
            aMacAddrs.mDestination.ToString().AsCString(), aFragmentHeader.GetDatagramTag(),
            aFragmentHeader.GetDatagramOffset(), aFragmentHeader.GetDatagramSize(), ToYesNo(aIsSecure));
}

void MeshForwarder::LogLowpanHcFrameDrop(Error                 aError,
                                         uint16_t              aFrameLength,
                                         const Mac::Addresses &aMacAddrs,
                                         bool                  aIsSecure)
{
    LogNote("Dropping rx lowpan HC frame, error:%s, len:%d, src:%s, dst:%s, sec:%s", ErrorToString(aError),
            aFrameLength, aMacAddrs.mSource.ToString().AsCString(), aMacAddrs.mDestination.ToString().AsCString(),
            ToYesNo(aIsSecure));
}

#else // #if OT_SHOULD_LOG_AT( OT_LOG_LEVEL_NOTE)

void MeshForwarder::LogMessage(MessageAction, const Message &, Error, const Mac::Address *)
{
}

void MeshForwarder::LogFrame(const char *, const Mac::Frame &, Error)
{
}

void MeshForwarder::LogFragmentFrameDrop(Error, uint16_t, const Mac::Addresses &, const Lowpan::FragmentHeader &, bool)
{
}

void MeshForwarder::LogLowpanHcFrameDrop(Error, uint16_t, const Mac::Addresses &, bool)
{
}

#endif // #if OT_SHOULD_LOG_AT( OT_LOG_LEVEL_NOTE)

// LCOV_EXCL_STOP

} // namespace ot
