/*
 * Copyright (c) 2024 Tokushima University, Japan
 *
 * SPDX-License-Identifier: GPL-2.0-only
 *
 * Authors:
 *
 *  Alberto Gallegos Ramonet <alramonet@is.tokushima-u.ac.jp>
 *  Ryo Okuda <c611901200@tokushima-u.ac.jp>
 */

#include "zigbee-nwk.h"

#include "zigbee-nwk-tables.h"

#include "ns3/log.h"
#include "ns3/packet.h"
#include "ns3/simulator.h"

using namespace ns3::lrwpan;

#undef NS_LOG_APPEND_CONTEXT
#define NS_LOG_APPEND_CONTEXT                                                                      \
    std::clog << "[" << m_nwkNetworkAddress << " | " << m_nwkIeeeAddress << "] ";

namespace ns3
{
namespace zigbee
{

NS_LOG_COMPONENT_DEFINE("ZigbeeNwk");
NS_OBJECT_ENSURE_REGISTERED(ZigbeeNwk);

TypeId
ZigbeeNwk::GetTypeId()
{
    static TypeId tid =
        TypeId("ns3::zigbee::ZigbeeNwk")
            .SetParent<Object>()
            .SetGroupName("Zigbee")
            .AddConstructor<ZigbeeNwk>()
            .AddAttribute("NwkcCoordinatorCapable",
                          "[Constant] Indicates whether the device is capable of becoming a"
                          "Zigbee coordinator.",
                          BooleanValue(true),
                          MakeBooleanAccessor(&ZigbeeNwk::m_nwkcCoordinatorCapable),
                          MakeBooleanChecker())
            .AddAttribute("NwkcProtocolVersion",
                          "[Constant] The version of the Zigbee NWK protocol in"
                          "the device (placeholder)",
                          UintegerValue(0x02),
                          MakeUintegerAccessor(&ZigbeeNwk::m_nwkcProtocolVersion),
                          MakeUintegerChecker<uint8_t>())
            .AddAttribute("NwkcRouteDiscoveryTime",
                          "[Constant] The duration until a route discovery expires",
                          TimeValue(MilliSeconds(0x2710)),
                          MakeTimeAccessor(&ZigbeeNwk::m_nwkcRouteDiscoveryTime),
                          MakeTimeChecker())
            .AddAttribute("NwkcInitialRREQRetries",
                          "[Constant] The number of times the first broadcast transmission"
                          "of a RREQ cmd frame is retried.",
                          UintegerValue(0x03),
                          MakeUintegerAccessor(&ZigbeeNwk::m_nwkcInitialRREQRetries),
                          MakeUintegerChecker<uint8_t>())
            .AddAttribute("NwkcRREQRetries",
                          "[Constant] The number of times the broadcast transmission of a"
                          "RREQ cmd frame is retried on relay by intermediate router or"
                          "coordinator.",
                          UintegerValue(0x02),
                          MakeUintegerAccessor(&ZigbeeNwk::m_nwkcRREQRetries),
                          MakeUintegerChecker<uint8_t>())
            .AddAttribute("NwkcRREQRetryInterval",
                          "[Constant] The duration between retries of a broadcast RREQ "
                          "cmd frame.",
                          TimeValue(MilliSeconds(0xFE)),
                          MakeTimeAccessor(&ZigbeeNwk::m_nwkcRREQRetryInterval),
                          MakeTimeChecker())
            .AddAttribute("NwkcMinRREQJitter",
                          "[Constant] The minimum jitter for broadcast retransmission "
                          "of a RREQ (msec)",
                          DoubleValue(2),
                          MakeDoubleAccessor(&ZigbeeNwk::m_nwkcMinRREQJitter),
                          MakeDoubleChecker<double>())
            .AddAttribute("NwkcMaxRREQJitter",
                          "[Constant] The duration between retries of a broadcast RREQ (msec)",
                          DoubleValue(128),
                          MakeDoubleAccessor(&ZigbeeNwk::m_nwkcMaxRREQJitter),
                          MakeDoubleChecker<double>())
            .AddAttribute("NwkcMaxBroadcastJitter",
                          "[Constant] The duration between retries of a broadcast RREQ (msec)",
                          DoubleValue(40),
                          MakeDoubleAccessor(&ZigbeeNwk::m_nwkcMaxBroadcastJitter),
                          MakeDoubleChecker<double>())
            .AddAttribute("nwkMaxBroadcastRetries",
                          "The maximum number of retries allowed after a broadcast "
                          "transmission failure.",
                          UintegerValue(0x03),
                          MakeUintegerAccessor(&ZigbeeNwk::m_nwkMaxBroadcastRetries),
                          MakeUintegerChecker<uint8_t>())
            .AddAttribute("NwkPassiveAckTimeout",
                          "The maximum time duration in milliseconds allowed for the parent "
                          "all the child devices to retransmit a broadcast message.",
                          TimeValue(MilliSeconds(500)),
                          MakeTimeAccessor(&ZigbeeNwk::m_nwkPassiveAckTimeout),
                          MakeTimeChecker())
            .AddAttribute("MaxPendingTxQueueSize",
                          "The maximum size of the table storing pending packets awaiting "
                          "to be transmitted after discovering a route to the destination.",
                          UintegerValue(10),
                          MakeUintegerAccessor(&ZigbeeNwk::m_maxPendingTxQueueSize),
                          MakeUintegerChecker<uint32_t>())
            .AddTraceSource("RreqRetriesExhausted",
                            "Trace source indicating when a node has "
                            "reached the maximum allowed number of RREQ retries during a "
                            "route discovery request",
                            MakeTraceSourceAccessor(&ZigbeeNwk::m_rreqRetriesExhaustedTrace),
                            "ns3::zigbee::ZigbeeNwk::RreqRetriesExhaustedTracedCallback");
    return tid;
}

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

void
ZigbeeNwk::NotifyConstructionCompleted()
{
    NS_LOG_FUNCTION(this);

    m_pendPrimitiveNwk = PendingPrimitiveNwk::NLDE_NLME_NONE;
    m_uniformRandomVariable = CreateObject<UniformRandomVariable>();
    m_uniformRandomVariable->SetAttribute("Min", DoubleValue(0.0));
    m_uniformRandomVariable->SetAttribute("Max", DoubleValue(255.0));

    m_scanEnergyThreshold = 127;

    m_netFormParams = {};
    m_netFormParamsGen = nullptr;
    m_beaconPayload = nullptr;
    m_nwkNetworkAddress = Mac16Address("ff:ff");
    m_nwkPanId = 0xffff;
    m_nwkExtendedPanId = 0xffffffffffffffff;
    m_nwkCapabilityInformation = 0;

    m_nwkStackProfile = ZIGBEE_PRO;
    m_nwkAddrAlloc = STOCHASTIC_ALLOC;
    m_nwkMaxDepth = 5;
    m_nwkMaxChildren = 20;
    m_nwkMaxRouters = 6;
    m_nwkEndDeviceTimeoutDefault = 8;
    m_nwkUseTreeRouting = false;

    m_nwkReportConstantCost = false;
    m_nwkSymLink = false;

    m_countRREQRetries = 0;

    m_nwkIsConcentrator = false;
    m_nwkConcentratorRadius = 5;
    m_nwkConcentratorDiscoveryTime = 0x00;

    // TODO, set according to equation 3.5.2.1?
    m_nwkNetworkBroadcastDeliveryTime = Seconds(9);

    m_nwkSequenceNumber = SequenceNumber8(m_uniformRandomVariable->GetValue());
    m_routeRequestId = SequenceNumber8(m_uniformRandomVariable->GetValue());
    m_macHandle = SequenceNumber8(m_uniformRandomVariable->GetValue());
    m_txBufferMaxSize = 10;

    m_rreqJitter = CreateObject<UniformRandomVariable>();
    m_rreqJitter->SetAttribute("Min", DoubleValue(m_nwkcMinRREQJitter));
    m_rreqJitter->SetAttribute("Max", DoubleValue(m_nwkcMaxRREQJitter));

    m_bcstJitter = CreateObject<UniformRandomVariable>();
    m_bcstJitter->SetAttribute("Min", DoubleValue(0));
    m_bcstJitter->SetAttribute("Max", DoubleValue(m_nwkcMaxBroadcastJitter));

    m_routeExpiryTime = Seconds(255);
}

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

void
ZigbeeNwk::DoInitialize()
{
    NS_LOG_FUNCTION(this);
    Object::DoInitialize();
}

void
ZigbeeNwk::DoDispose()
{
    NS_LOG_FUNCTION(this);
    m_panIdTable.Dispose();
    m_nwkNeighborTable.Dispose();
    m_nwkRoutingTable.Dispose();
    m_nwkRouteDiscoveryTable.Dispose();
    m_rreqRetryTable.Dispose();
    m_panIdTable.Dispose();
    m_btt.Dispose();

    DisposeTxPktBuffer();
    DisposePendingTx();

    m_nlmeDirectJoinConfirmCallback = MakeNullCallback<void, NlmeDirectJoinConfirmParams>();
    m_nlmeJoinConfirmCallback = MakeNullCallback<void, NlmeJoinConfirmParams>();
    m_nlmeJoinIndicationCallback = MakeNullCallback<void, NlmeJoinIndicationParams>();
    m_nlmeNetworkDiscoveryConfirmCallback =
        MakeNullCallback<void, NlmeNetworkDiscoveryConfirmParams>();
    m_nlmeNetworkFormationConfirmCallback =
        MakeNullCallback<void, NlmeNetworkFormationConfirmParams>();
    m_nlmeRouteDiscoveryConfirmCallback = MakeNullCallback<void, NlmeRouteDiscoveryConfirmParams>();
    m_nlmeStartRouterConfirmCallback = MakeNullCallback<void, NlmeStartRouterConfirmParams>();

    m_nldeDataConfirmCallback = MakeNullCallback<void, NldeDataConfirmParams>();
    m_nldeDataIndicationCallback = MakeNullCallback<void, NldeDataIndicationParams, Ptr<Packet>>();

    m_mac = nullptr;

    Object::DoDispose();
}

void
ZigbeeNwk::SetMac(Ptr<LrWpanMacBase> mac)
{
    m_mac = mac;
}

void
ZigbeeNwk::SetGroupTable(Ptr<ZigbeeGroupTable> groupTable)
{
    m_nwkGroupIdTable = groupTable;
}

Ptr<LrWpanMacBase>
ZigbeeNwk::GetMac() const
{
    return m_mac;
}

void
ZigbeeNwk::PrintRoutingTable(Ptr<OutputStreamWrapper> stream) const
{
    std::ostream* os = stream->GetStream();
    std::ios oldState(nullptr);
    oldState.copyfmt(*os);

    std::ostringstream nwkAddr;
    std::ostringstream ieeeAddr;

    nwkAddr << m_nwkNetworkAddress;
    ieeeAddr << m_nwkIeeeAddress;

    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
    *os << "[" << ieeeAddr.str() << " | " << nwkAddr.str() << "] | ";
    *os << "Time: " << Simulator::Now().As(Time::S) << " | ";
    m_nwkRoutingTable.Print(stream);
}

void
ZigbeeNwk::PrintRouteDiscoveryTable(Ptr<OutputStreamWrapper> stream)
{
    std::ostream* os = stream->GetStream();
    std::ios oldState(nullptr);
    oldState.copyfmt(*os);

    std::ostringstream nwkAddr;
    std::ostringstream ieeeAddr;

    nwkAddr << m_nwkNetworkAddress;
    ieeeAddr << m_nwkIeeeAddress;

    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
    *os << "[" << ieeeAddr.str() << " | " << nwkAddr.str() << "] | ";
    *os << "Time: " << Simulator::Now().As(Time::S) << " | ";
    m_nwkRouteDiscoveryTable.Print(stream);
}

void
ZigbeeNwk::PrintNeighborTable(Ptr<OutputStreamWrapper> stream) const
{
    std::ostream* os = stream->GetStream();
    std::ios oldState(nullptr);
    oldState.copyfmt(*os);

    std::ostringstream nwkAddr;
    std::ostringstream ieeeAddr;

    nwkAddr << m_nwkNetworkAddress;
    ieeeAddr << m_nwkIeeeAddress;

    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
    *os << "[" << ieeeAddr.str() << " | " << nwkAddr.str() << "] | ";
    *os << "Time: " << Simulator::Now().As(Time::S) << " | ";
    m_nwkNeighborTable.Print(stream);
}

void
ZigbeeNwk::PrintRREQRetryTable(Ptr<OutputStreamWrapper> stream) const
{
    std::ostream* os = stream->GetStream();
    std::ios oldState(nullptr);
    oldState.copyfmt(*os);

    std::ostringstream nwkAddr;
    std::ostringstream ieeeAddr;

    nwkAddr << m_nwkNetworkAddress;
    ieeeAddr << m_nwkIeeeAddress;

    *os << std::resetiosflags(std::ios::adjustfield) << std::setiosflags(std::ios::left);
    *os << "[" << ieeeAddr.str() << " | " << nwkAddr.str() << "] | ";
    *os << "Time: " << Simulator::Now().As(Time::S) << " | ";
    m_rreqRetryTable.Print(stream);
}

Mac16Address
ZigbeeNwk::FindRoute(Mac16Address dst, bool& neighbor)
{
    Ptr<NeighborTableEntry> neighborEntry;
    if (m_nwkNeighborTable.LookUpEntry(dst, neighborEntry))
    {
        neighbor = true;
        return dst;
    }

    Ptr<RoutingTableEntry> entry;
    if (m_nwkRoutingTable.LookUpEntry(dst, entry))
    {
        if (entry->GetStatus() == ROUTE_ACTIVE)
        {
            neighbor = false;
            return entry->GetNextHopAddr();
        }
    }

    neighbor = false;
    return Mac16Address("FF:FF"); // route not found
}

Mac16Address
ZigbeeNwk::GetNetworkAddress() const
{
    return m_nwkNetworkAddress;
}

Mac64Address
ZigbeeNwk::GetIeeeAddress() const
{
    return m_nwkIeeeAddress;
}

void
ZigbeeNwk::McpsDataIndication(McpsDataIndicationParams params, Ptr<Packet> msdu)
{
    NS_LOG_FUNCTION(this);

    ZigbeeNwkHeader nwkHeader;
    msdu->RemoveHeader(nwkHeader);

    // Decrease the radius in the network header as it might be retransmitted
    // to a next hop.
    uint8_t radius = nwkHeader.GetRadius();
    nwkHeader.SetRadius(radius - 1);

    // Check if the received frame is from a neighbor and update LQI if necessary
    Ptr<NeighborTableEntry> neighborEntry;
    if (m_nwkNeighborTable.LookUpEntry(nwkHeader.GetSrcAddr(), neighborEntry))
    {
        neighborEntry->SetLqi(params.m_mpduLinkQuality);
        neighborEntry->SetOutgoingCost(GetLQINonLinearValue(params.m_mpduLinkQuality));
    }

    switch (nwkHeader.GetFrameType())
    {
    case DATA: {
        if (nwkHeader.IsMulticast())
        {
            // DATA MULTICAST
            if (nwkHeader.GetMulticastMode() == MulticastMode::MEMBER)
            {
                // Mcst Member Mode,Zigbee specification r22.1.0 Section 3.6.6.3
                ReceiveMulticastMemberFrame(nwkHeader, msdu, params);
            }
            else
            {
                // Mcst Non-Member Mode,Zigbee specification r22.1.0 Section 3.6.6.4
                if (m_nwkGroupIdTable->IsGroupMember(nwkHeader.GetDstAddr().ConvertToInt()))
                {
                    // Treat frame as a multicast member
                    nwkHeader.SetMulticastMode(MulticastMode::MEMBER);
                    ReceiveMulticastMemberFrame(nwkHeader, msdu, params);
                }
                else
                {
                    ReceiveMulticastNonMemberFrame(nwkHeader, msdu, params);
                }
            }
        }
        else if (IsBroadcastAddress(nwkHeader.GetDstAddr()))
        {
            // DATA BROADCAST

            Ptr<BroadcastTransactionRecord> btr;
            if (m_btt.LookUpEntry(nwkHeader.GetSeqNum(), btr))
            {
                return;
            }

            // Check the capabilities of the current device
            CapabilityInformation capability;
            capability.SetCapability(m_nwkCapabilityInformation);

            bool rebroadcastFlag = false;

            if (nwkHeader.GetDstAddr() == 0xFFFF ||
                (nwkHeader.GetDstAddr() == 0xFFFD && capability.IsReceiverOnWhenIdle()) ||
                (nwkHeader.GetDstAddr() == 0xFFFC && capability.GetDeviceType() != ENDDEVICE))
            {
                rebroadcastFlag = true;
            }
            else if (nwkHeader.GetDstAddr() == 0xFFFB)
            {
                NS_FATAL_ERROR("Broadcast to low power routers not supported");
                return;
            }

            if (rebroadcastFlag && nwkHeader.GetRadius() > 0)
            {
                Ptr<Packet> rebroadcastPkt = msdu->Copy();
                rebroadcastPkt->AddHeader(nwkHeader);
                SendDataBcst(rebroadcastPkt, 0);
            }

            // Add a new broadcast transaction record to the
            // broadcasst transaction table
            btr = Create<BroadcastTransactionRecord>(nwkHeader.GetSrcAddr(),
                                                     nwkHeader.GetSeqNum(),
                                                     Simulator::Now() +
                                                         m_nwkNetworkBroadcastDeliveryTime);
            m_btt.AddEntry(btr);

            if (!m_nldeDataIndicationCallback.IsNull())
            {
                NldeDataIndicationParams dataParams;
                dataParams.m_srcAddr = nwkHeader.GetSrcAddr();
                dataParams.m_dstAddr = nwkHeader.GetDstAddr();
                dataParams.m_dstAddrMode = UCST_BCST;
                dataParams.m_linkQuality = params.m_mpduLinkQuality;
                dataParams.m_nsduLength = msdu->GetSize();
                dataParams.m_rxTime = Simulator::Now();
                dataParams.m_securityUse = false;
                m_nldeDataIndicationCallback(dataParams, msdu);
            }
        }
        else
        {
            // DATA UNICAST
            if (nwkHeader.GetDstAddr() == m_nwkNetworkAddress)
            {
                // Zigbee specification r22.1.0 Sections 3.6.2.2 and 3.6.3.3
                if (!m_nldeDataIndicationCallback.IsNull())
                {
                    NldeDataIndicationParams dataParams;
                    dataParams.m_srcAddr = nwkHeader.GetSrcAddr();
                    dataParams.m_dstAddr = nwkHeader.GetDstAddr();
                    dataParams.m_dstAddrMode = UCST_BCST;
                    dataParams.m_linkQuality = params.m_mpduLinkQuality;
                    dataParams.m_nsduLength = msdu->GetSize();
                    dataParams.m_rxTime = Simulator::Now();
                    dataParams.m_securityUse = false;
                    m_nldeDataIndicationCallback(dataParams, msdu);
                }
            }
            else
            {
                // This is not the packet destination,
                // Add again the network header to the DATA packet and
                // route the packet to the next hop
                msdu->AddHeader(nwkHeader);
                SendDataUcst(msdu, 0);
            }
        }
        break;
    }
    case NWK_COMMAND: {
        ZigbeePayloadType payloadType;
        msdu->RemoveHeader(payloadType);

        if (payloadType.GetCmdType() == ROUTE_REQ_CMD || payloadType.GetCmdType() == ROUTE_REP_CMD)
        {
            CapabilityInformation capability;
            capability.SetCapability(m_nwkCapabilityInformation);
            if (capability.GetDeviceType() != ROUTER)
            {
                // Received RREQ  or RREP but the device
                // has no routing capabilities
                return;
            }
        }
        // NOTE: this cover the cases for MESH routing
        // TREE routing is not supported
        uint8_t linkCost = GetLinkCost(params.m_mpduLinkQuality);

        if (payloadType.GetCmdType() == ROUTE_REQ_CMD)
        {
            ZigbeePayloadRouteRequestCommand payload;
            msdu->RemoveHeader(payload);
            // Zigbee specification r22.1.0 Section 3.6.3.5.2
            ReceiveRREQ(params.m_srcAddr, linkCost, nwkHeader, payload);
        }
        else if (payloadType.GetCmdType() == ROUTE_REP_CMD)
        {
            ZigbeePayloadRouteReplyCommand payload;
            msdu->RemoveHeader(payload);
            // Zigbee specification r22.1.0 Section 3.6.3.5.3
            ReceiveRREP(params.m_srcAddr, linkCost, nwkHeader, payload);
        }
        break;
    }
    case INTER_PAN:
        NS_LOG_ERROR("Inter PAN frame received but not supported");
        break;
    default:
        NS_LOG_ERROR("Unknown frame received in NWK layer");
    }
}

void
ZigbeeNwk::ReceiveRREQ(Mac16Address macSrcAddr,
                       uint8_t linkCost,
                       ZigbeeNwkHeader nwkHeader,
                       ZigbeePayloadRouteRequestCommand payload)
{
    NS_LOG_FUNCTION(this);

    if (nwkHeader.GetSrcAddr() == m_nwkNetworkAddress)
    {
        // I am the original initiator of the RREQ, ignore request
        return;
    }

    // Calculate the pathcost on the RREQ receiving device
    uint8_t pathCost = linkCost + payload.GetPathCost();

    // Many-to-one routing

    if (payload.GetCmdOptManyToOneField() != ManyToOne::NO_MANY_TO_ONE)
    {
        RouteDiscoveryStatus routeStatus =
            ProcessManyToOneRoute(macSrcAddr, pathCost, nwkHeader, payload);

        // Update the path cost of the RREQ
        payload.SetPathCost(pathCost);

        // Note: At this point we already have the updated radius, which was updated as soon
        // as the frame was received (i.e. In the MCPS-DATA.indication).

        if (routeStatus == MANY_TO_ONE_ROUTE || routeStatus == ROUTE_UPDATED)
        {
            Simulator::Schedule(MilliSeconds(m_rreqJitter->GetValue()),
                                &ZigbeeNwk::SendRREQ,
                                this,
                                nwkHeader,
                                payload,
                                0);
            m_nwkSequenceNumber++;
            m_routeRequestId++;
        }
        return;
    }

    // Mesh Routing (Unicast or Multicast Group)

    Mac16Address nextHop;
    RouteDiscoveryStatus nextHopStatus =
        FindNextHop(macSrcAddr, pathCost, nwkHeader, payload, nextHop);

    if (payload.GetDstAddr() == m_nwkNetworkAddress || nextHopStatus == ROUTE_FOUND)
    {
        // RREQ is for this device or its children
        NS_LOG_DEBUG("RREQ is for me or my children, sending a RREP to [" << macSrcAddr << "]");

        SendRREP(macSrcAddr,
                 nwkHeader.GetSrcAddr(),
                 payload.GetDstAddr(),
                 payload.GetRouteReqId(),
                 pathCost);
    }
    else if (nextHopStatus == ROUTE_NOT_FOUND || nextHopStatus == ROUTE_UPDATED)
    {
        NS_LOG_DEBUG("Route for device [" << payload.GetDstAddr()
                                          << "] not found, forwarding RREQ");

        // Update path cost and resend the RREQ
        payload.SetPathCost(pathCost);
        Simulator::Schedule(MilliSeconds(m_rreqJitter->GetValue()),
                            &ZigbeeNwk::SendRREQ,
                            this,
                            nwkHeader,
                            payload,
                            m_nwkcRREQRetries);
    }
}

void
ZigbeeNwk::ReceiveRREP(Mac16Address macSrcAddr,
                       uint8_t linkCost,
                       ZigbeeNwkHeader nwkHeader,
                       ZigbeePayloadRouteReplyCommand payload)
{
    NS_LOG_FUNCTION(this);

    // RREP received, cancel any ongoing RREQ retry events for that
    // RREQ ID and remove entry from RREQ retry table.
    Ptr<RreqRetryTableEntry> rreqRetryTableEntry;
    if (m_rreqRetryTable.LookUpEntry(payload.GetRouteReqId(), rreqRetryTableEntry))
    {
        rreqRetryTableEntry->GetRreqEventId().Cancel();
        m_rreqRetryTable.Delete(payload.GetRouteReqId());
    }

    uint8_t pathCost = linkCost + payload.GetPathCost();

    if (payload.GetOrigAddr() == m_nwkNetworkAddress)
    {
        // The RREP is destined for this device
        Ptr<RouteDiscoveryTableEntry> discEntry;
        if (m_nwkRouteDiscoveryTable.LookUpEntry(payload.GetRouteReqId(),
                                                 payload.GetOrigAddr(),
                                                 discEntry))
        {
            Ptr<RoutingTableEntry> routeEntry;
            if (m_nwkRoutingTable.LookUpEntry(payload.GetRespAddr(), routeEntry))
            {
                if (routeEntry->GetStatus() == ROUTE_DISCOVERY_UNDERWAY)
                {
                    if (routeEntry->IsGroupIdPresent())
                    {
                        routeEntry->SetStatus(ROUTE_VALIDATION_UNDERWAY);
                    }
                    else
                    {
                        routeEntry->SetStatus(ROUTE_ACTIVE);
                    }
                    routeEntry->SetNextHopAddr(macSrcAddr);
                    discEntry->SetResidualCost(pathCost);
                }
                else if (routeEntry->GetStatus() == ROUTE_VALIDATION_UNDERWAY ||
                         routeEntry->GetStatus() == ROUTE_ACTIVE)
                {
                    if (pathCost < discEntry->GetResidualCost())
                    {
                        routeEntry->SetNextHopAddr(macSrcAddr);
                        discEntry->SetResidualCost(pathCost);
                    }
                }

                NS_LOG_DEBUG("RREP from source [" << payload.GetRespAddr()
                                                  << "] is for me; received from last hop ["
                                                  << macSrcAddr << "]");

                if (m_pendPrimitiveNwk == NLME_ROUTE_DISCOVERY)
                {
                    // We only report the result of the route discovery request
                    // with the first RREP received.
                    m_pendPrimitiveNwk = NLDE_NLME_NONE;
                    if (!m_nlmeRouteDiscoveryConfirmCallback.IsNull())
                    {
                        NlmeRouteDiscoveryConfirmParams routeDiscConfirmParams;
                        routeDiscConfirmParams.m_status = NwkStatus::SUCCESS;
                        m_nlmeRouteDiscoveryConfirmCallback(routeDiscConfirmParams);
                    }
                }

                auto pendingTxPkt = std::make_shared<PendingTxPkt>();
                if (!m_pendingTxQueue.empty() &&
                    DequeuePendingTx(payload.GetRespAddr(), pendingTxPkt))
                {
                    // Buffer a copy of the DATA packet that will be transmitted
                    // for handling after transmission (i.e. NSDE-DATA.confirm)
                    BufferTxPkt(pendingTxPkt->txPkt->Copy(),
                                m_macHandle.GetValue(),
                                pendingTxPkt->nsduHandle);

                    // There is a pending packet awaiting to be transmitted
                    // to the next hop, send it.
                    McpsDataRequestParams mcpsDataparams;
                    mcpsDataparams.m_txOptions = 0x01; // Acknowledment on.
                    mcpsDataparams.m_dstPanId = m_nwkPanId;
                    mcpsDataparams.m_msduHandle = m_macHandle.GetValue();
                    mcpsDataparams.m_srcAddrMode = SHORT_ADDR;
                    mcpsDataparams.m_dstAddrMode = SHORT_ADDR;
                    mcpsDataparams.m_dstAddr = routeEntry->GetNextHopAddr();
                    m_macHandle++;

                    Simulator::ScheduleNow(&LrWpanMacBase::McpsDataRequest,
                                           m_mac,
                                           mcpsDataparams,
                                           pendingTxPkt->txPkt);
                }
            }
            else
            {
                m_nwkRouteDiscoveryTable.Delete(payload.GetRouteReqId(), payload.GetOrigAddr());
            }
        }
    }
    else
    {
        // The RREP is NOT destined for this device
        Ptr<RouteDiscoveryTableEntry> discEntry;
        if (m_nwkRouteDiscoveryTable.LookUpEntry(payload.GetRouteReqId(),
                                                 payload.GetOrigAddr(),
                                                 discEntry))
        {
            if (payload.GetPathCost() < discEntry->GetResidualCost())
            {
                Ptr<RoutingTableEntry> routeEntry;
                if (m_nwkRoutingTable.LookUpEntry(payload.GetRespAddr(), routeEntry))
                {
                    routeEntry->SetNextHopAddr(macSrcAddr);
                    routeEntry->SetStatus(ROUTE_ACTIVE);
                    discEntry->SetResidualCost(pathCost);
                    // Forward route reply to the next hop back to the original route requester
                    SendRREP(discEntry->GetSenderAddr(),
                             payload.GetOrigAddr(),
                             payload.GetRespAddr(),
                             payload.GetRouteReqId(),
                             pathCost);
                }
                else
                {
                    NS_LOG_ERROR("Route discovery entry detected but no corresponding routing "
                                 "table entry found");
                }
            }
        }
    }
}

void
ZigbeeNwk::ReceiveMulticastMemberFrame(ZigbeeNwkHeader nwkHeader,
                                       Ptr<Packet> msdu,
                                       McpsDataIndicationParams params)
{
    NS_LOG_FUNCTION(this);

    // As described in Zigbee specification r22.1.0 Section 3.6.6.3

    // Drop if we've already seen this multicast frame
    Ptr<BroadcastTransactionRecord> btr;
    if (m_btt.LookUpEntry(nwkHeader.GetSeqNum(), btr))
    {
        return;
    }

    // Add new record to BTT
    Ptr<BroadcastTransactionRecord> newBtr =
        Create<BroadcastTransactionRecord>(nwkHeader.GetSrcAddr(),
                                           nwkHeader.GetSeqNum(),
                                           Simulator::Now() + m_nwkNetworkBroadcastDeliveryTime);

    if (!m_btt.AddEntry(newBtr))
    {
        return;
    }

    // Verify if the device is capable of relaying multicast frames
    // (i.e., not an end device)
    CapabilityInformation capability(m_nwkCapabilityInformation);
    bool canRelay = (capability.GetDeviceType() != MacDeviceType::ENDDEVICE);

    // Case 1: Group Member — process and optionally retransmit

    if (m_nwkGroupIdTable->IsGroupMember(nwkHeader.GetDstAddr().ConvertToInt()))
    {
        // Push the multicast frame to the APS layer
        if (!m_nldeDataIndicationCallback.IsNull())
        {
            NldeDataIndicationParams dataParams;
            dataParams.m_srcAddr = nwkHeader.GetSrcAddr();
            dataParams.m_dstAddr = nwkHeader.GetDstAddr();
            dataParams.m_dstAddrMode = AddressMode::MCST;
            dataParams.m_linkQuality = params.m_mpduLinkQuality;
            dataParams.m_nsduLength = msdu->GetSize();
            dataParams.m_rxTime = Simulator::Now();
            dataParams.m_securityUse = false;
            m_nldeDataIndicationCallback(dataParams, msdu->Copy());
        }
        // TODO: Add Rx Trace here

        if (canRelay)
        {
            nwkHeader.SetMulticastMode(MulticastMode::MEMBER);
            nwkHeader.SetNonMemberRadius(nwkHeader.GetMaxNonMemberRadius());
            msdu->AddHeader(nwkHeader);
            Simulator::Schedule(MilliSeconds(m_bcstJitter->GetValue()),
                                &ZigbeeNwk::SendDataBcst,
                                this,
                                msdu,
                                0);
        }
        return;
    }

    // Case 2: Non Group Member — forward if non-zero radius

    uint8_t radius = nwkHeader.GetNonMemberRadius();
    if (radius == 0)
    {
        m_btt.Delete(nwkHeader.GetSeqNum());
        return;
    }

    // Decrement radius and retransmit if allowed
    nwkHeader.SetNonMemberRadius(radius - 1);

    if (radius != 0 && canRelay)
    {
        msdu->AddHeader(nwkHeader);
        Simulator::Schedule(MilliSeconds(m_bcstJitter->GetValue()),
                            &ZigbeeNwk::SendDataBcst,
                            this,
                            msdu,
                            0);
    }
}

void
ZigbeeNwk::ReceiveMulticastNonMemberFrame(ZigbeeNwkHeader nwkHeader,
                                          Ptr<Packet> msdu,
                                          McpsDataIndicationParams params)
{
    NS_LOG_FUNCTION(this);

    NS_ASSERT_MSG(!nwkHeader.GetDstAddr().IsBroadcast(),
                  "Error: Non-member multicast frame should have a Group ID address");

    // As described in Zigbee specification r22.1.0 Section 3.6.6.4
    // If the GROUP ID (destination address) is in our routing table,
    // our non member multicast frame is transmitted as a unicast frame
    // to the next hop in the routing table, otherwise it is discarded.
    Ptr<RoutingTableEntry> entry;
    if (m_nwkRoutingTable.LookUpEntry(nwkHeader.GetDstAddr(), entry))
    {
        if (entry->GetStatus() == RouteStatus::ROUTE_VALIDATION_UNDERWAY)
        {
            entry->SetStatus(RouteStatus::ROUTE_ACTIVE);
        }

        if ((entry->GetStatus() == ROUTE_ACTIVE))
        {
            msdu->AddHeader(nwkHeader);
            McpsDataRequestParams mcpsDataparams;
            mcpsDataparams.m_dstPanId = m_nwkPanId;
            mcpsDataparams.m_msduHandle = m_macHandle.GetValue();
            mcpsDataparams.m_txOptions = 0x01; // Acknowledment on for Non-Member Multicast
            mcpsDataparams.m_srcAddrMode = SHORT_ADDR;
            mcpsDataparams.m_dstAddrMode = SHORT_ADDR;
            mcpsDataparams.m_dstAddr = entry->GetNextHopAddr();
            m_macHandle++;
            Simulator::ScheduleNow(&LrWpanMacBase::McpsDataRequest, m_mac, mcpsDataparams, msdu);
        }
    }
}

bool
ZigbeeNwk::IsBroadcastAddress(Mac16Address address)
{
    return address == "FF:FF" || address == "FF:FD" || address == "FF:FC" || address == "FF:FB";
}

RouteDiscoveryStatus
ZigbeeNwk::FindNextHop(Mac16Address macSrcAddr,
                       uint8_t pathCost,
                       ZigbeeNwkHeader nwkHeader,
                       ZigbeePayloadRouteRequestCommand payload,
                       Mac16Address& nextHop)
{
    NS_LOG_FUNCTION(this);

    if (payload.GetMulticastField() &&
        m_nwkGroupIdTable->IsGroupMember(payload.GetDstAddr().ConvertToInt()))
    {
        // If the destination is a group member, we can directly return the next hop
        // as the destination address.
        nextHop = payload.GetDstAddr();
        return ROUTE_FOUND;
    }

    // Mesh routing

    // Check if I am the destination
    if (payload.GetDstAddr() == m_nwkNetworkAddress)
    {
        // I am the destination, return myself as next hop
        nextHop = m_nwkNetworkAddress;
        return ROUTE_FOUND;
    }

    // Check if the destination is our neighbor
    Ptr<NeighborTableEntry> neighborEntry;
    if (m_nwkNeighborTable.LookUpEntry(payload.GetDstAddr(), neighborEntry))
    {
        nextHop = payload.GetDstAddr();
        return ROUTE_FOUND;
    }

    Ptr<RoutingTableEntry> entry;
    if (m_nwkRoutingTable.LookUpEntry(payload.GetDstAddr(), entry))
    {
        if (!(entry->GetStatus() == ROUTE_ACTIVE) &&
            !(entry->GetStatus() == ROUTE_VALIDATION_UNDERWAY))
        {
            // Entry found but is not valid
            entry->SetStatus(ROUTE_DISCOVERY_UNDERWAY);
        }
        else
        {
            // Entry found
            nextHop = entry->GetNextHopAddr();
            return ROUTE_FOUND;
        }
    }
    else if (nwkHeader.GetDiscoverRoute() == DiscoverRouteType::ENABLE_ROUTE_DISCOVERY)
    {
        // Check that the max routing capacity has not been reached. If the capacity was reached,
        // attempt to delete the first found expired entry, if the table persist to be full
        // then send a route error.
        if (m_nwkRoutingTable.GetSize() >= m_nwkRoutingTable.GetMaxTableSize())
        {
            m_nwkRoutingTable.DeleteExpiredEntry();

            if (m_nwkRoutingTable.GetSize() >= m_nwkRoutingTable.GetMaxTableSize())
            {
                if (!m_nlmeRouteDiscoveryConfirmCallback.IsNull())
                {
                    NlmeRouteDiscoveryConfirmParams confirmParams;
                    confirmParams.m_status = ROUTE_ERROR;
                    confirmParams.m_networkStatusCode = NO_ROUTING_CAPACITY;
                    m_nlmeRouteDiscoveryConfirmCallback(confirmParams);
                }
                return TABLE_FULL;
            }
        }

        // Entry not found, create a new routing entry
        Ptr<RoutingTableEntry> newRoutingEntry =
            Create<RoutingTableEntry>(payload.GetDstAddr(),
                                      ROUTE_DISCOVERY_UNDERWAY,
                                      true,                        // TODO no route cache
                                      false,                       // Many to one
                                      false,                       // TODO: Route record
                                      payload.GetMulticastField(), // Group id
                                      Mac16Address("FF:FF"));
        newRoutingEntry->SetLifeTime(Simulator::Now() + m_routeExpiryTime);

        m_nwkRoutingTable.AddEntry(newRoutingEntry);
    }
    else
    {
        if (!m_nlmeRouteDiscoveryConfirmCallback.IsNull())
        {
            NlmeRouteDiscoveryConfirmParams confirmParams;
            confirmParams.m_status = ROUTE_ERROR;
            confirmParams.m_networkStatusCode = NO_ROUTE_AVAILABLE;
            m_nlmeRouteDiscoveryConfirmCallback(confirmParams);
        }
        return NO_DISCOVER_ROUTE;
    }

    // 2- Find entry in DISCOVERY TABLE
    Ptr<RouteDiscoveryTableEntry> discEntry;
    if (m_nwkRouteDiscoveryTable.LookUpEntry(payload.GetRouteReqId(),
                                             nwkHeader.GetSrcAddr(),
                                             discEntry))
    {
        // Entry Found
        if (pathCost < discEntry->GetForwardCost())
        {
            // More optimal route found, update route discovery values.
            discEntry->SetSenderAddr(macSrcAddr);
            discEntry->SetForwardCost(pathCost);
            discEntry->SetExpTime(Simulator::Now() + m_nwkcRouteDiscoveryTime);
            return ROUTE_UPDATED;
        }
        else
        {
            return DISCOVER_UNDERWAY;
        }
    }
    else
    {
        // Entry NOT found, add NEW entry to route discovery table.
        Ptr<RouteDiscoveryTableEntry> newDiscEntry =
            Create<RouteDiscoveryTableEntry>(payload.GetRouteReqId(),
                                             nwkHeader.GetSrcAddr(),
                                             macSrcAddr, // macSrcAddr,
                                             pathCost,   // payload.GetPathCost(), // Forward cost
                                             0xff,       // Residual cost
                                             (Simulator::Now() + m_nwkcRouteDiscoveryTime));

        if (!m_nwkRouteDiscoveryTable.AddEntry(newDiscEntry))
        {
            return TABLE_FULL;
        }
    }
    return ROUTE_NOT_FOUND;
}

RouteDiscoveryStatus
ZigbeeNwk::ProcessManyToOneRoute(Mac16Address macSrcAddr,
                                 uint8_t pathCost,
                                 ZigbeeNwkHeader nwkHeader,
                                 ZigbeePayloadRouteRequestCommand payload)
{
    Ptr<RouteDiscoveryTableEntry> discEntry;
    if (m_nwkRouteDiscoveryTable.LookUpEntry(payload.GetRouteReqId(),
                                             nwkHeader.GetSrcAddr(),
                                             discEntry))
    {
        Ptr<RoutingTableEntry> routeEntry;
        if (m_nwkRoutingTable.LookUpEntry(nwkHeader.GetSrcAddr(), routeEntry))
        {
            if (routeEntry->GetStatus() == ROUTE_VALIDATION_UNDERWAY ||
                routeEntry->GetStatus() == ROUTE_ACTIVE)
            {
                if (pathCost < discEntry->GetForwardCost())
                {
                    // Update with a better route.
                    routeEntry->SetNextHopAddr(macSrcAddr);
                    discEntry->SetForwardCost(pathCost);
                    discEntry->SetExpTime(Simulator::Now() + m_nwkcRouteDiscoveryTime);
                    return ROUTE_UPDATED;
                }
                return NO_ROUTE_CHANGE;
            }
        }
        else
        {
            NS_LOG_ERROR("Entry found in the discovery table but not the routing table");
            return NO_ROUTE_CHANGE;
        }
    }
    else
    {
        // Entry NOT found, add NEW entry to route discovery table.
        Ptr<RouteDiscoveryTableEntry> newDiscEntry =
            Create<RouteDiscoveryTableEntry>(payload.GetRouteReqId(),
                                             nwkHeader.GetSrcAddr(),
                                             macSrcAddr, // previous hop address
                                             pathCost,   // Forward cost
                                             0xff,       // Residual cost (not used by Many-to-One)
                                             (Simulator::Now() + m_nwkcRouteDiscoveryTime));

        // TODO: what to do if route discovery table is full?
        m_nwkRouteDiscoveryTable.AddEntry(newDiscEntry);

        // Define the type of Many-To-One routing (with or without route record)
        bool routeRecord = false;
        if (payload.GetCmdOptManyToOneField() == ManyToOne::ROUTE_RECORD)
        {
            routeRecord = true;
        }

        Ptr<RoutingTableEntry> routeEntry;
        if (m_nwkRoutingTable.LookUpEntry(nwkHeader.GetSrcAddr(), routeEntry))
        {
            if (routeEntry->GetStatus() == ROUTE_VALIDATION_UNDERWAY ||
                routeEntry->GetStatus() == ROUTE_ACTIVE)
            {
                // The entry exist in routing table but it was not in discovery table
                // Refresh the Route
                routeEntry->SetNextHopAddr(macSrcAddr);
                // TODO: other parameters
                return ROUTE_UPDATED;
            }
            return NO_ROUTE_CHANGE;
        }
        else
        {
            // New routing table entry
            // Check that the max routing capacity has not been reached. If the capacity was
            // reached, attempt to delete the first found expired entry, if the table persist to be
            // full then send a route error.
            if (m_nwkRoutingTable.GetSize() >= m_nwkRoutingTable.GetMaxTableSize())
            {
                m_nwkRoutingTable.DeleteExpiredEntry();

                if (m_nwkRoutingTable.GetSize() >= m_nwkRoutingTable.GetMaxTableSize())
                {
                    if (!m_nlmeRouteDiscoveryConfirmCallback.IsNull())
                    {
                        NlmeRouteDiscoveryConfirmParams confirmParams;
                        confirmParams.m_status = ROUTE_ERROR;
                        confirmParams.m_networkStatusCode = NO_ROUTING_CAPACITY;
                        m_nlmeRouteDiscoveryConfirmCallback(confirmParams);
                    }
                    return TABLE_FULL;
                }
            }

            Ptr<RoutingTableEntry> newRoutingEntry =
                Create<RoutingTableEntry>(nwkHeader.GetSrcAddr(),
                                          ROUTE_ACTIVE,
                                          true,        // TODO no route cache
                                          true,        // Many to one
                                          routeRecord, // TODO: Route record
                                          false,       // TODO: Group id
                                          macSrcAddr);

            newRoutingEntry->SetLifeTime(Simulator::Now() + m_routeExpiryTime);

            m_nwkRoutingTable.AddEntry(newRoutingEntry);
            return MANY_TO_ONE_ROUTE;
        }
    }
    return NO_ROUTE_CHANGE;
}

void
ZigbeeNwk::SendDataUcst(Ptr<Packet> packet, uint8_t nwkHandle)
{
    NS_LOG_FUNCTION(this);

    // Obtain information from the DATA packet
    ZigbeeNwkHeader nwkHeaderData;
    packet->PeekHeader(nwkHeaderData);

    // Construct a RREQ network header and payload for possible
    // route discovery request

    ZigbeeNwkHeader nwkHeaderRreq;
    nwkHeaderRreq.SetFrameType(NWK_COMMAND);
    nwkHeaderRreq.SetProtocolVer(m_nwkcProtocolVersion);
    nwkHeaderRreq.SetDiscoverRoute(nwkHeaderData.GetDiscoverRoute());
    // See r22.1.0, Table 3-69
    // Set destination to broadcast (all routers and coordinator)
    nwkHeaderRreq.SetDstAddr(Mac16Address("FF:FC"));
    nwkHeaderRreq.SetSrcAddr(m_nwkNetworkAddress);
    nwkHeaderRreq.SetSeqNum(m_nwkSequenceNumber.GetValue());

    // see Zigbee specification 3.2.2.33.3
    if (nwkHeaderData.GetRadius() == 0)
    {
        nwkHeaderRreq.SetRadius(m_nwkMaxDepth * 2);
    }
    else
    {
        nwkHeaderRreq.SetRadius(nwkHeaderData.GetRadius());
    }

    ZigbeePayloadRouteRequestCommand payloadRreq;
    payloadRreq.SetRouteReqId(m_routeRequestId.GetValue());
    payloadRreq.SetDstAddr(nwkHeaderData.GetDstAddr());
    payloadRreq.SetPathCost(0);

    // If the original Data packet is marked as multicast,
    // then set the multicast field in the RREQ payload
    // In essence, this will initiate a multicast route discovery
    // if necessary
    if (nwkHeaderData.IsMulticast())
    {
        payloadRreq.SetMulticastField(true);
    }

    Mac16Address nextHop;
    RouteDiscoveryStatus nextHopStatus =
        FindNextHop(m_nwkNetworkAddress, 0, nwkHeaderRreq, payloadRreq, nextHop);

    if (nextHopStatus == ROUTE_FOUND)
    {
        // Buffer a copy of the DATA packet that will be transmitted
        // for handling after transmission (i.e. NSDE-DATA.confirm)
        BufferTxPkt(packet->Copy(), m_macHandle.GetValue(), nwkHandle);

        // Parameters as described in Section 3.6.3.3
        McpsDataRequestParams mcpsDataparams;
        mcpsDataparams.m_dstPanId = m_nwkPanId;
        mcpsDataparams.m_msduHandle = m_macHandle.GetValue();
        mcpsDataparams.m_txOptions = 0x01; // Acknowledment on.
        mcpsDataparams.m_srcAddrMode = SHORT_ADDR;
        mcpsDataparams.m_dstAddrMode = SHORT_ADDR;
        mcpsDataparams.m_dstAddr = nextHop;
        m_macHandle++;
        Simulator::ScheduleNow(&LrWpanMacBase::McpsDataRequest, m_mac, mcpsDataparams, packet);
    }
    else if (nextHopStatus == ROUTE_NOT_FOUND)
    {
        // Route not found. Route marked as DISCOVER UNDERWAY,
        // packet added to pending Tx Queue and we initiate route
        // discovery

        EnqueuePendingTx(packet, nwkHandle);

        Simulator::Schedule(MilliSeconds(m_rreqJitter->GetValue()),
                            &ZigbeeNwk::SendRREQ,
                            this,
                            nwkHeaderRreq,
                            payloadRreq,
                            m_nwkcInitialRREQRetries);

        m_nwkSequenceNumber++;
        m_routeRequestId++;
    }
}

void
ZigbeeNwk::SendDataBcst(Ptr<Packet> packet, uint8_t nwkHandle)
{
    // Buffer a copy of the DATA packet that will be transmitted
    // for handling after transmission (i.e. NSDE-DATA.confirm)
    BufferTxPkt(packet->Copy(), m_macHandle.GetValue(), nwkHandle);

    // Parameters as described in Section 3.6.5
    // TxOptions = 0 by default, i.e. no acknowledgment
    McpsDataRequestParams mcpsDataparams;
    mcpsDataparams.m_dstPanId = m_nwkPanId;
    mcpsDataparams.m_msduHandle = m_macHandle.GetValue();
    mcpsDataparams.m_srcAddrMode = SHORT_ADDR;
    mcpsDataparams.m_dstAddrMode = SHORT_ADDR;
    mcpsDataparams.m_dstAddr = Mac16Address("FF:FF");
    m_macHandle++;
    Simulator::ScheduleNow(&LrWpanMacBase::McpsDataRequest, m_mac, mcpsDataparams, packet);
}

void
ZigbeeNwk::McpsDataConfirm(McpsDataConfirmParams params)
{
    std::shared_ptr<TxPkt> bufferedElement;
    if (RetrieveTxPkt(params.m_msduHandle, bufferedElement))
    {
        ZigbeeNwkHeader nwkHeader;
        bufferedElement->txPkt->PeekHeader(nwkHeader);

        if (nwkHeader.GetFrameType() != NwkType::DATA)
        {
            NS_LOG_ERROR("Received DATA.confirm for a non-DATA packet");
            return;
        }

        if (IsBroadcastAddress(nwkHeader.GetDstAddr()) || nwkHeader.IsMulticast())
        {
            if (params.m_status != MacStatus::SUCCESS)
            {
                // Broadcast or Multicast failed, retry sending the packet.
                // Zigbee specification r22.1.0 Section 3.6.6.3
                Ptr<BroadcastTransactionRecord> btr;
                if (m_btt.LookUpEntry(nwkHeader.GetSeqNum(), btr))
                {
                    btr->SetBcstRetryCount(btr->GetBcstRetryCount() + 1);
                    if (btr->GetBcstRetryCount() <= m_nwkMaxBroadcastRetries)
                    {
                        // Retry sending the Broadcast or Multicast packet

                        Simulator::Schedule(MilliSeconds(m_nwkPassiveAckTimeout.GetMilliSeconds()),
                                            &ZigbeeNwk::SendDataBcst,
                                            this,
                                            bufferedElement->txPkt,
                                            bufferedElement->nwkHandle);
                    }
                    else
                    {
                        // Broadcast retries exhausted, silently discard the packet
                        NS_LOG_WARN("Broadcast or Multicast packet retries exhausted for packet"
                                    " with sequence number "
                                    << nwkHeader.GetSeqNum());
                    }
                }
            }
        }
        else if (nwkHeader.GetSrcAddr() == m_nwkNetworkAddress)
        {
            // Send the confirmation to next layer after the packet transmission,
            // only if packet transmitted was in the initiator of the NLDE-DATA.request

            if (!m_nldeDataConfirmCallback.IsNull())
            {
                // Zigbee Specification r22.1.0, End of Section 3.2.1.1.3
                // Report the the results of a request to a transmission of a packet
                NldeDataConfirmParams nldeDataConfirmParams;
                // nldeDataConfirmParams.m_status =
                // static_cast<ZigbeeNwkStatus>(params.m_status);
                nldeDataConfirmParams.m_nsduHandle = bufferedElement->nwkHandle;
                m_nldeDataConfirmCallback(nldeDataConfirmParams);
            }
        }
    }
}

void
ZigbeeNwk::MlmeScanConfirm(MlmeScanConfirmParams params)
{
    NS_LOG_FUNCTION(this);

    if (m_pendPrimitiveNwk == NLME_NETWORK_FORMATION && params.m_scanType == MLMESCAN_ED)
    {
        if (params.m_status != MacStatus::SUCCESS)
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_netFormParams = {};
            m_netFormParamsGen = nullptr;

            // See Zigbee specification r22.1.0, Section 3.2.2.5.3, (6.b.i)
            if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
            {
                NlmeNetworkFormationConfirmParams confirmParams;
                confirmParams.m_status = GetNwkStatus(params.m_status);
                m_nlmeNetworkFormationConfirmCallback(confirmParams);
            }
        }
        else
        {
            // TODO: Continue energy detection (ED) scan in other interfaces if supported.

            // See Zigbee specification r22.1.0, Section 3.2.2.5.3, (6.b.ii)
            // Pick the list of acceptable channels on which to continue doing an ACTIVE scan
            std::vector<uint8_t> energyList = params.m_energyDetList;
            uint32_t channelMask = m_netFormParams.m_scanChannelList.channelsField[0];

            NS_LOG_DEBUG("[NLME-NETWORK-FORMATION.request]: \n              "
                         << "EnergyThreshold: " << m_scanEnergyThreshold << " | ChannelMask: 0x"
                         << std::hex << channelMask << std::dec << " | EnergyList: " << energyList);

            m_filteredChannelMask = 0;
            uint32_t countAcceptableChannels = 0;
            uint8_t energyListPos = 0;
            for (uint32_t i = 0; i < 32; i++)
            {
                // check if the i position exist in the ChannelMask
                if (channelMask & (1 << i))
                {
                    if (energyList[energyListPos] <= m_scanEnergyThreshold)
                    {
                        m_filteredChannelMask |= (1 << i);
                        countAcceptableChannels++;
                    }
                    energyListPos++;
                }
            }

            NS_LOG_DEBUG("[NLME-NETWORK-FORMATION.request]:\n              "
                         << "Energy scan complete, " << countAcceptableChannels
                         << " acceptable channels found : 0x" << std::hex << m_filteredChannelMask
                         << std::dec);

            if (countAcceptableChannels == 0)
            {
                m_pendPrimitiveNwk = NLDE_NLME_NONE;
                m_netFormParams = {};
                m_netFormParamsGen = nullptr;

                if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
                {
                    NlmeNetworkFormationConfirmParams confirmParams;
                    confirmParams.m_status = NwkStatus::STARTUP_FAILURE;
                    m_nlmeNetworkFormationConfirmCallback(confirmParams);
                }
            }
            else
            {
                // See Zigbee specification r22.1.0, Section 3.2.2.5.3, (6.c)
                MlmeScanRequestParams mlmeParams;
                mlmeParams.m_chPage = (m_filteredChannelMask >> 27) & (0x01F);
                mlmeParams.m_scanChannels = m_filteredChannelMask;
                mlmeParams.m_scanDuration = m_netFormParams.m_scanDuration;
                mlmeParams.m_scanType = MLMESCAN_ACTIVE;
                Simulator::ScheduleNow(&LrWpanMacBase::MlmeScanRequest, m_mac, mlmeParams);
            }
        }
    }
    else if (m_pendPrimitiveNwk == NLME_NETWORK_FORMATION && params.m_scanType == MLMESCAN_ACTIVE)
    {
        if (params.m_status == MacStatus::NO_BEACON || params.m_status == MacStatus::SUCCESS)
        {
            // TODO: We should ACTIVE scan channels on each interface
            // (only possible when more interfaces (nwkMacInterfaceTable) are supported)
            // for now, only a single interface is considered.

            // See Zigbee specification r22.1.0, (3.2.2.5.3, 6.d.ii)
            // Check results of an ACTIVE scan an select a different PAN ID and channel:
            // Choose a random PAN ID
            // Page is always 0 until more interfaces supported
            uint8_t channel = 0;
            uint8_t page = 0;
            uint16_t panId = m_uniformRandomVariable->GetInteger(1, 0xFFF7);

            std::vector<uint8_t> pansPerChannel(27);
            uint32_t secondFilteredChannelMask = m_filteredChannelMask;
            for (const auto& panDescriptor : params.m_panDescList)
            {
                // Clear all the bit positions of channels with active PAN networks
                // (The channels with PAN descriptors received)
                secondFilteredChannelMask &= ~(1 << panDescriptor.m_logCh);
                // Add to the number of PAN detected in this channel
                pansPerChannel[panDescriptor.m_logCh] += 1;
            }

            for (uint32_t i = 0; i < 32; i++)
            {
                // Pick the first channel in the list that does not contain
                // any PAN network
                if (secondFilteredChannelMask & (1 << i))
                {
                    channel = i;
                    break;
                }
            }

            if (channel < 11 || channel > 26)
            {
                // Extreme case: All the channels in the previous step contained PAN networks
                // therefore choose the channel with the least PAN networks.
                uint8_t channelIndex = 0;
                uint8_t lowestPanNum = 99;
                for (const auto& numPans : pansPerChannel)
                {
                    if (numPans != 0 && numPans <= lowestPanNum)
                    {
                        channel = channelIndex;
                        lowestPanNum = numPans;
                    }
                    channelIndex++;
                }

                NS_ASSERT_MSG(channel < 11 || channel > 26,
                              "Invalid channel in PAN descriptor list during ACTIVE scan");
            }

            // store the chosen page, channel and pan Id.
            m_netFormParamsGen = Create<NetFormPendingParamsGen>();
            m_netFormParamsGen->page = page;
            m_netFormParamsGen->channel = channel;
            m_netFormParamsGen->panId = panId;

            NS_LOG_DEBUG("[NLME-NETWORK-FORMATION.request]:\n              "
                         << "Active scan complete, page " << std::dec << page << ", channel "
                         << std::dec << channel << " and PAN ID 0x" << std::hex << panId << std::dec
                         << " chosen.");

            // Set the device short address (3.2.2.5.3 , 6.f)
            Ptr<MacPibAttributes> pibAttr = Create<MacPibAttributes>();
            if (m_netFormParams.m_distributedNetwork)
            {
                pibAttr->macShortAddress = m_netFormParams.m_distributedNetworkAddress;
                m_nwkNetworkAddress = m_netFormParams.m_distributedNetworkAddress;
            }
            else
            {
                pibAttr->macShortAddress = Mac16Address("00:00");
                m_nwkNetworkAddress = Mac16Address("00:00");
            }
            // Set Short Address and continue with beacon payload afterwards.
            Simulator::ScheduleNow(&LrWpanMacBase::MlmeSetRequest,
                                   m_mac,
                                   MacPibAttributeIdentifier::macShortAddress,
                                   pibAttr);
        }
        else
        {
            // Error occurred during network formation active scan
            // report to higher layer (Section 3.2.2.5.3, 6.d)

            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_netFormParams = {};
            m_netFormParamsGen = nullptr;

            if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
            {
                NlmeNetworkFormationConfirmParams confirmParams;
                confirmParams.m_status = GetNwkStatus(params.m_status);
                m_nlmeNetworkFormationConfirmCallback(confirmParams);
            }
        }
    }
    else if (m_pendPrimitiveNwk == NLME_NET_DISCV && params.m_scanType == MLMESCAN_ACTIVE)
    {
        NlmeNetworkDiscoveryConfirmParams netDiscConfirmParams;
        m_pendPrimitiveNwk = NLDE_NLME_NONE;

        if (params.m_status == MacStatus::SUCCESS)
        {
            NS_LOG_DEBUG("[NLME-NETWORK-DISCOVERY.request]:\n              "
                         << "Active scan, " << m_networkDescriptorList.size()
                         << " PARENT capable device(s) found");

            netDiscConfirmParams.m_netDescList = m_networkDescriptorList;
            netDiscConfirmParams.m_networkCount = m_networkDescriptorList.size();
            netDiscConfirmParams.m_status = NwkStatus::SUCCESS;
            m_networkDescriptorList = {};
        }
        else
        {
            NS_LOG_DEBUG("[NLME-NETWORK-DISCOVERY.request]: Active scan failed with"
                         " status: "
                         << GetNwkStatus(params.m_status));
            netDiscConfirmParams.m_status = GetNwkStatus(params.m_status);
        }

        if (!m_nlmeNetworkDiscoveryConfirmCallback.IsNull())
        {
            m_nlmeNetworkDiscoveryConfirmCallback(netDiscConfirmParams);
        }
    }
    else if (m_pendPrimitiveNwk == NLME_JOIN && params.m_scanType == MLMESCAN_ORPHAN)
    {
        // TODO: Add macInterfaceIndex and channelListStructure params when supported
        if (params.m_status == MacStatus::SUCCESS)
        {
            // Orphan scan was successful (Join success), first update the extended
            // PAN id and the capability information, then the
            // the nwkNetworkAddress with the macShortAddress, this
            // will be followed by an update of the m_nwkPanId with macPanId
            // and finally the join confirmation
            m_nwkExtendedPanId = m_joinParams.m_extendedPanId;
            m_nwkCapabilityInformation = m_joinParams.m_capabilityInfo;
            Simulator::ScheduleNow(&LrWpanMacBase::MlmeGetRequest,
                                   m_mac,
                                   MacPibAttributeIdentifier::macShortAddress);
        }
        else
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_joinParams = {};
            NS_LOG_DEBUG("[NLME-JOIN.request]: Orphan scan completed but no networks found");

            if (!m_nlmeJoinConfirmCallback.IsNull())
            {
                NlmeJoinConfirmParams joinConfirmParams;
                joinConfirmParams.m_status = NwkStatus::NO_NETWORKS;
                m_nlmeJoinConfirmCallback(joinConfirmParams);
            }
        }
    }
}

void
ZigbeeNwk::MlmeAssociateConfirm(MlmeAssociateConfirmParams params)
{
    NS_LOG_FUNCTION(this);

    if (m_pendPrimitiveNwk == NLME_JOIN)
    {
        NlmeJoinConfirmParams joinConfirmParams;
        joinConfirmParams.m_extendedPanId = m_joinParams.m_extendedPanId;
        joinConfirmParams.m_enhancedBeacon = false; // hardcoded, no support
        joinConfirmParams.m_macInterfaceIndex = 0;  // hardcoded, no support
        joinConfirmParams.m_networkAddress = params.m_assocShortAddr;

        Ptr<NeighborTableEntry> entry;

        if (params.m_status == MacStatus::SUCCESS)
        {
            joinConfirmParams.m_status = NwkStatus::SUCCESS;
            joinConfirmParams.m_networkAddress = params.m_assocShortAddr;

            // Update NWK NIB values
            m_nwkNetworkAddress = params.m_assocShortAddr;
            m_nwkExtendedPanId = m_joinParams.m_extendedPanId;
            m_nwkPanId = m_associateParams.m_coordPanId;

            // Update relationship
            if (m_associateParams.m_coordAddrMode == lrwpan::AddressMode::EXT_ADDR)
            {
                if (m_nwkNeighborTable.LookUpEntry(m_associateParams.m_coordExtAddr, entry))
                {
                    entry->SetRelationship(NBR_PARENT);

                    NS_LOG_DEBUG("[NLME-JOIN.request]:\n              "
                                 << "Status: " << joinConfirmParams.m_status << " | PAN ID: 0x"
                                 << std::hex << m_nwkPanId << " | Extended PAN ID: 0x"
                                 << m_nwkExtendedPanId << std::dec);
                }
                else
                {
                    NS_LOG_ERROR("Entry not found while updating relationship");
                }
            }
            else
            {
                if (m_nwkNeighborTable.LookUpEntry(m_associateParams.m_coordShortAddr, entry))
                {
                    entry->SetRelationship(NBR_PARENT);

                    NS_LOG_DEBUG("[NLME-JOIN.request]:\n              "
                                 << "Status: " << joinConfirmParams.m_status << " | PAN ID: 0x"
                                 << std::hex << m_nwkPanId << " | Extended PAN ID: 0x"
                                 << m_nwkExtendedPanId << std::dec);
                }
                else
                {
                    NS_LOG_ERROR("Entry not found while updating relationship");
                }
            }

            // TODO:m_nwkUpdateId
        }
        else
        {
            if (params.m_status == MacStatus::FULL_CAPACITY)
            {
                // Discard neighbor as potential parent
                if (m_associateParams.m_coordAddrMode == lrwpan::AddressMode::EXT_ADDR)
                {
                    if (m_nwkNeighborTable.LookUpEntry(m_associateParams.m_coordExtAddr, entry))
                    {
                        entry->SetPotentialParent(false);
                    }
                    else
                    {
                        NS_LOG_ERROR("Neighbor not found when discarding as potential parent");
                    }
                }
                else
                {
                    if (m_nwkNeighborTable.LookUpEntry(m_associateParams.m_coordShortAddr, entry))
                    {
                        entry->SetPotentialParent(false);
                    }
                    else
                    {
                        NS_LOG_ERROR("Neighbor not found when discarding as potential parent");
                    }
                }

                joinConfirmParams.m_status = NwkStatus::NEIGHBOR_TABLE_FULL;
            }
            else
            {
                joinConfirmParams.m_status = GetNwkStatus(params.m_status);
            }
        }

        m_pendPrimitiveNwk = NLDE_NLME_NONE;
        m_joinParams = {};
        m_associateParams = {};

        if (!m_nlmeJoinConfirmCallback.IsNull())
        {
            m_nlmeJoinConfirmCallback(joinConfirmParams);
        }
    }
}

void
ZigbeeNwk::MlmeStartConfirm(MlmeStartConfirmParams params)
{
    NS_LOG_FUNCTION(this);

    NwkStatus nwkConfirmStatus;
    nwkConfirmStatus = GetNwkStatus(params.m_status);

    if (nwkConfirmStatus != NwkStatus::SUCCESS)
    {
        m_nwkExtendedPanId = 0xffffffffffffffed;
        m_nwkNetworkAddress = Mac16Address("ff:ff");
        m_nwkPanId = 0xffff;
    }

    if (m_pendPrimitiveNwk == NLME_NETWORK_FORMATION)
    {
        NS_LOG_DEBUG("[NLME-NETWORK-FORMATION.request]:\n              "
                     << "Status: " << nwkConfirmStatus << " | PAN ID:" << std::hex << m_nwkPanId
                     << " | Extended PAN ID: 0x" << m_nwkExtendedPanId << std::dec);

        m_pendPrimitiveNwk = NLDE_NLME_NONE;
        m_netFormParams = {};
        m_netFormParamsGen = nullptr;

        if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
        {
            NlmeNetworkFormationConfirmParams confirmParams;
            confirmParams.m_status = nwkConfirmStatus;
            m_nlmeNetworkFormationConfirmCallback(confirmParams);
        }
    }
    else if (m_pendPrimitiveNwk == NLME_START_ROUTER)
    {
        NS_LOG_DEBUG("[NLME-START-ROUTER.request]:\n              "
                     << "Status: " << nwkConfirmStatus << " | PAN ID: 0x" << std::hex << m_nwkPanId
                     << " | Extended PAN ID: 0x" << m_nwkExtendedPanId << std::dec);

        if (nwkConfirmStatus != NwkStatus::SUCCESS)
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_startRouterParams = {};
            if (!m_nlmeStartRouterConfirmCallback.IsNull())
            {
                NlmeStartRouterConfirmParams confirmParams;
                confirmParams.m_status = nwkConfirmStatus;
                m_nlmeStartRouterConfirmCallback(confirmParams);
            }
        }
        else
        {
            UpdateBeaconPayloadLength();
        }
    }
}

void
ZigbeeNwk::MlmeSetConfirm(MlmeSetConfirmParams params)
{
    NS_LOG_FUNCTION(this << params.id);

    if (m_pendPrimitiveNwk == NLME_NETWORK_FORMATION)
    {
        if (params.m_status == MacStatus::SUCCESS &&
            params.id == MacPibAttributeIdentifier::macShortAddress)
        {
            // Section (3.2.2.5.3 , 6.g)
            // Getting this device MAC extended address using MLME-GET
            Simulator::ScheduleNow(&LrWpanMacBase::MlmeGetRequest,
                                   m_mac,
                                   MacPibAttributeIdentifier::macExtendedAddress);
        }
        else if (params.m_status == MacStatus::SUCCESS &&
                 params.id == MacPibAttributeIdentifier::macBeaconPayload)
        {
            // Finalize Network Formation (Start network)
            MlmeStartRequestParams startParams;
            startParams.m_logCh = m_netFormParamsGen->channel;
            startParams.m_logChPage = m_netFormParamsGen->page;
            startParams.m_PanId = m_netFormParamsGen->panId;
            startParams.m_bcnOrd = m_netFormParams.m_beaconOrder;
            startParams.m_sfrmOrd = m_netFormParams.m_superFrameOrder;
            startParams.m_battLifeExt = m_netFormParams.m_batteryLifeExtension;
            startParams.m_coorRealgn = false;
            startParams.m_panCoor = true;
            Simulator::ScheduleNow(&LrWpanMacBase::MlmeStartRequest, m_mac, startParams);
        }
        else if (params.m_status == MacStatus::SUCCESS &&
                 params.id == MacPibAttributeIdentifier::macBeaconPayloadLength)
        {
            UpdateBeaconPayload();
        }
        else
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_netFormParams = {};
            m_netFormParamsGen = nullptr;

            if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
            {
                NlmeNetworkFormationConfirmParams confirmParams;
                confirmParams.m_status = NwkStatus::STARTUP_FAILURE;
                m_nlmeNetworkFormationConfirmCallback(confirmParams);
            }
        }
    }
    else if (m_pendPrimitiveNwk == NLME_JOIN_INDICATION)
    {
        if (params.m_status == MacStatus::SUCCESS &&
            params.id == MacPibAttributeIdentifier::macBeaconPayloadLength)
        {
            UpdateBeaconPayload();
        }
        else
        {
            NlmeJoinIndicationParams joinIndParams = m_joinIndParams;

            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_joinIndParams = {};

            if (!m_nlmeJoinIndicationCallback.IsNull())
            {
                m_nlmeJoinIndicationCallback(joinIndParams);
            }
        }
    }
    else if (m_pendPrimitiveNwk == NLME_START_ROUTER)
    {
        if (params.m_status == MacStatus::SUCCESS &&
            params.id == MacPibAttributeIdentifier::macBeaconPayloadLength)
        {
            UpdateBeaconPayload();
        }
        else if (params.m_status == MacStatus::SUCCESS &&
                 params.id == MacPibAttributeIdentifier::macBeaconPayload)
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_startRouterParams = {};
            if (!m_nlmeStartRouterConfirmCallback.IsNull())
            {
                NlmeStartRouterConfirmParams confirmParams;
                confirmParams.m_status = NwkStatus::SUCCESS;
                m_nlmeStartRouterConfirmCallback(confirmParams);
            }
        }
        else
        {
            NS_LOG_ERROR("Beacon payload update failed during a NLME-START-ROUTER.request");
        }
    }
}

void
ZigbeeNwk::MlmeGetConfirm(MacStatus status,
                          MacPibAttributeIdentifier id,
                          Ptr<MacPibAttributes> attribute)
{
    NS_LOG_FUNCTION(this);

    // Update the values of attributes in the network layer
    if (status == MacStatus::SUCCESS)
    {
        if (id == MacPibAttributeIdentifier::macExtendedAddress)
        {
            m_nwkIeeeAddress = attribute->macExtendedAddress;
        }
        else if (id == MacPibAttributeIdentifier::macShortAddress)
        {
            m_nwkNetworkAddress = attribute->macShortAddress;
        }
        else if (id == MacPibAttributeIdentifier::macPanId)
        {
            m_nwkPanId = attribute->macPanId;
        }
        else if (id == MacPibAttributeIdentifier::pCurrentChannel)
        {
            m_currentChannel = attribute->pCurrentChannel;
        }
    }

    if (m_pendPrimitiveNwk == PendingPrimitiveNwk::NLME_NETWORK_FORMATION)
    {
        if (id == MacPibAttributeIdentifier::macExtendedAddress && status == MacStatus::SUCCESS)
        {
            // Section (3.2.2.5.3 , 6.g)
            // Set nwkExtendedPanId and m_nwkIeeeAddress and nwkPanId
            m_nwkExtendedPanId = m_nwkIeeeAddress.ConvertToInt();
            m_nwkPanId = m_netFormParamsGen->panId;

            // Configure the capability information of the PAN coordinator
            CapabilityInformation capaInfo;
            capaInfo.SetDeviceType(zigbee::MacDeviceType::ROUTER);
            m_nwkCapabilityInformation = capaInfo.GetCapability();

            // Set Beacon payload size followed by the beacon payload content
            // before starting a network
            // See Figure 3-37 Establishing a Network
            // See also 3.6.7.
            UpdateBeaconPayloadLength();
        }
        else
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_netFormParams = {};
            m_netFormParamsGen = nullptr;

            if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
            {
                NlmeNetworkFormationConfirmParams confirmParams;
                confirmParams.m_status = NwkStatus::STARTUP_FAILURE;
                m_nlmeNetworkFormationConfirmCallback(confirmParams);
            }
        }
    }
    else if (m_pendPrimitiveNwk == PendingPrimitiveNwk::NLME_JOIN && status == MacStatus::SUCCESS)
    {
        if (id == MacPibAttributeIdentifier::macShortAddress)
        {
            Simulator::ScheduleNow(&LrWpanMacBase::MlmeGetRequest,
                                   m_mac,
                                   MacPibAttributeIdentifier::macPanId);
        }
        else if (id == MacPibAttributeIdentifier::macPanId)
        {
            NlmeJoinConfirmParams joinConfirmParams;
            joinConfirmParams.m_channelList = m_joinParams.m_scanChannelList;
            joinConfirmParams.m_status = NwkStatus::SUCCESS;
            joinConfirmParams.m_networkAddress = m_nwkNetworkAddress;
            joinConfirmParams.m_extendedPanId = m_nwkExtendedPanId;
            joinConfirmParams.m_enhancedBeacon = false;

            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_joinParams = {};

            if (!m_nlmeJoinConfirmCallback.IsNull())
            {
                m_nlmeJoinConfirmCallback(joinConfirmParams);
            }
        }
    }
    else if (m_pendPrimitiveNwk == PendingPrimitiveNwk::NLME_START_ROUTER &&
             status == MacStatus::SUCCESS)
    {
        if (id == MacPibAttributeIdentifier::pCurrentChannel)
        {
            // TODO: MLME-START.request should be issue sequentially to all the interfaces in the
            // nwkMacInterfaceTable (currently not supported), for the moment only a single
            // interface is supported.
            MlmeStartRequestParams startParams;
            startParams.m_logCh = m_currentChannel;
            startParams.m_logChPage = 0; // In zigbee, only page 0 is supported.
            startParams.m_PanId = m_nwkPanId;
            startParams.m_bcnOrd = m_startRouterParams.m_beaconOrder;
            startParams.m_sfrmOrd = m_startRouterParams.m_superframeOrder;
            startParams.m_battLifeExt = m_startRouterParams.m_batteryLifeExt;
            startParams.m_coorRealgn = false;
            startParams.m_panCoor = false;

            Simulator::ScheduleNow(&LrWpanMacBase::MlmeStartRequest, m_mac, startParams);
        }
    }
}

void
ZigbeeNwk::MlmeOrphanIndication(MlmeOrphanIndicationParams params)
{
    NS_LOG_FUNCTION(this);

    Ptr<NeighborTableEntry> entry;
    MlmeOrphanResponseParams respParams;

    if (m_nwkNeighborTable.LookUpEntry(params.m_orphanAddr, entry))
    {
        respParams.m_assocMember = true;
        respParams.m_orphanAddr = params.m_orphanAddr;
        respParams.m_shortAddr = entry->GetNwkAddr();

        // Temporarily store the NLME-JOIN.indications parameters that will be
        // returned after the DIRECT_JOIN process concludes.
        // (after MLME-COMM-STATUS.indication is received)
        CapabilityInformation capability;
        capability.SetReceiverOnWhenIdle(entry->IsRxOnWhenIdle());

        if (entry->GetDeviceType() == NwkDeviceType::ZIGBEE_ROUTER)
        {
            capability.SetDeviceType(MacDeviceType::ROUTER);
        }
        else if (entry->GetDeviceType() == NwkDeviceType::ZIGBEE_ENDDEVICE)
        {
            capability.SetDeviceType(MacDeviceType::ENDDEVICE);
        }
        m_joinIndParams.m_capabilityInfo = capability.GetCapability();
        m_joinIndParams.m_extendedAddress = params.m_orphanAddr;
        m_joinIndParams.m_networkAddress = entry->GetNwkAddr();
        m_joinIndParams.m_rejoinNetwork = DIRECT_OR_REJOIN;

        NS_LOG_DEBUG("[NLME-JOIN.request]: ["
                     << params.m_orphanAddr << " | " << entry->GetNwkAddr()
                     << "] found in neighbor table, responding to orphaned device");

        Simulator::ScheduleNow(&LrWpanMacBase::MlmeOrphanResponse, m_mac, respParams);
    }
}

void
ZigbeeNwk::MlmeCommStatusIndication(MlmeCommStatusIndicationParams params)
{
    NS_LOG_FUNCTION(this);

    // Return the results to the next layer of the router or coordinator
    // only after a SUCCESSFUL join to the network.
    if (params.m_status == MacStatus::SUCCESS)
    {
        if (params.m_dstExtAddr == m_joinIndParams.m_extendedAddress &&
            m_joinIndParams.m_rejoinNetwork == DIRECT_OR_REJOIN)
        {
            NlmeJoinIndicationParams joinIndParams = m_joinIndParams;
            m_joinIndParams = {};

            if (!m_nlmeJoinIndicationCallback.IsNull())
            {
                m_nlmeJoinIndicationCallback(joinIndParams);
            }
        }
        else if (params.m_dstExtAddr == m_joinIndParams.m_extendedAddress &&
                 m_joinIndParams.m_rejoinNetwork == ASSOCIATION)
        {
            m_pendPrimitiveNwk = NLME_JOIN_INDICATION;
            UpdateBeaconPayloadLength();
        }
    }

    // TODO: Handle other situations for MlmeCommStatusIndication according
    // to the status and primitive in use.
}

void
ZigbeeNwk::MlmeBeaconNotifyIndication(MlmeBeaconNotifyIndicationParams params)
{
    NS_LOG_FUNCTION(this);

    // Zigbee specification  Section 3.6.1.3
    // Update Neighbor Table with information of the beacon payload
    // during a network-discovery

    if ((params.m_sdu->GetSize() == 0) ||
        (params.m_panDescriptor.m_coorAddrMode != lrwpan::AddressMode::SHORT_ADDR))
    {
        // The beacon do not contain beacon payload or is for a different network
        // stop any further process.
        return;
    }

    ZigbeeBeaconPayload beaconPayload;
    params.m_sdu->RemoveHeader(beaconPayload);

    if (beaconPayload.GetProtocolId() != 0)
    {
        return;
    }

    // TODO: Add a Permit to join, stack profile , update id and capability  check

    if (m_pendPrimitiveNwk == NLME_NET_DISCV)
    {
        // Keep a network descriptor list from the information in the beacon
        // to later on pass to the next higher layer when the network-discovery
        // process is over (NLME-NETWORK-DISCOVERY.confirm)
        NetworkDescriptor descriptor;
        descriptor.m_extPanId = beaconPayload.GetExtPanId();
        descriptor.m_panId = params.m_panDescriptor.m_coorPanId;
        descriptor.m_updateId = 0; // TODO: unknown
        descriptor.m_logCh = params.m_panDescriptor.m_logCh;
        descriptor.m_stackProfile = static_cast<StackProfile>(beaconPayload.GetStackProfile());
        descriptor.m_zigbeeVersion = beaconPayload.GetProtocolId();

        SuperframeInformation superframe(params.m_panDescriptor.m_superframeSpec);
        descriptor.m_beaconOrder = superframe.GetBeaconOrder();
        descriptor.m_superframeOrder = superframe.GetFrameOrder();
        descriptor.m_permitJoining = superframe.IsAssocPermit();

        descriptor.m_routerCapacity = beaconPayload.GetRouterCapacity();
        descriptor.m_endDeviceCapacity = beaconPayload.GetEndDevCapacity();
        m_networkDescriptorList.emplace_back(descriptor);

        // Keep track of the pan id (16 bits) and the extended PAN id for
        // future join (association) procedures.
        m_panIdTable.AddEntry(descriptor.m_extPanId, descriptor.m_panId);
        // NOTE:  In Zigbee all PAN coordinators or routers work with a
        //        SOURCE short address addressing mode, therefore the PAN descriptors only
        //        contain the short address.
        NS_LOG_DEBUG("Received beacon frame from [" << params.m_panDescriptor.m_coorShortAddr
                                                    << "]");
    }

    Ptr<NeighborTableEntry> entry;
    if (m_nwkNeighborTable.LookUpEntry(params.m_panDescriptor.m_coorShortAddr, entry))
    {
        // Update Neighbor table with the info of the received beacon

        entry->SetNwkAddr(params.m_panDescriptor.m_coorShortAddr);
        entry->SetTimeoutCounter(Seconds(15728640));
        entry->SetDevTimeout(Minutes(RequestedTimeoutField[m_nwkEndDeviceTimeoutDefault]));
        entry->SetLqi(params.m_panDescriptor.m_linkQuality);
        entry->SetOutgoingCost(GetLQINonLinearValue(params.m_panDescriptor.m_linkQuality));
        // m_nwkNeighborTable.Update(params.m_panDescriptor.m_coorShortAddr, entry);
        //  TODO: Update other fields if necessary and
        //        Additional and optional fields.
    }
    else
    {
        // Add a new entry to the neighbor table, information comes from
        // the MAC PAN descriptor and the beacon payload received.
        NwkDeviceType devType;
        if (params.m_panDescriptor.m_coorShortAddr == Mac16Address("00:00"))
        {
            devType = NwkDeviceType::ZIGBEE_COORDINATOR;
        }
        else
        {
            devType = NwkDeviceType::ZIGBEE_ROUTER;
        }

        // Create neighbor table entry with the basic fields
        Ptr<NeighborTableEntry> newEntry =
            Create<NeighborTableEntry>(Mac64Address("FF:FF:FF:FF:FF:FF:FF:FF"),
                                       params.m_panDescriptor.m_coorShortAddr,
                                       devType,
                                       true,
                                       0,
                                       Seconds(15728640),
                                       Minutes(RequestedTimeoutField[m_nwkEndDeviceTimeoutDefault]),
                                       NBR_NONE,
                                       0,
                                       params.m_panDescriptor.m_linkQuality,
                                       GetLQINonLinearValue(params.m_panDescriptor.m_linkQuality),
                                       0,
                                       false,
                                       0);

        // If necessary add information to the
        // additional and optional fields. Currently only 2 additional fields are added:
        newEntry->SetExtPanId(beaconPayload.GetExtPanId());
        newEntry->SetLogicalCh(params.m_panDescriptor.m_logCh);

        m_nwkNeighborTable.AddEntry(newEntry);
    }
}

void
ZigbeeNwk::MlmeAssociateIndication(MlmeAssociateIndicationParams params)
{
    NS_LOG_FUNCTION(this);

    // Joining Procedure through Association (Parent procedure)
    // Zigbee Specification 3.6.1.4.1

    CapabilityInformation receivedCapability(params.capabilityInfo);
    auto devType = static_cast<NwkDeviceType>(receivedCapability.GetDeviceType());

    Ptr<NeighborTableEntry> entry;
    if (m_nwkNeighborTable.LookUpEntry(params.m_extDevAddr, entry))
    {
        if (entry->GetDeviceType() == devType)
        {
            MlmeAssociateResponseParams responseParams;
            responseParams.m_status = MacStatus::SUCCESS;
            responseParams.m_assocShortAddr = entry->GetNwkAddr();
            responseParams.m_extDevAddr = entry->GetExtAddr();
            Simulator::ScheduleNow(&LrWpanMacBase::MlmeAssociateResponse, m_mac, responseParams);
        }
        else
        {
            m_nwkNeighborTable.Delete(params.m_extDevAddr);
            MlmeAssociateIndication(params);
        }
    }
    else
    {
        // Device currently do not exist in coordinator,
        // allocate an address and add to neighbor table.

        Mac16Address allocatedAddr;
        if (receivedCapability.IsAllocateAddrOn())
        {
            allocatedAddr = AllocateNetworkAddress();
        }
        else
        {
            // The device is associated but it will only use its
            // extended address (EUI-64 also known as IEEE Address)
            allocatedAddr = Mac16Address("FF:FE");
        }

        CapabilityInformation capability(params.capabilityInfo);

        Ptr<NeighborTableEntry> newEntry =
            Create<NeighborTableEntry>(params.m_extDevAddr,
                                       allocatedAddr,
                                       devType,
                                       capability.IsReceiverOnWhenIdle(),
                                       0,
                                       Seconds(15728640),
                                       Minutes(RequestedTimeoutField[m_nwkEndDeviceTimeoutDefault]),
                                       NBR_CHILD,
                                       0,
                                       params.lqi,
                                       0,
                                       0,
                                       true,
                                       0);
        // Optional parameters
        newEntry->SetExtPanId(m_nwkExtendedPanId);

        MlmeAssociateResponseParams responseParams;
        responseParams.m_extDevAddr = params.m_extDevAddr;

        if (m_nwkNeighborTable.AddEntry(newEntry))
        {
            responseParams.m_status = MacStatus::SUCCESS;
            responseParams.m_assocShortAddr = allocatedAddr;

            // Temporarily store the NLME-JOIN.indications parameters that will be
            // returned after the association process concludes.
            // (after MLME-COMM-STATUS.indication received and beacon payload updated)
            m_joinIndParams.m_capabilityInfo = receivedCapability.GetCapability();
            m_joinIndParams.m_extendedAddress = params.m_extDevAddr;
            m_joinIndParams.m_networkAddress = allocatedAddr;
            m_joinIndParams.m_rejoinNetwork = ASSOCIATION;
        }
        else
        {
            responseParams.m_status = MacStatus::FULL_CAPACITY;
            responseParams.m_assocShortAddr = Mac16Address("FF:FF");
        }

        NS_LOG_DEBUG("\n              "
                     << "Storing an Associate response command with the allocated address "
                     << "[" << responseParams.m_assocShortAddr << "]");

        Simulator::ScheduleNow(&LrWpanMacBase::MlmeAssociateResponse, m_mac, responseParams);
    }
}

void
ZigbeeNwk::NldeDataRequest(NldeDataRequestParams params, Ptr<Packet> packet)
{
    NS_LOG_FUNCTION(this << packet);

    if (params.m_dstAddr == m_nwkNetworkAddress)
    {
        NS_LOG_WARN("The source and the destination of the route request are the same!");
        return;
    }

    // Zigbee specification r22.1.0, Section 3.2.1.1.3 and Section 3.6.2.1
    // check that we are associated
    if (m_nwkNetworkAddress == "FF:FF")
    {
        NS_LOG_WARN("Cannot send data, the device is not currently associated");

        if (!m_nldeDataConfirmCallback.IsNull())
        {
            NldeDataConfirmParams confirmParams;
            confirmParams.m_status = NwkStatus::INVALID_REQUEST;
            confirmParams.m_txTime = Simulator::Now();
            confirmParams.m_nsduHandle = params.m_nsduHandle;
            m_nldeDataConfirmCallback(confirmParams);
        }
        return;
    }

    // Constructing the NPDU (Zigbee specification r22.1.0, Section 3.2.1.1.3 and Section 3.6.2.1)
    ZigbeeNwkHeader nwkHeader;
    nwkHeader.SetFrameType(DATA);
    nwkHeader.SetProtocolVer(3);
    nwkHeader.SetDiscoverRoute(static_cast<DiscoverRouteType>(params.m_discoverRoute));
    nwkHeader.SetDstAddr(params.m_dstAddr);

    if (params.m_useAlias && params.m_dstAddrMode != AddressMode::MCST)
    {
        nwkHeader.SetSrcAddr(params.m_aliasSrcAddr);
        nwkHeader.SetSeqNum(params.m_aliasSeqNumber.GetValue());
    }
    else
    {
        nwkHeader.SetSrcAddr(m_nwkNetworkAddress);
        nwkHeader.SetSeqNum(m_nwkSequenceNumber.GetValue());
    }

    if (params.m_radius == 0)
    {
        nwkHeader.SetRadius(m_nwkMaxDepth * 2);
    }
    else
    {
        nwkHeader.SetRadius(params.m_radius);
    }

    if (params.m_securityEnable)
    {
        // TODO: Secure processing (Section 3.6.2.1)
        NS_ABORT_MSG("Security processing is currently not supported");
    }

    // Check the current device capabilities
    CapabilityInformation capability;
    capability.SetCapability(m_nwkCapabilityInformation);

    if (capability.GetDeviceType() == MacDeviceType::ENDDEVICE)
    {
        nwkHeader.SetEndDeviceInitiator();
    }

    // CASE 1: The destination is a multicast address (Group address)
    // See Sections 3.6.6.2 and 3.2.1.1.3
    if (params.m_dstAddrMode == AddressMode::MCST)
    {
        if (nwkHeader.GetEndDeviceInitiator())
        {
            NS_LOG_WARN("EndDevices cannot send multicast packets, dropping the packet");
            if (!m_nldeDataConfirmCallback.IsNull())
            {
                NldeDataConfirmParams confirmParams;
                confirmParams.m_status = NwkStatus::INVALID_REQUEST;
                confirmParams.m_txTime = Simulator::Now();
                confirmParams.m_nsduHandle = params.m_nsduHandle;
                m_nldeDataConfirmCallback(confirmParams);
            }
            return;
            // TODO: Add trace here.
        }

        nwkHeader.SetMulticast();
        nwkHeader.SetNonMemberRadius(params.m_nonMemberRadius);
        nwkHeader.SetMaxNonMemberRadius(params.m_nonMemberRadius);

        // Check if the current device is member of the multicast Group (dstAddr)
        bool groupMember = m_nwkGroupIdTable->IsGroupMember(params.m_dstAddr.ConvertToInt());

        if (groupMember)
        {
            nwkHeader.SetMulticastMode(MulticastMode::MEMBER);

            // Add an entry to the Broadcast Transaction Table (BTT)
            Ptr<BroadcastTransactionRecord> btr = Create<BroadcastTransactionRecord>(
                nwkHeader.GetSrcAddr(),
                nwkHeader.GetSeqNum(),
                Simulator::Now() + m_nwkNetworkBroadcastDeliveryTime);

            if (!m_btt.AddEntry(btr))
            {
                NS_LOG_DEBUG("Broadcast Transaction Table is full, dropping the packet");
                // TODO: Add multicast packet trace here
                if (!m_nldeDataConfirmCallback.IsNull())
                {
                    NldeDataConfirmParams confirmParams;
                    confirmParams.m_status = NwkStatus::BT_TABLE_FULL;
                    confirmParams.m_txTime = Simulator::Now();
                    confirmParams.m_nsduHandle = params.m_nsduHandle;
                    m_nldeDataConfirmCallback(confirmParams);
                }
                return;
            }
        }
        else
        {
            nwkHeader.SetMulticastMode(MulticastMode::NONMEMBER);
        }

        packet->AddHeader(nwkHeader);

        if (groupMember)
        {
            // See 3.6.6.2.1: Group Members initiate the transmission
            // using a broadcast (MAC level)
            SendDataBcst(packet, params.m_nsduHandle);
        }
        else
        {
            // See 3.6.6.2.2: Non-Group member initiate the transmission
            // using a unicast (MAC level) and search for the next hop
            // in its routing table if necessary.
            SendDataUcst(packet, params.m_nsduHandle);
        }
        return;
    }

    // CASE 2: The destination is a unicast address, a broadcast address
    // or has no address (Many to one routing)

    packet->AddHeader(nwkHeader);

    if (capability.GetDeviceType() == MacDeviceType::ROUTER)
    {
        if (IsBroadcastAddress(params.m_dstAddr))
        {
            // The destination is BROADCAST (See 3.6.5)
            SendDataBcst(packet, params.m_nsduHandle);
        }
        else
        {
            // The destination is UNICAST (See 3.6.3.3)
            SendDataUcst(packet, params.m_nsduHandle);
        }
    }
    else
    {
        // The device is an END DEVICE
        // direct message to its Parent device (Coordinator)
        Ptr<NeighborTableEntry> entry;
        if (m_nwkNeighborTable.GetParent(entry))
        {
            // Buffer a copy of the DATA packet that will be transmitted
            // for handling after transmission (i.e. NSDE-DATA.confirm)
            BufferTxPkt(packet->Copy(), m_macHandle.GetValue(), params.m_nsduHandle);

            McpsDataRequestParams mcpsDataparams;
            mcpsDataparams.m_txOptions = 0x01; // Acknowledment on.
            mcpsDataparams.m_dstPanId = m_nwkPanId;
            mcpsDataparams.m_msduHandle = m_macHandle.GetValue();
            mcpsDataparams.m_srcAddrMode = SHORT_ADDR;
            mcpsDataparams.m_dstAddrMode = SHORT_ADDR;
            mcpsDataparams.m_dstAddr = entry->GetNwkAddr();
            m_macHandle++;
            Simulator::ScheduleNow(&LrWpanMacBase::McpsDataRequest, m_mac, mcpsDataparams, packet);
        }
        else
        {
            // Section 3.6.3.7.1
            // Link failure with Parent device
            // TODO
            /*
            if (!m_nlmeNwkStatusIndicationCallback.IsNull())
            {
                NlmeNetworkStatusIndication indicationParams;

                 m_networkStatusCode = PARENT_LINK_FAILURE;
                m_nlmeNwkStatusIndicationCallback(confirmParams);
            }
            */
        }
    }
    m_pendPrimitiveNwk = NLDE_NLME_NONE;
}

void
ZigbeeNwk::NlmeNetworkFormationRequest(NlmeNetworkFormationRequestParams params)
{
    NS_LOG_FUNCTION(this);

    NS_ASSERT_MSG(m_netFormParams.m_scanChannelList.channelPageCount ==
                      m_netFormParams.m_scanChannelList.channelsField.size(),
                  "channelsField and its channelPageCount size do not match "
                  "in networkFormationParams");

    if (!m_nwkcCoordinatorCapable)
    {
        m_pendPrimitiveNwk = NLDE_NLME_NONE;
        m_netFormParams = {};
        m_netFormParamsGen = nullptr;

        if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
        {
            NlmeNetworkFormationConfirmParams confirmParams;
            confirmParams.m_status = NwkStatus::INVALID_REQUEST;
            m_nlmeNetworkFormationConfirmCallback(confirmParams);
        }
        return;
    }

    if (params.m_distributedNetwork)
    {
        // Zigbee Specification r22.1.0, 3.2.2.5 , 3)
        // Verify Distributed Network Address is in a valid range.
        // TODO: Verify the address is not > 0xFFF7
        if (params.m_distributedNetwork == Mac16Address("00:00"))
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_netFormParams = {};
            m_netFormParamsGen = nullptr;

            if (!m_nlmeNetworkFormationConfirmCallback.IsNull())
            {
                NlmeNetworkFormationConfirmParams confirmParams;
                confirmParams.m_status = NwkStatus::INVALID_REQUEST;
                m_nlmeNetworkFormationConfirmCallback(confirmParams);
            }
            return;
        }
    }

    // 4. On receipt of this primitive the NLME shall first validate the
    //  ChannelListStructure parameter according to section 3.2.2.2.2.
    // (if nwkMacInterfaceTable support is added)
    // If validation fails the NLME-NETWORK-FORMATION.confirm
    // primitive shall be issued with a Status parameter set to INVALID_PARAMETER.

    if (params.m_scanChannelList.channelPageCount != 1)
    {
        NS_FATAL_ERROR("Multi page scanning not supported");
    }

    // Only page 0 is supported (O-QPSK 250 kbps)
    // Take 5 MSB bits: b27-b31 to check the page
    uint32_t page = (params.m_scanChannelList.channelsField[0] >> 27) & (0x01F);

    if (page != 0)
    {
        NS_FATAL_ERROR("PHY band not supported (Only page 0 is supported)");
    }

    uint8_t channelsCount = 0;
    for (int i = 11; i <= 26; i++)
    {
        channelsCount += (params.m_scanChannelList.channelsField[0] >> i) & 1;
    }

    m_pendPrimitiveNwk = NLME_NETWORK_FORMATION;
    m_netFormParams = params;

    if (channelsCount == 1)
    {
        // There is only 1 channel, skip energy scan and go directly to
        // active scan instead
        MlmeScanRequestParams mlmeParams;
        mlmeParams.m_chPage = page;
        mlmeParams.m_scanChannels = params.m_scanChannelList.channelsField[0];
        mlmeParams.m_scanDuration = params.m_scanDuration;
        mlmeParams.m_scanType = MLMESCAN_ACTIVE;
        Simulator::ScheduleNow(&LrWpanMacBase::MlmeScanRequest, m_mac, mlmeParams);
    }
    else if (channelsCount > 1)
    {
        MlmeScanRequestParams mlmeParams;
        mlmeParams.m_chPage = page;
        mlmeParams.m_scanChannels = params.m_scanChannelList.channelsField[0];
        mlmeParams.m_scanDuration = params.m_scanDuration;
        mlmeParams.m_scanType = MLMESCAN_ED;
        Simulator::ScheduleNow(&LrWpanMacBase::MlmeScanRequest, m_mac, mlmeParams);
    }
}

void
ZigbeeNwk::NlmeRouteDiscoveryRequest(NlmeRouteDiscoveryRequestParams params)
{
    NS_LOG_FUNCTION(this);

    if (params.m_dstAddr == m_nwkNetworkAddress && params.m_dstAddrMode == UCST_BCST)
    {
        NS_FATAL_ERROR("The source and the destination of the route request are the same!");
        return;
    }

    // (See 3.2.2.33.3)
    // - Check the device has routing capacity
    // - Check the device dstAddrMode != NO_ADDRESS &&  dst != Broadcast address
    if (params.m_dstAddrMode != NO_ADDRESS && IsBroadcastAddress(params.m_dstAddr))
    {
        if (!m_nlmeRouteDiscoveryConfirmCallback.IsNull())
        {
            NlmeRouteDiscoveryConfirmParams confirmParams;
            confirmParams.m_status = NwkStatus::INVALID_REQUEST;
            m_nlmeRouteDiscoveryConfirmCallback(confirmParams);
        }

        NS_LOG_WARN("The destination address is a broadcast address, or the destination "
                    "address mode is NO_ADDRESS, cannot send route discovery request");

        return;
    }

    CapabilityInformation capability;
    capability.SetCapability(m_nwkCapabilityInformation);
    if (capability.GetDeviceType() != ROUTER && params.m_dstAddrMode != NO_ADDRESS)
    {
        if (!m_nlmeRouteDiscoveryConfirmCallback.IsNull())
        {
            NlmeRouteDiscoveryConfirmParams confirmParams;
            confirmParams.m_status = NwkStatus::ROUTE_ERROR;
            confirmParams.m_networkStatusCode = NetworkStatusCode::NO_ROUTING_CAPACITY;
            m_nlmeRouteDiscoveryConfirmCallback(confirmParams);
        }

        NS_LOG_WARN("Device does not have routing capacity, cannot send route discovery request");

        return;
    }

    m_pendPrimitiveNwk = NLME_ROUTE_DISCOVERY;

    ZigbeeNwkHeader nwkHeader;
    nwkHeader.SetFrameType(NWK_COMMAND);
    nwkHeader.SetProtocolVer(m_nwkcProtocolVersion);
    nwkHeader.SetDiscoverRoute(DiscoverRouteType::ENABLE_ROUTE_DISCOVERY);
    // See r22.1.0, Table 3-69
    // Set destination to broadcast (all routers and coordinator)
    nwkHeader.SetDstAddr(Mac16Address("FF:FC"));
    nwkHeader.SetSrcAddr(m_nwkNetworkAddress);
    nwkHeader.SetSeqNum(m_nwkSequenceNumber.GetValue());

    ZigbeePayloadRouteRequestCommand payload;
    payload.SetRouteReqId(m_routeRequestId.GetValue());
    payload.SetPathCost(0);

    if (params.m_dstAddrMode == AddressMode::UCST_BCST || params.m_dstAddrMode == AddressMode::MCST)
    {
        // Set the rest of the nwkHeader and command payload (RREQ) parameters
        // as described in Zigbee specification r22.1.0, Section 3.2.2.33.3

        if (params.m_radius == 0)
        {
            nwkHeader.SetRadius(m_nwkMaxDepth * 2);
        }
        else
        {
            nwkHeader.SetRadius(params.m_radius);
        }

        payload.SetDstAddr(params.m_dstAddr);

        if (params.m_dstAddrMode == AddressMode::MCST)
        {
            // Set the Multicast flag and proceed with the
            // multicast destination route discovery
            payload.SetMulticastField(true);
        }

        Mac16Address nextHop;
        RouteDiscoveryStatus routeStatus =
            FindNextHop(m_nwkNetworkAddress, 0, nwkHeader, payload, nextHop);

        if (routeStatus == ROUTE_FOUND)
        {
            if (!m_nlmeRouteDiscoveryConfirmCallback.IsNull())
            {
                NlmeRouteDiscoveryConfirmParams confirmParams;
                confirmParams.m_status = NwkStatus::SUCCESS;
                m_nlmeRouteDiscoveryConfirmCallback(confirmParams);
            }
        }
        else if (routeStatus == ROUTE_NOT_FOUND)
        {
            // Route not found. Route marked as DISCOVER UNDERWAY,
            // we initiate route discovery.
            Simulator::Schedule(MilliSeconds(m_rreqJitter->GetValue()),
                                &ZigbeeNwk::SendRREQ,
                                this,
                                nwkHeader,
                                payload,
                                m_nwkcInitialRREQRetries);

            m_nwkSequenceNumber++;
            m_routeRequestId++;
        }
    }
    else if (params.m_dstAddrMode == NO_ADDRESS)
    {
        // Many-to-one route discovery.
        // (See Last paragraph of Zigbee Specification, Section 3.6.3.5.1)
        m_nwkIsConcentrator = true;

        nwkHeader.SetRadius(m_nwkConcentratorRadius);

        payload.SetDstAddr(Mac16Address("FF:FF"));
        if (params.m_noRouteCache)
        {
            payload.SetCmdOptManyToOneField(ManyToOne::NO_ROUTE_RECORD);
        }
        else
        {
            payload.SetCmdOptManyToOneField(ManyToOne::ROUTE_RECORD);
        }

        RouteDiscoveryStatus routeStatus =
            ProcessManyToOneRoute(m_nwkNetworkAddress, 0, nwkHeader, payload);

        if (routeStatus == MANY_TO_ONE_ROUTE || routeStatus == ROUTE_UPDATED)
        {
            // TODO if nwkConcentratorDiscoveryTime != 0,  schedule
            // RREQ every nwkConcentratorDiscovery time.

            Simulator::Schedule(MilliSeconds(m_rreqJitter->GetValue()),
                                &ZigbeeNwk::SendRREQ,
                                this,
                                nwkHeader,
                                payload,
                                0);
            m_nwkSequenceNumber++;
            m_routeRequestId++;
        }
    }
}

void
ZigbeeNwk::NlmeNetworkDiscoveryRequest(NlmeNetworkDiscoveryRequestParams params)
{
    NS_LOG_FUNCTION(this);

    if (params.m_scanDuration > 14)
    {
        NS_FATAL_ERROR("Scan duration must be an int between 0 and 14");
    }

    if (params.m_scanChannelList.channelPageCount != params.m_scanChannelList.channelsField.size())
    {
        NS_FATAL_ERROR("In scanChannelList parameter, channelPageCount "
                       "and the channelsField structure size does not match");
    }

    // TODO: Add support to scan other MAC interfaces, for the moment
    // only a single interface and only Page 0 is supported (PHY O-QPSK 250 kbps)

    if (params.m_scanChannelList.channelsField.size() != 1)
    {
        NS_FATAL_ERROR("Only a single MAC interface supported");
    }

    uint8_t page = (params.m_scanChannelList.channelsField[0] >> 27) & (0x01F);
    if (page != 0)
    {
        NS_FATAL_ERROR("Only Page 0 (O-QPSK 250 kbps) is supported.");
    }

    m_pendPrimitiveNwk = NLME_NET_DISCV;

    MlmeScanRequestParams scanParams;
    scanParams.m_chPage = 0; // Only page 0 is supported.
    scanParams.m_scanChannels = params.m_scanChannelList.channelsField[0];
    scanParams.m_scanDuration = params.m_scanDuration;
    scanParams.m_scanType = MLMESCAN_ACTIVE;

    NS_LOG_DEBUG("Active scanning started, "
                 << " on page " << page << " and channels 0x" << std::hex
                 << params.m_scanChannelList.channelsField[0] << std::dec);

    Simulator::ScheduleNow(&LrWpanMacBase::MlmeScanRequest, m_mac, scanParams);
}

void
ZigbeeNwk::NlmeDirectJoinRequest(NlmeDirectJoinRequestParams params)
{
    NS_LOG_FUNCTION(this);

    // TODO: Check the device is router or coordinator, send invalid_request
    //       status otherwise. See 3.6.1.4.3.

    Ptr<NeighborTableEntry> entry;
    if (m_nwkNeighborTable.LookUpEntry(params.m_deviceAddr, entry))
    {
        NS_LOG_WARN("[NLME-DIRECT-JOIN.request]: "
                    "Device already present in neighbor table. ");

        if (!m_nlmeDirectJoinConfirmCallback.IsNull())
        {
            NlmeDirectJoinConfirmParams confirmParams;
            confirmParams.m_status = NwkStatus::ALREADY_PRESENT;
            confirmParams.m_deviceAddr = params.m_deviceAddr;
            m_nlmeDirectJoinConfirmCallback(confirmParams);
        }
    }
    else
    {
        CapabilityInformation capaInfo;
        capaInfo.SetCapability(params.m_capabilityInfo);

        Mac16Address allocatedAddr;
        if (capaInfo.IsAllocateAddrOn())
        {
            allocatedAddr = AllocateNetworkAddress();
        }
        else
        {
            // The device is associated but it will only use its
            // extended address (EUI-64 also known as IEEE Address)
            allocatedAddr = Mac16Address("FF:FE");
        }

        NwkDeviceType devType;
        if (capaInfo.GetDeviceType() == MacDeviceType::ROUTER)
        {
            devType = NwkDeviceType::ZIGBEE_ROUTER;
        }
        else
        {
            devType = NwkDeviceType::ZIGBEE_ENDDEVICE;
        }

        Ptr<NeighborTableEntry> newEntry =
            Create<NeighborTableEntry>(params.m_deviceAddr,
                                       allocatedAddr,
                                       devType,
                                       capaInfo.IsReceiverOnWhenIdle(),
                                       0,
                                       Seconds(15728640),
                                       Minutes(RequestedTimeoutField[m_nwkEndDeviceTimeoutDefault]),
                                       NBR_CHILD,
                                       0,
                                       255,
                                       0,
                                       0,
                                       true,
                                       0);

        NlmeDirectJoinConfirmParams confirmParams;

        if (m_nwkNeighborTable.AddEntry(newEntry))
        {
            NS_LOG_DEBUG("Device added to neighbor table (" << m_nwkNeighborTable.GetSize()
                                                            << ") with address [" << allocatedAddr
                                                            << " | " << params.m_deviceAddr << "]");
            if (!m_nlmeDirectJoinConfirmCallback.IsNull())
            {
                confirmParams.m_status = NwkStatus::SUCCESS;
                confirmParams.m_deviceAddr = params.m_deviceAddr;
                m_nlmeDirectJoinConfirmCallback(confirmParams);
            }
        }
        else
        {
            NS_LOG_WARN("Error, neighbor table is full");
            if (!m_nlmeDirectJoinConfirmCallback.IsNull())
            {
                confirmParams.m_status = NwkStatus::NEIGHBOR_TABLE_FULL;
                confirmParams.m_deviceAddr = params.m_deviceAddr;
                m_nlmeDirectJoinConfirmCallback(confirmParams);
            }
        }
    }
}

void
ZigbeeNwk::NlmeJoinRequest(NlmeJoinRequestParams params)
{
    NS_LOG_FUNCTION(this);

    if (params.m_scanDuration > 14)
    {
        NS_FATAL_ERROR("Scan duration must be an int between 0 and 14");
    }

    if (params.m_scanChannelList.channelPageCount != params.m_scanChannelList.channelsField.size())
    {
        NS_FATAL_ERROR("In scanChannelList parameter, channelPageCount "
                       "and the channelsField structure size does not match");
    }

    // TODO: Add support to scan other MAC interfaces, for the moment
    // only a single interface and only Page 0 is supported (PHY O-QPSK 250 kbps)

    // TODO: Only devices who have not join another network can call JOIN.

    if (params.m_scanChannelList.channelsField.size() != 1)
    {
        NS_FATAL_ERROR("Only a single MAC interface supported");
    }

    uint8_t page = (params.m_scanChannelList.channelsField[0] >> 27) & (0x01F);
    if (page != 0)
    {
        NS_FATAL_ERROR("Only Page 0 (O-QPSK 250 kbps) is supported.");
    }

    m_pendPrimitiveNwk = NLME_JOIN;
    m_joinParams = params;

    if (params.m_rejoinNetwork == DIRECT_OR_REJOIN)
    {
        // Zigbee specification r22.1.0 Section 3.6.1.4.3.1,
        // Child procedure for joining or re-joining a network through
        // orphaning (DIRECT JOIN procedure).

        MlmeScanRequestParams scanParams;
        scanParams.m_chPage = page;
        scanParams.m_scanChannels = params.m_scanChannelList.channelsField[0];

        // Note: Scan duration is fixed to a macResponseWaitTime in an Orphan scan
        //       (i.e. It does not use the scanDuration parameter)
        scanParams.m_scanType = MLMESCAN_ORPHAN;
        NS_LOG_DEBUG("Orphan scanning started, sending orphan notifications on page "
                     << page << " and channels " << std::hex
                     << params.m_scanChannelList.channelsField[0]);

        Simulator::ScheduleNow(&LrWpanMacBase::MlmeScanRequest, m_mac, scanParams);
    }
    else if (params.m_rejoinNetwork == ASSOCIATION)
    {
        // Check if we have the MAC pan id info recorded during the discovery process
        uint16_t panId;
        if (!m_panIdTable.GetEntry(params.m_extendedPanId, panId))
        {
            NS_LOG_ERROR("Error PAN id of neighbor device not found");
        }

        // Zigbee specification r22.1.0 Section 3.6.1.4.1
        // Child procedure for joining a network through ASSOCIATION.

        NlmeJoinConfirmParams joinConfirmParams;
        Ptr<NeighborTableEntry> bestParentEntry;

        if (m_nwkNeighborTable.LookUpForBestParent(params.m_extendedPanId, bestParentEntry))
        {
            MlmeAssociateRequestParams assocParams;
            m_nwkCapabilityInformation = params.m_capabilityInfo;

            assocParams.m_chNum = bestParentEntry->GetLogicalCh();
            assocParams.m_chPage = 0; // Zigbee assumes Page is always 0
            assocParams.m_capabilityInfo = params.m_capabilityInfo;
            assocParams.m_coordPanId = panId;

            if (bestParentEntry->GetNwkAddr() != Mac16Address("FF:FE"))
            {
                assocParams.m_coordAddrMode = lrwpan::AddressMode::SHORT_ADDR;
                assocParams.m_coordShortAddr = bestParentEntry->GetNwkAddr();
                NS_LOG_DEBUG("\n              "
                             << "Send Association Request [" << bestParentEntry->GetNwkAddr()
                             << "] | PAN ID: " << std::hex << "0x" << panId
                             << " | Extended PAN ID: 0x" << params.m_extendedPanId << std::dec);
            }
            else
            {
                assocParams.m_coordAddrMode = lrwpan::AddressMode::EXT_ADDR;
                assocParams.m_coordExtAddr = bestParentEntry->GetExtAddr();
                NS_LOG_DEBUG("Send Assoc. Req. to [" << bestParentEntry->GetNwkAddr()
                                                     << "] in \nPAN id and Ext PAN id: " << std::hex
                                                     << "(0x" << panId << " | 0x"
                                                     << params.m_extendedPanId << ")" << std::dec);
            }

            m_nwkParentInformation = 0;
            m_nwkCapabilityInformation = params.m_capabilityInfo;

            // Temporarily store MLME-ASSOCIATE.request parameters until the JOIN process concludes
            m_associateParams = assocParams;

            Simulator::ScheduleNow(&LrWpanMacBase::MlmeAssociateRequest, m_mac, assocParams);
        }
        else
        {
            m_pendPrimitiveNwk = NLDE_NLME_NONE;
            m_joinParams = {};

            if (!m_nlmeJoinConfirmCallback.IsNull())
            {
                joinConfirmParams.m_extendedPanId = params.m_extendedPanId;
                joinConfirmParams.m_networkAddress = Mac16Address("FF:FF");
                joinConfirmParams.m_enhancedBeacon = false;
                joinConfirmParams.m_macInterfaceIndex = 0;
                joinConfirmParams.m_status = NwkStatus::NOT_PERMITED;
                m_nlmeJoinConfirmCallback(joinConfirmParams);
            }
        }
    }
    else
    {
        NS_FATAL_ERROR("Joining method not supported");
    }
}

void
ZigbeeNwk::NlmeStartRouterRequest(NlmeStartRouterRequestParams params)
{
    NS_LOG_FUNCTION(this);

    NS_ASSERT_MSG(params.m_beaconOrder == 15, "Beacon mode not supported for zigbee");
    NS_ASSERT_MSG(params.m_superframeOrder == 15, "Beacon mode not supported for zigbee");

    CapabilityInformation capability;
    capability.SetCapability(m_nwkCapabilityInformation);

    if (capability.GetDeviceType() != MacDeviceType::ROUTER)
    {
        m_pendPrimitiveNwk = NLDE_NLME_NONE;
        m_startRouterParams = {};
        if (!m_nlmeStartRouterConfirmCallback.IsNull())
        {
            NlmeStartRouterConfirmParams confirmParams;
            confirmParams.m_status = NwkStatus::INVALID_REQUEST;
            m_nlmeStartRouterConfirmCallback(confirmParams);
        }
        NS_LOG_ERROR("This device is not a Zigbee Router or is not joined to this network");
    }
    else
    {
        m_pendPrimitiveNwk = NLME_START_ROUTER;
        // store the NLME-START-ROUTER.request params while request the current channel
        m_startRouterParams = params;
        // request an update of the current channel in use in the PHY
        Simulator::ScheduleNow(&LrWpanMacBase::MlmeGetRequest,
                               m_mac,
                               MacPibAttributeIdentifier::pCurrentChannel);
    }
}

void
ZigbeeNwk::SetNldeDataIndicationCallback(NldeDataIndicationCallback c)
{
    m_nldeDataIndicationCallback = c;
}

void
ZigbeeNwk::SetNldeDataConfirmCallback(NldeDataConfirmCallback c)
{
    m_nldeDataConfirmCallback = c;
}

void
ZigbeeNwk::SetNlmeNetworkFormationConfirmCallback(NlmeNetworkFormationConfirmCallback c)
{
    m_nlmeNetworkFormationConfirmCallback = c;
}

void
ZigbeeNwk::SetNlmeNetworkDiscoveryConfirmCallback(NlmeNetworkDiscoveryConfirmCallback c)
{
    m_nlmeNetworkDiscoveryConfirmCallback = c;
}

void
ZigbeeNwk::SetNlmeRouteDiscoveryConfirmCallback(NlmeRouteDiscoveryConfirmCallback c)
{
    m_nlmeRouteDiscoveryConfirmCallback = c;
}

void
ZigbeeNwk::SetNlmeDirectJoinConfirmCallback(NlmeDirectJoinConfirmCallback c)
{
    m_nlmeDirectJoinConfirmCallback = c;
}

void
ZigbeeNwk::SetNlmeJoinConfirmCallback(NlmeJoinConfirmCallback c)
{
    m_nlmeJoinConfirmCallback = c;
}

void
ZigbeeNwk::SetNlmeJoinIndicationCallback(NlmeJoinIndicationCallback c)
{
    m_nlmeJoinIndicationCallback = c;
}

void
ZigbeeNwk::SetNlmeStartRouterConfirmCallback(NlmeStartRouterConfirmCallback c)
{
    m_nlmeStartRouterConfirmCallback = c;
}

void
ZigbeeNwk::EnqueuePendingTx(Ptr<Packet> p, uint8_t nsduHandle)
{
    // TODO : PurgeTxQueue();
    if (m_pendingTxQueue.size() < m_maxPendingTxQueueSize)
    {
        ZigbeeNwkHeader peekedNwkHeader;
        p->PeekHeader(peekedNwkHeader);

        auto pendingTxPkt = std::make_shared<PendingTxPkt>();
        pendingTxPkt->dstAddr = peekedNwkHeader.GetDstAddr();
        pendingTxPkt->nsduHandle = nsduHandle;
        pendingTxPkt->txPkt = p;
        // TODO: expiration time here
        m_pendingTxQueue.emplace_back(pendingTxPkt);
        // TODO: pending trace here
    }
    else
    {
        // TODO: Drop trace here
    }
}

bool
ZigbeeNwk::DequeuePendingTx(Mac16Address dst, std::shared_ptr<PendingTxPkt> entry)
{
    // TODO : PurgeTxQueue();

    /* std::erase_if(m_pendingTxQueue, [&dst](Ptr<PendingTxPkt> pkt) {
         return pkt->dstAddr == dst;
     });*/

    for (auto iter = m_pendingTxQueue.begin(); iter != m_pendingTxQueue.end(); iter++)
    {
        if ((*iter)->dstAddr == dst)
        {
            *entry = **iter;
            // TODO: Dequeue trace if needed here.
            m_pendingTxQueue.erase(iter);
            return true;
        }
    }
    return false;
}

void
ZigbeeNwk::DisposePendingTx()
{
    for (auto element : m_pendingTxQueue)
    {
        element = nullptr;
    }
    m_pendingTxQueue.clear();
}

void
ZigbeeNwk::BufferTxPkt(Ptr<Packet> p, uint8_t macHandle, uint8_t nwkHandle)
{
    if (m_txBuffer.size() < m_txBufferMaxSize)
    {
        auto txPkt = std::make_shared<TxPkt>();
        txPkt->macHandle = macHandle;
        txPkt->nwkHandle = nwkHandle;
        txPkt->txPkt = p;
        m_txBuffer.emplace_back(txPkt);
    }
    else
    {
        NS_LOG_DEBUG("Zigbee Tx Buffer is full, packet dropped.");
        // TODO : Drop trace for TX buffer
    }
}

bool
ZigbeeNwk::RetrieveTxPkt(uint8_t macHandle, std::shared_ptr<TxPkt>& txPkt)
{
    for (const auto& bufferedPkt : m_txBuffer)
    {
        if (bufferedPkt->macHandle == macHandle)
        {
            txPkt = bufferedPkt;

            std::erase_if(m_txBuffer, [&macHandle](std::shared_ptr<TxPkt> pkt) {
                return pkt->macHandle == macHandle;
            });

            return true;
        }
    }
    return false;
}

void
ZigbeeNwk::DisposeTxPktBuffer()
{
    for (auto element : m_txBuffer)
    {
        element = nullptr;
    }
    m_txBuffer.clear();
}

NwkStatus
ZigbeeNwk::GetNwkStatus(MacStatus macStatus) const
{
    return static_cast<NwkStatus>(macStatus);
}

Mac16Address
ZigbeeNwk::AllocateNetworkAddress()
{
    NS_LOG_FUNCTION(this);

    if (m_nwkAddrAlloc == DISTRIBUTED_ALLOC)
    {
        NS_FATAL_ERROR("Distributed allocation not supported");
        return Mac16Address("FF:FF");
    }
    else if (m_nwkAddrAlloc == STOCHASTIC_ALLOC)
    {
        // See nwkNetworkAddress valid range Zigbee specification r22.1.0, 3.5.2
        // Valid values in the Zigbee specification goes from 1 to 0xFFF7,
        // However, the range 0x8000 to 0x9FFF is used for multicast in other networks
        // (i.e. IPV6 over IEEE 802.15.4) for this reason, we avoid this range as well.
        // See RFC 4944, Section 9
        uint16_t rndValue = m_uniformRandomVariable->GetInteger(1, 0x7FFF);
        uint16_t rndValue2 = m_uniformRandomVariable->GetInteger(0xA000, 0xFFF7);
        uint16_t rndValue3 = m_uniformRandomVariable->GetInteger(1, 2);

        Mac16Address allocAddr;
        if (rndValue3 == 1)
        {
            allocAddr = Mac16Address(rndValue);
        }
        else
        {
            allocAddr = Mac16Address(rndValue2);
        }
        return allocAddr;
    }
    else
    {
        NS_FATAL_ERROR("Address allocation method not supported");
        return Mac16Address("FF:FF");
    }
}

uint8_t
ZigbeeNwk::GetLQINonLinearValue(uint8_t lqi) const
{
    uint8_t mappedValue;

    if (lqi > 50)
    {
        mappedValue = 1;
    }
    else if ((lqi <= 50) && (lqi > 45))
    {
        mappedValue = 2;
    }
    else if ((lqi <= 45) && (lqi > 40))
    {
        mappedValue = 3;
    }
    else if ((lqi <= 40) && (lqi > 38))
    {
        mappedValue = 4;
    }
    else if ((lqi <= 38) && (lqi > 35))
    {
        mappedValue = 5;
    }
    else if ((lqi <= 35) && (lqi > 24))
    {
        mappedValue = 6;
    }
    else
    {
        mappedValue = 7;
    }

    return mappedValue;
}

uint8_t
ZigbeeNwk::GetLinkCost(uint8_t lqi) const
{
    NS_LOG_FUNCTION(this);

    if (m_nwkReportConstantCost)
    {
        // Hop count based. Report constant value
        return 7;
    }
    else
    {
        // Based on non-linear mapping of LQI
        return GetLQINonLinearValue(lqi);
    }
}

void
ZigbeeNwk::SendRREQ(ZigbeeNwkHeader nwkHeader,
                    ZigbeePayloadRouteRequestCommand payload,
                    uint8_t rreqRetries)
{
    NS_LOG_FUNCTION(this);

    ZigbeePayloadType payloadType(ROUTE_REQ_CMD);

    Ptr<Packet> nsdu = Create<Packet>();
    nsdu->AddHeader(payload);
    nsdu->AddHeader(payloadType);
    nsdu->AddHeader(nwkHeader);

    if (payload.GetCmdOptManyToOneField() == ManyToOne::NO_MANY_TO_ONE &&
        nwkHeader.GetRadius() != 0)
    {
        // Set RREQ RETRIES
        Time rreqRetryTime = m_nwkcRREQRetryInterval + MilliSeconds(m_rreqJitter->GetValue());

        Ptr<RreqRetryTableEntry> rreqRetryTableEntry;
        if (m_rreqRetryTable.LookUpEntry(payload.GetRouteReqId(), rreqRetryTableEntry))
        {
            if (rreqRetryTableEntry->GetRreqRetryCount() >= rreqRetries)
            {
                NS_LOG_DEBUG("Maximum RREQ retries reached for dst [" << payload.GetDstAddr()
                                                                      << "] and rreq ID "
                                                                      << payload.GetRouteReqId());
                // Note: The value of the maximum number of retries (rreqRetries) is either
                // nwkcInitialRREQRetries or nwkcRREQRetries depending on where the RREQ is
                // transmitted. See Zigbee specification r22.1.0, Section 3.6.3.5.1 This trace here
                // is used to keep track when the maximum RREQ retries is reached.
                m_rreqRetriesExhaustedTrace(payload.GetRouteReqId(),
                                            payload.GetDstAddr(),
                                            rreqRetries);
            }
            else
            {
                // Schedule the next RREQ RETRY event and update entry.
                EventId rreqRetryEvent = Simulator::Schedule(rreqRetryTime,
                                                             &ZigbeeNwk::SendRREQ,
                                                             this,
                                                             nwkHeader,
                                                             payload,
                                                             rreqRetries);

                rreqRetryTableEntry->SetRreqRetryCount(rreqRetryTableEntry->GetRreqRetryCount() +
                                                       1);
                rreqRetryTableEntry->SetRreqEventId(rreqRetryEvent);
            }
        }
        else
        {
            // Schedule the next RREQ RETRY and add a new record of the event.
            EventId rreqRetryEvent = Simulator::Schedule(rreqRetryTime,
                                                         &ZigbeeNwk::SendRREQ,
                                                         this,
                                                         nwkHeader,
                                                         payload,
                                                         rreqRetries);

            Ptr<RreqRetryTableEntry> newEntry =
                Create<RreqRetryTableEntry>(payload.GetRouteReqId(), rreqRetryEvent, 0);

            m_rreqRetryTable.AddEntry(newEntry);
        }
    }

    // Send the RREQ
    // See Section 3.4.1.1 Mac Data Service Requirements for RREQ
    if (nwkHeader.GetRadius() != 0)
    {
        McpsDataRequestParams params;
        params.m_dstPanId = m_nwkPanId;
        params.m_srcAddrMode = SHORT_ADDR;
        params.m_dstAddrMode = SHORT_ADDR;
        params.m_dstAddr = Mac16Address::GetBroadcast();
        params.m_msduHandle = m_macHandle.GetValue();
        m_macHandle++;
        Simulator::ScheduleNow(&LrWpanMacBase::McpsDataRequest, m_mac, params, nsdu);
    }
    else
    {
        NS_LOG_DEBUG("Maximum radius reached, dropping RREQ");
    }
}

void
ZigbeeNwk::SendRREP(Mac16Address nextHop,
                    Mac16Address originator,
                    Mac16Address responder,
                    uint8_t rreqId,
                    uint8_t pathcost)
{
    NS_LOG_FUNCTION(this);

    ZigbeeNwkHeader nwkHeader;
    nwkHeader.SetFrameType(NWK_COMMAND);
    nwkHeader.SetProtocolVer(m_nwkcProtocolVersion);
    nwkHeader.SetDiscoverRoute(DiscoverRouteType::ENABLE_ROUTE_DISCOVERY);

    nwkHeader.SetDstAddr(nextHop);
    nwkHeader.SetSrcAddr(m_nwkNetworkAddress);
    m_nwkSequenceNumber++;
    nwkHeader.SetSeqNum(m_nwkSequenceNumber.GetValue());
    // see Zigbee specification 3.4.2.2
    // Use the maximum possible radius
    nwkHeader.SetRadius(m_nwkMaxDepth * 2);

    ZigbeePayloadType payloadType(ROUTE_REP_CMD);

    ZigbeePayloadRouteReplyCommand payload;
    payload.SetRouteReqId(rreqId);
    payload.SetOrigAddr(originator);
    payload.SetRespAddr(responder);
    payload.SetPathCost(pathcost);

    // See Section 3.4.2 Mac Data Service Requirements for RREP
    McpsDataRequestParams params;
    params.m_dstPanId = m_nwkPanId;
    params.m_srcAddrMode = SHORT_ADDR;
    params.m_dstAddrMode = SHORT_ADDR;
    params.m_dstAddr = nextHop;
    params.m_msduHandle = m_macHandle.GetValue();
    m_macHandle++;

    Ptr<Packet> nsdu = Create<Packet>();
    nsdu->AddHeader(payload);
    nsdu->AddHeader(payloadType);
    nsdu->AddHeader(nwkHeader);

    Simulator::ScheduleNow(&LrWpanMacBase::McpsDataRequest, m_mac, params, nsdu);
}

int64_t
ZigbeeNwk::AssignStreams(int64_t stream)
{
    NS_LOG_FUNCTION(this << stream);
    m_uniformRandomVariable->SetStream(stream);
    return 1;
}

void
ZigbeeNwk::UpdateBeaconPayloadLength()
{
    NS_LOG_FUNCTION(this);

    ZigbeeBeaconPayload beaconPayloadHeader;
    beaconPayloadHeader.SetStackProfile(static_cast<uint8_t>(m_nwkStackProfile));
    beaconPayloadHeader.SetRouterCapacity(m_nwkcCoordinatorCapable);
    beaconPayloadHeader.SetDeviceDepth(0); // Not used by stack profile (0x02 =ZIGBEE pro)
    beaconPayloadHeader.SetEndDevCapacity(true);
    beaconPayloadHeader.SetExtPanId(m_nwkExtendedPanId);
    beaconPayloadHeader.SetTxOffset(0xFFFFFF);
    // TODO: beaconPayload.SetNwkUpdateId(m_nwkUpdateId);

    // Set the beacon payload length in the MAC PIBs and then proceed to
    // update the beacon payload
    m_beaconPayload = Create<Packet>();
    m_beaconPayload->AddHeader(beaconPayloadHeader);
    Ptr<MacPibAttributes> pibAttr = Create<MacPibAttributes>();
    pibAttr->macBeaconPayloadLength = m_beaconPayload->GetSize();
    Simulator::ScheduleNow(&LrWpanMacBase::MlmeSetRequest,
                           m_mac,
                           MacPibAttributeIdentifier::macBeaconPayloadLength,
                           pibAttr);
}

void
ZigbeeNwk::UpdateBeaconPayload()
{
    NS_LOG_FUNCTION(this);

    // Extract octets from m_beaconPayload and copy them into the
    // macBeaconPayload attribute
    Ptr<MacPibAttributes> pibAttr = Create<MacPibAttributes>();
    pibAttr->macBeaconPayload.resize(m_beaconPayload->GetSize());
    m_beaconPayload->CopyData(pibAttr->macBeaconPayload.data(), m_beaconPayload->GetSize());
    m_beaconPayload = nullptr;
    Simulator::ScheduleNow(&LrWpanMacBase::MlmeSetRequest,
                           m_mac,
                           MacPibAttributeIdentifier::macBeaconPayload,
                           pibAttr);
}

std::ostream&
operator<<(std::ostream& os, const NwkStatus& state)
{
    switch (state)
    {
    case NwkStatus::SUCCESS:
        os << "SUCCESS";
        break;
    case NwkStatus::FULL_CAPACITY:
        os << "FULL CAPACITY";
        break;
    case NwkStatus::ACCESS_DENIED:
        os << "ACCESS DENIED";
        break;
    case NwkStatus::COUNTER_ERROR:
        os << "COUNTER ERROR";
        break;
    case NwkStatus::IMPROPER_KEY_TYPE:
        os << "IMPROPER KEY TYPE";
        break;
    case NwkStatus::IMPROPER_SECURITY_LEVEL:
        os << "IMPROPER SECURITY LEVEL";
        break;
    case NwkStatus::UNSUPPORTED_LEGACY:
        os << "UNSUPPORTED LEGACY";
        break;
    case NwkStatus::UNSUPPORTED_SECURITY:
        os << "UNSUPPORTED SECURITY";
        break;
    case NwkStatus::BEACON_LOSS:
        os << "BEACON LOSS";
        break;
    case NwkStatus::CHANNEL_ACCESS_FAILURE:
        os << "CHANNEL ACCESS FAILURE";
        break;
    case NwkStatus::DENIED:
        os << "DENIED";
        break;
    case NwkStatus::DISABLE_TRX_FAILURE:
        os << "DISABLE TRX FAILURE";
        break;
    case NwkStatus::SECURITY_ERROR:
        os << "SECURITY ERROR";
        break;
    case NwkStatus::FRAME_TOO_LONG:
        os << "FRAME TOO LONG";
        break;
    case NwkStatus::INVALID_GTS:
        os << "INVALID GTS";
        break;
    case NwkStatus::INVALID_HANDLE:
        os << "INVALID HANDLE";
        break;
    case NwkStatus::INVALID_PARAMETER_MAC:
        os << "INVALID PARAMETER MAC";
        break;
    case NwkStatus::NO_ACK:
        os << "NO ACKNOLEDGMENT";
        break;
    case NwkStatus::NO_BEACON:
        os << "NO BEACON";
        break;
    case NwkStatus::NO_DATA:
        os << "NO DATA";
        break;
    case NwkStatus::NO_SHORT_ADDRESS:
        os << "NO SHORT ADDRESS";
        break;
    case NwkStatus::OUT_OF_CAP:
        os << "OUT OF CAP";
        break;
    case NwkStatus::PAN_ID_CONFLICT:
        os << "PAN ID CONFLICT";
        break;
    case NwkStatus::REALIGMENT:
        os << "REALIGMENT";
        break;
    case NwkStatus::TRANSACTION_EXPIRED:
        os << "TRANSACTION EXPIRED";
        break;
    case NwkStatus::TRANSACTION_OVERFLOW:
        os << "TRANSACTION OVERFLOW";
        break;
    case NwkStatus::TX_ACTIVE:
        os << "TX ACTIVE";
        break;
    case NwkStatus::UNAVAILABLE_KEY:
        os << "UNAVAILABLE KEY";
        break;
    case NwkStatus::INVALID_ADDRESS:
        os << "INVALID ADDRESS";
        break;
    case NwkStatus::ON_TIME_TOO_LONG:
        os << "ON TIME TOO LONG";
        break;
    case NwkStatus::PAST_TIME:
        os << "PAST TIME";
        break;
    case NwkStatus::TRACKING_OFF:
        os << "TRACKING OFF";
        break;
    case NwkStatus::INVALID_INDEX:
        os << "INVALID INDEX";
        break;
    case NwkStatus::READ_ONLY:
        os << "READ ONLY";
        break;
    case NwkStatus::SUPERFRAME_OVERLAP:
        os << "SUPERFRAME OVERLAP";
        break;
    case NwkStatus::INVALID_PARAMETER:
        os << "INVALID PARAMETER";
        break;
    case NwkStatus::INVALID_REQUEST:
        os << "INVALID REQUEST";
        break;
    case NwkStatus::NOT_PERMITED:
        os << "NO PERMITED";
        break;
    case NwkStatus::STARTUP_FAILURE:
        os << "STARTUP FAILURE";
        break;
    case NwkStatus::ALREADY_PRESENT:
        os << "ALREADY PRESENT";
        break;
    case NwkStatus::SYNC_FAILURE:
        os << "SYNC FAILURE";
        break;
    case NwkStatus::NEIGHBOR_TABLE_FULL:
        os << "NEIGHBOR TABLE FULL";
        break;
    case NwkStatus::UNKNOWN_DEVICE:
        os << "UNKNOWN DEVICE";
        break;
    case NwkStatus::UNSUPPORTED_ATTRIBUTE:
        os << "UNSUPPORTED ATTRIBUTE";
        break;
    case NwkStatus::NO_NETWORKS:
        os << "NO NETWORKS";
        break;
    case NwkStatus::MAX_FRM_COUNTER:
        os << "MAX FRAME COUNTER";
        break;
    case NwkStatus::NO_KEY:
        os << "NO KEY";
        break;
    case NwkStatus::BAD_CCM_OUTPUT:
        os << "BAD CCM OUTPUT";
        break;
    case NwkStatus::ROUTE_DISCOVERY_FAILED:
        os << "ROUTE DISCOVERY FAILED";
        break;
    case NwkStatus::ROUTE_ERROR:
        os << "ROUTE ERROR";
        break;
    case NwkStatus::BT_TABLE_FULL:
        os << "BT TABLE FULL";
        break;
    case NwkStatus::FRAME_NOT_BUFFERED:
        os << "FRAME NOT BUFFERED";
        break;
    case NwkStatus::INVALID_INTERFACE:
        os << "INVALID INTERFACE";
        break;
    case NwkStatus::LIMIT_REACHED:
        os << "LIMIT REACHED";
        break;
    case NwkStatus::SCAN_IN_PROGRESS:
        os << "SCAN IN PROGRESS";
        break;
    }
    return os;
}

std::ostream&
operator<<(std::ostream& os, const std::vector<uint8_t>& vec)
{
    std::copy(vec.begin(), vec.end(), std::ostream_iterator<uint16_t>(os, " "));
    return os;
}

std::ostream&
operator<<(std::ostream& os, const uint8_t& num)
{
    os << static_cast<uint16_t>(num);
    return os;
}

} // namespace zigbee
} // namespace ns3
