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

/**
 * @file
 *   This file includes definitions for Thread Link Metrics.
 */

#include "link_metrics.hpp"

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE

#include "common/code_utils.hpp"
#include "common/encoding.hpp"
#include "common/locator_getters.hpp"
#include "common/log.hpp"
#include "common/num_utils.hpp"
#include "common/numeric_limits.hpp"
#include "instance/instance.hpp"
#include "mac/mac.hpp"
#include "thread/link_metrics_tlvs.hpp"
#include "thread/neighbor_table.hpp"

namespace ot {
namespace LinkMetrics {

RegisterLogModule("LinkMetrics");

static constexpr uint8_t kQueryIdSingleProbe = 0;   // This query ID represents Single Probe.
static constexpr uint8_t kSeriesIdAllSeries  = 255; // This series ID represents all series.

// Constants for scaling Link Margin and RSSI to raw value
static constexpr uint8_t kMaxLinkMargin = 130;
static constexpr int32_t kMinRssi       = -130;
static constexpr int32_t kMaxRssi       = 0;

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE

Initiator::Initiator(Instance &aInstance)
    : InstanceLocator(aInstance)
{
}

Error Initiator::Query(const Ip6::Address &aDestination, uint8_t aSeriesId, const Metrics *aMetrics)
{
    Error     error;
    Neighbor *neighbor;
    QueryInfo info;

    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));

    info.Clear();
    info.mSeriesId = aSeriesId;

    if (aMetrics != nullptr)
    {
        info.mTypeIdCount = aMetrics->ConvertToTypeIds(info.mTypeIds);
    }

    if (aSeriesId != 0)
    {
        VerifyOrExit(info.mTypeIdCount == 0, error = kErrorInvalidArgs);
    }

    error = Get<Mle::Mle>().SendDataRequestForLinkMetricsReport(aDestination, info);

exit:
    return error;
}

Error Initiator::AppendLinkMetricsQueryTlv(Message &aMessage, const QueryInfo &aInfo)
{
    Error error = kErrorNone;
    Tlv   tlv;

    // The MLE Link Metrics Query TLV has two sub-TLVs:
    // - Query ID sub-TLV with series ID as value.
    // - Query Options sub-TLV with Type IDs as value.

    tlv.SetType(Mle::Tlv::kLinkMetricsQuery);
    tlv.SetLength(sizeof(Tlv) + sizeof(uint8_t) + ((aInfo.mTypeIdCount == 0) ? 0 : (sizeof(Tlv) + aInfo.mTypeIdCount)));

    SuccessOrExit(error = aMessage.Append(tlv));

    SuccessOrExit(error = Tlv::Append<QueryIdSubTlv>(aMessage, aInfo.mSeriesId));

    if (aInfo.mTypeIdCount != 0)
    {
        QueryOptionsSubTlv queryOptionsTlv;

        queryOptionsTlv.Init();
        queryOptionsTlv.SetLength(aInfo.mTypeIdCount);
        SuccessOrExit(error = aMessage.Append(queryOptionsTlv));
        SuccessOrExit(error = aMessage.AppendBytes(aInfo.mTypeIds, aInfo.mTypeIdCount));
    }

exit:
    return error;
}

void Initiator::HandleReport(const Message &aMessage, OffsetRange &aOffsetRange, const Ip6::Address &aAddress)
{
    Error           error     = kErrorNone;
    bool            hasStatus = false;
    bool            hasReport = false;
    Tlv::ParsedInfo tlvInfo;
    ReportSubTlv    reportTlv;
    MetricsValues   values;
    uint8_t         status;
    uint8_t         typeId;

    OT_UNUSED_VARIABLE(error);

    VerifyOrExit(mReportCallback.IsSet());

    values.Clear();

    for (; !aOffsetRange.IsEmpty(); aOffsetRange.AdvanceOffset(tlvInfo.GetSize()))
    {
        SuccessOrExit(error = tlvInfo.ParseFrom(aMessage, aOffsetRange));

        if (tlvInfo.mIsExtended)
        {
            continue;
        }

        // The report must contain either:
        // - One or more Report Sub-TLVs (in case of success), or
        // - A single Status Sub-TLV (in case of failure).

        switch (tlvInfo.mType)
        {
        case StatusSubTlv::kType:
            VerifyOrExit(!hasStatus && !hasReport, error = kErrorDrop);
            SuccessOrExit(error = Tlv::Read<StatusSubTlv>(aMessage, aOffsetRange.GetOffset(), status));
            hasStatus = true;
            break;

        case ReportSubTlv::kType:
            VerifyOrExit(!hasStatus, error = kErrorDrop);

            // Read the report sub-TLV assuming minimum length
            SuccessOrExit(error = aMessage.Read(aOffsetRange, &reportTlv, sizeof(Tlv) + ReportSubTlv::kMinLength));
            VerifyOrExit(reportTlv.IsValid(), error = kErrorParse);
            hasReport = true;

            typeId = reportTlv.GetMetricsTypeId();

            if (TypeId::IsExtended(typeId))
            {
                // Skip the sub-TLV if `E` flag is set.
                break;
            }

            if (TypeId::GetValueLength(typeId) > sizeof(uint8_t))
            {
                // If Type ID indicates metric value has 4 bytes length, we
                // read the full `reportTlv`.
                SuccessOrExit(error = aMessage.Read(aOffsetRange.GetOffset(), reportTlv));
            }

            switch (typeId)
            {
            case TypeId::kPdu:
                values.mMetrics.mPduCount = true;
                values.mPduCountValue     = reportTlv.GetMetricsValue32();
                LogDebg(" - PDU Counter: %lu (Count/Summation)", ToUlong(values.mPduCountValue));
                break;

            case TypeId::kLqi:
                values.mMetrics.mLqi = true;
                values.mLqiValue     = reportTlv.GetMetricsValue8();
                LogDebg(" - LQI: %u (Exponential Moving Average)", values.mLqiValue);
                break;

            case TypeId::kLinkMargin:
                values.mMetrics.mLinkMargin = true;
                values.mLinkMarginValue     = ScaleRawValueToLinkMargin(reportTlv.GetMetricsValue8());
                LogDebg(" - Margin: %u (dB) (Exponential Moving Average)", values.mLinkMarginValue);
                break;

            case TypeId::kRssi:
                values.mMetrics.mRssi = true;
                values.mRssiValue     = ScaleRawValueToRssi(reportTlv.GetMetricsValue8());
                LogDebg(" - RSSI: %u (dBm) (Exponential Moving Average)", values.mRssiValue);
                break;
            }

            break;
        }
    }

    VerifyOrExit(hasStatus || hasReport);

    mReportCallback.Invoke(&aAddress, hasStatus ? nullptr : &values,
                           hasStatus ? MapEnum(static_cast<Status>(status)) : MapEnum(kStatusSuccess));

exit:
    LogDebg("HandleReport, error:%s", ErrorToString(error));
}

Error Initiator::SendMgmtRequestForwardTrackingSeries(const Ip6::Address &aDestination,
                                                      uint8_t             aSeriesId,
                                                      const SeriesFlags  &aSeriesFlags,
                                                      const Metrics      *aMetrics)
{
    Error               error;
    Neighbor           *neighbor;
    uint8_t             typeIdCount = 0;
    FwdProbingRegSubTlv fwdProbingSubTlv;

    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));

    VerifyOrExit(aSeriesId > kQueryIdSingleProbe, error = kErrorInvalidArgs);

    fwdProbingSubTlv.Init();
    fwdProbingSubTlv.SetSeriesId(aSeriesId);
    fwdProbingSubTlv.SetSeriesFlagsMask(aSeriesFlags.ConvertToMask());

    if (aMetrics != nullptr)
    {
        typeIdCount = aMetrics->ConvertToTypeIds(fwdProbingSubTlv.GetTypeIds());
    }

    fwdProbingSubTlv.SetLength(sizeof(aSeriesId) + sizeof(uint8_t) + typeIdCount);

    error = Get<Mle::Mle>().SendLinkMetricsManagementRequest(aDestination, fwdProbingSubTlv);

exit:
    LogDebg("SendMgmtRequestForwardTrackingSeries, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
    return error;
}

Error Initiator::SendMgmtRequestEnhAckProbing(const Ip6::Address &aDestination,
                                              EnhAckFlags         aEnhAckFlags,
                                              const Metrics      *aMetrics)
{
    Error              error;
    Neighbor          *neighbor;
    uint8_t            typeIdCount = 0;
    EnhAckConfigSubTlv enhAckConfigSubTlv;

    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));

    if (aEnhAckFlags == kEnhAckClear)
    {
        VerifyOrExit(aMetrics == nullptr, error = kErrorInvalidArgs);
    }

    enhAckConfigSubTlv.Init();
    enhAckConfigSubTlv.SetEnhAckFlags(aEnhAckFlags);

    if (aMetrics != nullptr)
    {
        typeIdCount = aMetrics->ConvertToTypeIds(enhAckConfigSubTlv.GetTypeIds());
    }

    enhAckConfigSubTlv.SetLength(EnhAckConfigSubTlv::kMinLength + typeIdCount);

    error = Get<Mle::Mle>().SendLinkMetricsManagementRequest(aDestination, enhAckConfigSubTlv);

    if (aMetrics != nullptr)
    {
        neighbor->SetEnhAckProbingMetrics(*aMetrics);
    }
    else
    {
        Metrics metrics;

        metrics.Clear();
        neighbor->SetEnhAckProbingMetrics(metrics);
    }

exit:
    return error;
}

Error Initiator::HandleManagementResponse(const Message &aMessage, const Ip6::Address &aAddress)
{
    Error           error = kErrorNone;
    OffsetRange     offsetRange;
    Tlv::ParsedInfo tlvInfo;
    uint8_t         status;
    bool            hasStatus = false;

    VerifyOrExit(mMgmtResponseCallback.IsSet());

    SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offsetRange));

    for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(tlvInfo.GetSize()))
    {
        SuccessOrExit(error = tlvInfo.ParseFrom(aMessage, offsetRange));

        if (tlvInfo.mIsExtended)
        {
            continue;
        }

        switch (tlvInfo.mType)
        {
        case StatusSubTlv::kType:
            VerifyOrExit(!hasStatus, error = kErrorParse);
            SuccessOrExit(error = Tlv::Read<StatusSubTlv>(aMessage, offsetRange.GetOffset(), status));
            hasStatus = true;
            break;

        default:
            break;
        }
    }

    VerifyOrExit(hasStatus, error = kErrorParse);

    mMgmtResponseCallback.Invoke(&aAddress, MapEnum(static_cast<Status>(status)));

exit:
    return error;
}

Error Initiator::SendLinkProbe(const Ip6::Address &aDestination, uint8_t aSeriesId, uint8_t aLength)
{
    Error     error;
    uint8_t   buf[kLinkProbeMaxLen];
    Neighbor *neighbor;

    SuccessOrExit(error = FindNeighbor(aDestination, neighbor));

    VerifyOrExit(aLength <= kLinkProbeMaxLen && aSeriesId != kQueryIdSingleProbe && aSeriesId != kSeriesIdAllSeries,
                 error = kErrorInvalidArgs);

    error = Get<Mle::Mle>().SendLinkProbe(aDestination, aSeriesId, buf, aLength);
exit:
    LogDebg("SendLinkProbe, error:%s, Series ID:%u", ErrorToString(error), aSeriesId);
    return error;
}

void Initiator::ProcessEnhAckIeData(const uint8_t *aData, uint8_t aLength, const Neighbor &aNeighbor)
{
    MetricsValues values;
    uint8_t       idx = 0;

    VerifyOrExit(mEnhAckProbingIeReportCallback.IsSet());

    values.SetMetrics(aNeighbor.GetEnhAckProbingMetrics());

    if (values.GetMetrics().mLqi && idx < aLength)
    {
        values.mLqiValue = aData[idx++];
    }
    if (values.GetMetrics().mLinkMargin && idx < aLength)
    {
        values.mLinkMarginValue = ScaleRawValueToLinkMargin(aData[idx++]);
    }
    if (values.GetMetrics().mRssi && idx < aLength)
    {
        values.mRssiValue = ScaleRawValueToRssi(aData[idx++]);
    }

    mEnhAckProbingIeReportCallback.Invoke(aNeighbor.GetRloc16(), &aNeighbor.GetExtAddress(), &values);

exit:
    return;
}

Error Initiator::FindNeighbor(const Ip6::Address &aDestination, Neighbor *&aNeighbor)
{
    Error        error = kErrorUnknownNeighbor;
    Mac::Address macAddress;

    aNeighbor = nullptr;

    VerifyOrExit(aDestination.IsLinkLocalUnicast());
    aDestination.GetIid().ConvertToMacAddress(macAddress);

    aNeighbor = Get<NeighborTable>().FindNeighbor(macAddress);
    VerifyOrExit(aNeighbor != nullptr);

    VerifyOrExit(aNeighbor->GetVersion() >= kThreadVersion1p2, error = kErrorNotCapable);
    error = kErrorNone;

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
Subject::Subject(Instance &aInstance)
    : InstanceLocator(aInstance)
{
}

Error Subject::AppendReport(Message &aMessage, const Message &aRequestMessage, Neighbor &aNeighbor)
{
    Error           error = kErrorNone;
    Tlv             tlv;
    Tlv::ParsedInfo tlvInfo;
    uint8_t         queryId;
    bool            hasQueryId = false;
    uint16_t        length;
    uint16_t        offset;
    OffsetRange     offsetRange;
    MetricsValues   values;

    values.Clear();

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Parse MLE Link Metrics Query TLV and its sub-TLVs from
    // `aRequestMessage`.

    SuccessOrExit(error =
                      Tlv::FindTlvValueOffsetRange(aRequestMessage, Mle::Tlv::Type::kLinkMetricsQuery, offsetRange));

    for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(tlvInfo.GetSize()))
    {
        SuccessOrExit(error = tlvInfo.ParseFrom(aRequestMessage, offsetRange));

        if (tlvInfo.mIsExtended)
        {
            continue;
        }

        switch (tlvInfo.mType)
        {
        case SubTlv::kQueryId:
            SuccessOrExit(error =
                              Tlv::Read<QueryIdSubTlv>(aRequestMessage, tlvInfo.mTlvOffsetRange.GetOffset(), queryId));
            hasQueryId = true;
            break;

        case SubTlv::kQueryOptions:
            SuccessOrExit(error =
                              ReadTypeIdsFromMessage(aRequestMessage, tlvInfo.mValueOffsetRange, values.GetMetrics()));
            break;

        default:
            break;
        }
    }

    VerifyOrExit(hasQueryId, error = kErrorParse);

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Append MLE Link Metrics Report TLV and its sub-TLVs to
    // `aMessage`.

    offset = aMessage.GetLength();
    tlv.SetType(Mle::Tlv::kLinkMetricsReport);
    SuccessOrExit(error = aMessage.Append(tlv));

    if (queryId == kQueryIdSingleProbe)
    {
        values.mPduCountValue   = aRequestMessage.GetPsduCount();
        values.mLqiValue        = aRequestMessage.GetAverageLqi();
        values.mLinkMarginValue = Get<Mac::Mac>().ComputeLinkMargin(aRequestMessage.GetAverageRss());
        values.mRssiValue       = aRequestMessage.GetAverageRss();
        SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, values));
    }
    else
    {
        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(queryId);

        if (seriesInfo == nullptr)
        {
            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusSeriesIdNotRecognized));
        }
        else if (seriesInfo->GetPduCount() == 0)
        {
            SuccessOrExit(error = Tlv::Append<StatusSubTlv>(aMessage, kStatusNoMatchingFramesReceived));
        }
        else
        {
            values.SetMetrics(seriesInfo->GetLinkMetrics());
            values.mPduCountValue   = seriesInfo->GetPduCount();
            values.mLqiValue        = seriesInfo->GetAverageLqi();
            values.mLinkMarginValue = Get<Mac::Mac>().ComputeLinkMargin(seriesInfo->GetAverageRss());
            values.mRssiValue       = seriesInfo->GetAverageRss();
            SuccessOrExit(error = AppendReportSubTlvToMessage(aMessage, values));
        }
    }

    // Update the TLV length in message.
    length = aMessage.GetLength() - offset - sizeof(Tlv);
    tlv.SetLength(static_cast<uint8_t>(length));
    aMessage.Write(offset, tlv);

exit:
    LogDebg("AppendReport, error:%s", ErrorToString(error));
    return error;
}

Error Subject::HandleManagementRequest(const Message &aMessage, Neighbor &aNeighbor, Status &aStatus)
{
    Error               error = kErrorNone;
    OffsetRange         offsetRange;
    Tlv::ParsedInfo     tlvInfo;
    FwdProbingRegSubTlv fwdProbingSubTlv;
    EnhAckConfigSubTlv  enhAckConfigSubTlv;
    Metrics             metrics;

    SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aMessage, Mle::Tlv::Type::kLinkMetricsManagement, offsetRange));

    // Set sub-TLV lengths to zero to indicate that we have
    // not yet seen them in the message.
    fwdProbingSubTlv.SetLength(0);
    enhAckConfigSubTlv.SetLength(0);

    for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(tlvInfo.GetSize()))
    {
        uint16_t    minTlvSize;
        Tlv        *subTlv;
        OffsetRange tlvOffsetRange;

        SuccessOrExit(error = tlvInfo.ParseFrom(aMessage, offsetRange));

        if (tlvInfo.mIsExtended)
        {
            continue;
        }

        tlvOffsetRange = tlvInfo.mTlvOffsetRange;

        switch (tlvInfo.mType)
        {
        case SubTlv::kFwdProbingReg:
            subTlv     = &fwdProbingSubTlv;
            minTlvSize = sizeof(Tlv) + FwdProbingRegSubTlv::kMinLength;
            break;

        case SubTlv::kEnhAckConfig:
            subTlv     = &enhAckConfigSubTlv;
            minTlvSize = sizeof(Tlv) + EnhAckConfigSubTlv::kMinLength;
            break;

        default:
            continue;
        }

        // Ensure message contains only one sub-TLV.
        VerifyOrExit(fwdProbingSubTlv.GetLength() == 0, error = kErrorParse);
        VerifyOrExit(enhAckConfigSubTlv.GetLength() == 0, error = kErrorParse);

        VerifyOrExit(tlvInfo.GetSize() >= minTlvSize, error = kErrorParse);

        // Read `subTlv` with its `minTlvSize`, followed by the Type IDs.
        SuccessOrExit(error = aMessage.Read(tlvOffsetRange, subTlv, minTlvSize));

        tlvOffsetRange.AdvanceOffset(minTlvSize);
        SuccessOrExit(error = ReadTypeIdsFromMessage(aMessage, tlvOffsetRange, metrics));
    }

    if (fwdProbingSubTlv.GetLength() != 0)
    {
        aStatus = ConfigureForwardTrackingSeries(fwdProbingSubTlv.GetSeriesId(), fwdProbingSubTlv.GetSeriesFlagsMask(),
                                                 metrics, aNeighbor);
    }

    if (enhAckConfigSubTlv.GetLength() != 0)
    {
        aStatus = ConfigureEnhAckProbing(enhAckConfigSubTlv.GetEnhAckFlags(), metrics, aNeighbor);
    }

exit:
    return error;
}

Error Subject::HandleLinkProbe(const Message &aMessage, uint8_t &aSeriesId)
{
    Error       error = kErrorNone;
    OffsetRange offsetRange;

    SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aMessage, Mle::Tlv::Type::kLinkProbe, offsetRange));
    error = aMessage.Read(offsetRange, aSeriesId);

exit:
    return error;
}

Error Subject::AppendReportSubTlvToMessage(Message &aMessage, const MetricsValues &aValues)
{
    Error        error = kErrorNone;
    ReportSubTlv reportTlv;

    reportTlv.Init();

    if (aValues.mMetrics.mPduCount)
    {
        reportTlv.SetMetricsTypeId(TypeId::kPdu);
        reportTlv.SetMetricsValue32(aValues.mPduCountValue);
        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
    }

    if (aValues.mMetrics.mLqi)
    {
        reportTlv.SetMetricsTypeId(TypeId::kLqi);
        reportTlv.SetMetricsValue8(aValues.mLqiValue);
        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
    }

    if (aValues.mMetrics.mLinkMargin)
    {
        reportTlv.SetMetricsTypeId(TypeId::kLinkMargin);
        reportTlv.SetMetricsValue8(ScaleLinkMarginToRawValue(aValues.mLinkMarginValue));
        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
    }

    if (aValues.mMetrics.mRssi)
    {
        reportTlv.SetMetricsTypeId(TypeId::kRssi);
        reportTlv.SetMetricsValue8(ScaleRssiToRawValue(aValues.mRssiValue));
        SuccessOrExit(error = reportTlv.AppendTo(aMessage));
    }

exit:
    return error;
}

void Subject::Free(SeriesInfo &aSeriesInfo) { mSeriesInfoPool.Free(aSeriesInfo); }

Error Subject::ReadTypeIdsFromMessage(const Message &aMessage, const OffsetRange &aOffsetRange, Metrics &aMetrics)
{
    Error       error       = kErrorNone;
    OffsetRange offsetRange = aOffsetRange;

    aMetrics.Clear();

    while (!offsetRange.IsEmpty())
    {
        uint8_t typeId;

        SuccessOrExit(aMessage.Read(offsetRange, typeId));

        switch (typeId)
        {
        case TypeId::kPdu:
            VerifyOrExit(!aMetrics.mPduCount, error = kErrorParse);
            aMetrics.mPduCount = true;
            break;

        case TypeId::kLqi:
            VerifyOrExit(!aMetrics.mLqi, error = kErrorParse);
            aMetrics.mLqi = true;
            break;

        case TypeId::kLinkMargin:
            VerifyOrExit(!aMetrics.mLinkMargin, error = kErrorParse);
            aMetrics.mLinkMargin = true;
            break;

        case TypeId::kRssi:
            VerifyOrExit(!aMetrics.mRssi, error = kErrorParse);
            aMetrics.mRssi = true;
            break;

        default:
            if (TypeId::IsExtended(typeId))
            {
                offsetRange.AdvanceOffset(sizeof(uint8_t)); // Skip the additional second byte.
            }
            else
            {
                aMetrics.mReserved = true;
            }
            break;
        }

        offsetRange.AdvanceOffset(sizeof(uint8_t));
    }

exit:
    return error;
}

Status Subject::ConfigureForwardTrackingSeries(uint8_t        aSeriesId,
                                               uint8_t        aSeriesFlagsMask,
                                               const Metrics &aMetrics,
                                               Neighbor      &aNeighbor)
{
    Status status = kStatusSuccess;

    VerifyOrExit(0 < aSeriesId, status = kStatusOtherError);

    if (aSeriesFlagsMask == 0) // Remove the series
    {
        if (aSeriesId == kSeriesIdAllSeries) // Remove all
        {
            aNeighbor.RemoveAllForwardTrackingSeriesInfo();
        }
        else
        {
            SeriesInfo *seriesInfo = aNeighbor.RemoveForwardTrackingSeriesInfo(aSeriesId);
            VerifyOrExit(seriesInfo != nullptr, status = kStatusSeriesIdNotRecognized);
            mSeriesInfoPool.Free(*seriesInfo);
        }
    }
    else // Add a new series
    {
        SeriesInfo *seriesInfo = aNeighbor.GetForwardTrackingSeriesInfo(aSeriesId);
        VerifyOrExit(seriesInfo == nullptr, status = kStatusSeriesIdAlreadyRegistered);
        seriesInfo = mSeriesInfoPool.Allocate();
        VerifyOrExit(seriesInfo != nullptr, status = kStatusCannotSupportNewSeries);

        seriesInfo->Init(aSeriesId, aSeriesFlagsMask, aMetrics);

        aNeighbor.AddForwardTrackingSeriesInfo(*seriesInfo);
    }

exit:
    return status;
}

Status Subject::ConfigureEnhAckProbing(uint8_t aEnhAckFlags, const Metrics &aMetrics, Neighbor &aNeighbor)
{
    Status status = kStatusSuccess;
    Error  error  = kErrorNone;

    VerifyOrExit(!aMetrics.mReserved, status = kStatusOtherError);

    if (aEnhAckFlags == kEnhAckRegister)
    {
        VerifyOrExit(!aMetrics.mPduCount, status = kStatusOtherError);
        VerifyOrExit(aMetrics.mLqi || aMetrics.mLinkMargin || aMetrics.mRssi, status = kStatusOtherError);
        VerifyOrExit(!(aMetrics.mLqi && aMetrics.mLinkMargin && aMetrics.mRssi), status = kStatusOtherError);

        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
    }
    else if (aEnhAckFlags == kEnhAckClear)
    {
        VerifyOrExit(!aMetrics.mLqi && !aMetrics.mLinkMargin && !aMetrics.mRssi, status = kStatusOtherError);
        error = Get<Radio>().ConfigureEnhAckProbing(aMetrics, aNeighbor.GetRloc16(), aNeighbor.GetExtAddress());
    }
    else
    {
        status = kStatusOtherError;
    }

    VerifyOrExit(error == kErrorNone, status = kStatusOtherError);

exit:
    return status;
}

#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE

uint8_t ScaleLinkMarginToRawValue(uint8_t aLinkMargin)
{
    // Linearly scale Link Margin from [0, 130] to [0, 255].
    // `kMaxLinkMargin = 130`.

    uint16_t value;

    value = Min(aLinkMargin, kMaxLinkMargin);
    value = value * NumericLimits<uint8_t>::kMax;
    value = DivideAndRoundToClosest<uint16_t>(value, kMaxLinkMargin);

    return static_cast<uint8_t>(value);
}

uint8_t ScaleRawValueToLinkMargin(uint8_t aRawValue)
{
    // Scale back raw value of [0, 255] to Link Margin from [0, 130].

    uint16_t value = aRawValue;

    value = value * kMaxLinkMargin;
    value = DivideAndRoundToClosest<uint16_t>(value, NumericLimits<uint8_t>::kMax);
    return static_cast<uint8_t>(value);
}

uint8_t ScaleRssiToRawValue(int8_t aRssi)
{
    // Linearly scale RSSI from [-130, 0] to [0, 255].
    // `kMinRssi = -130`, `kMaxRssi = 0`.

    int32_t value = aRssi;

    value = Clamp(value, kMinRssi, kMaxRssi) - kMinRssi;
    value = value * NumericLimits<uint8_t>::kMax;
    value = DivideAndRoundToClosest<int32_t>(value, kMaxRssi - kMinRssi);

    return static_cast<uint8_t>(value);
}

int8_t ScaleRawValueToRssi(uint8_t aRawValue)
{
    int32_t value = aRawValue;

    value = value * (kMaxRssi - kMinRssi);
    value = DivideAndRoundToClosest<int32_t>(value, NumericLimits<uint8_t>::kMax);
    value += kMinRssi;

    return ClampToInt8(value);
}

} // namespace LinkMetrics
} // namespace ot

#endif // OPENTHREAD_CONFIG_MLE_LINK_METRICS_INITIATOR_ENABLE || OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
