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

/**
 * @file
 *   This file implements 6LoWPAN header compression.
 */

#include "lowpan.hpp"

#include "common/code_utils.hpp"
#include "common/debug.hpp"
#include "common/encoding.hpp"
#include "common/locator_getters.hpp"
#include "common/numeric_limits.hpp"
#include "instance/instance.hpp"
#include "net/ip6.hpp"
#include "net/udp6.hpp"
#include "thread/network_data_leader.hpp"
#include "thread/thread_netif.hpp"

namespace ot {
namespace Lowpan {

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

void Lowpan::FindContextForId(uint8_t aContextId, Context &aContext) const
{
    if (Get<NetworkData::Leader>().GetContext(aContextId, aContext) != kErrorNone)
    {
        aContext.Clear();
    }
}

void Lowpan::FindContextToCompressAddress(const Ip6::Address &aIp6Address, Context &aContext) const
{
    Error error = Get<NetworkData::Leader>().GetContext(aIp6Address, aContext);

    if ((error != kErrorNone) || !aContext.mCompressFlag)
    {
        aContext.Clear();
    }
}

Error Lowpan::ComputeIid(const Mac::Address &aMacAddr, const Context &aContext, Ip6::InterfaceIdentifier &aIid)
{
    Error error = kErrorNone;

    switch (aMacAddr.GetType())
    {
    case Mac::Address::kTypeShort:
        aIid.SetToLocator(aMacAddr.GetShort());
        break;

    case Mac::Address::kTypeExtended:
        aIid.SetFromExtAddress(aMacAddr.GetExtended());
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    aIid.ApplyPrefix(aContext.mPrefix);

exit:
    return error;
}

Error Lowpan::CompressSourceIid(const Mac::Address &aMacAddr,
                                const Ip6::Address &aIpAddr,
                                const Context      &aContext,
                                uint16_t           &aHcCtl,
                                FrameBuilder       &aFrameBuilder)
{
    Error                    error = kErrorNone;
    Ip6::InterfaceIdentifier iid;

    IgnoreError(ComputeIid(aMacAddr, aContext, iid));

    if (iid == aIpAddr.GetIid())
    {
        aHcCtl |= kHcSrcAddrMode3;
    }
    else if (aIpAddr.GetIid().IsLocator())
    {
        aHcCtl |= kHcSrcAddrMode2;
        error = aFrameBuilder.AppendBigEndianUint16(aIpAddr.GetIid().GetLocator());
    }
    else
    {
        aHcCtl |= kHcSrcAddrMode1;
        error = aFrameBuilder.Append(aIpAddr.GetIid());
    }

    return error;
}

Error Lowpan::CompressDestinationIid(const Mac::Address &aMacAddr,
                                     const Ip6::Address &aIpAddr,
                                     const Context      &aContext,
                                     uint16_t           &aHcCtl,
                                     FrameBuilder       &aFrameBuilder)
{
    Error                    error = kErrorNone;
    Ip6::InterfaceIdentifier iid;

    IgnoreError(ComputeIid(aMacAddr, aContext, iid));

    if (iid == aIpAddr.GetIid())
    {
        aHcCtl |= kHcDstAddrMode3;
    }
    else if (aIpAddr.GetIid().IsLocator())
    {
        aHcCtl |= kHcDstAddrMode2;
        error = aFrameBuilder.AppendBigEndianUint16(aIpAddr.GetIid().GetLocator());
    }
    else
    {
        aHcCtl |= kHcDstAddrMode1;
        error = aFrameBuilder.Append(aIpAddr.GetIid());
    }

    return error;
}

Error Lowpan::CompressMulticast(const Ip6::Address &aIpAddr, uint16_t &aHcCtl, FrameBuilder &aFrameBuilder)
{
    Error   error = kErrorNone;
    Context multicastContext;

    aHcCtl |= kHcMulticast;

    for (unsigned int i = 2; i < sizeof(Ip6::Address); i++)
    {
        if (aIpAddr.mFields.m8[i])
        {
            // Check if multicast address can be compressed to 8-bits (ff02::00xx)
            if (aIpAddr.mFields.m8[1] == 0x02 && i >= 15)
            {
                aHcCtl |= kHcDstAddrMode3;
                SuccessOrExit(error = aFrameBuilder.AppendUint8(aIpAddr.mFields.m8[15]));
            }
            // Check if multicast address can be compressed to 32-bits (ffxx::00xx:xxxx)
            else if (i >= 13)
            {
                aHcCtl |= kHcDstAddrMode2;
                SuccessOrExit(error = aFrameBuilder.AppendUint8(aIpAddr.mFields.m8[1]));
                SuccessOrExit(error = aFrameBuilder.AppendBytes(aIpAddr.mFields.m8 + 13, 3));
            }
            // Check if multicast address can be compressed to 48-bits (ffxx::00xx:xxxx:xxxx)
            else if (i >= 11)
            {
                aHcCtl |= kHcDstAddrMode1;
                SuccessOrExit(error = aFrameBuilder.AppendUint8(aIpAddr.mFields.m8[1]));
                SuccessOrExit(error = aFrameBuilder.AppendBytes(aIpAddr.mFields.m8 + 11, 5));
            }
            else
            {
                // Check if multicast address can be compressed using Context ID 0 (mesh local prefix)
                FindContextForId(0, multicastContext);

                if (multicastContext.mPrefix.GetLength() == aIpAddr.mFields.m8[3] &&
                    memcmp(multicastContext.mPrefix.GetBytes(), aIpAddr.mFields.m8 + 4, 8) == 0)
                {
                    aHcCtl |= kHcDstAddrContext | kHcDstAddrMode0;
                    SuccessOrExit(error = aFrameBuilder.AppendBytes(aIpAddr.mFields.m8 + 1, 2));
                    SuccessOrExit(error = aFrameBuilder.AppendBytes(aIpAddr.mFields.m8 + 12, 4));
                }
                else
                {
                    SuccessOrExit(error = aFrameBuilder.Append(aIpAddr));
                }
            }

            break;
        }
    }

exit:
    return error;
}

Error Lowpan::Compress(Message &aMessage, const Mac::Addresses &aMacAddrs, FrameBuilder &aFrameBuilder)
{
    Error   error       = kErrorNone;
    uint8_t headerDepth = 0xff;

    while (headerDepth > 0)
    {
        FrameBuilder frameBuilder = aFrameBuilder;

        error = Compress(aMessage, aMacAddrs, aFrameBuilder, headerDepth);

        // We exit if `Compress()` is successful. Otherwise we reset
        // the `aFrameBuidler` to its earlier state (remove all
        // appended content from the failed `Compress()` call) and
        // try again with a different `headerDepth`.

        VerifyOrExit(error != kErrorNone);
        aFrameBuilder = frameBuilder;
    }

exit:
    return error;
}

Error Lowpan::Compress(Message              &aMessage,
                       const Mac::Addresses &aMacAddrs,
                       FrameBuilder         &aFrameBuilder,
                       uint8_t              &aHeaderDepth)
{
    Error       error       = kErrorNone;
    uint16_t    startOffset = aMessage.GetOffset();
    uint16_t    hcCtl       = kHcDispatch;
    uint16_t    hcCtlOffset = 0;
    Ip6::Header ip6Header;
    uint8_t    *ip6HeaderBytes = reinterpret_cast<uint8_t *>(&ip6Header);
    Context     srcContext, dstContext;
    uint8_t     nextHeader;
    uint8_t     ecn;
    uint8_t     dscp;
    uint8_t     headerDepth    = 0;
    uint8_t     headerMaxDepth = aHeaderDepth;

    SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), ip6Header));

    FindContextToCompressAddress(ip6Header.GetSource(), srcContext);
    FindContextToCompressAddress(ip6Header.GetDestination(), dstContext);

    // Lowpan HC Control Bits
    hcCtlOffset = aFrameBuilder.GetLength();
    SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(hcCtl));

    // Context Identifier
    if (srcContext.mContextId != 0 || dstContext.mContextId != 0)
    {
        hcCtl |= kHcContextId;
        SuccessOrExit(error = aFrameBuilder.AppendUint8(((srcContext.mContextId << 4) | dstContext.mContextId) & 0xff));
    }

    dscp = ((ip6HeaderBytes[0] << 2) & 0x3c) | (ip6HeaderBytes[1] >> 6);
    ecn  = (ip6HeaderBytes[1] << 2) & 0xc0;

    // Flow Label
    if (((ip6HeaderBytes[1] & 0x0f) == 0) && ((ip6HeaderBytes[2]) == 0) && ((ip6HeaderBytes[3]) == 0))
    {
        if (dscp == 0 && ecn == 0)
        {
            // Elide Flow Label and Traffic Class.
            hcCtl |= kHcTrafficClass | kHcFlowLabel;
        }
        else
        {
            // Elide Flow Label and carry Traffic Class in-line.
            hcCtl |= kHcFlowLabel;

            SuccessOrExit(error = aFrameBuilder.AppendUint8(ecn | dscp));
        }
    }
    else if (dscp == 0)
    {
        // Carry Flow Label and ECN only with 2-bit padding.
        hcCtl |= kHcTrafficClass;

        SuccessOrExit(error = aFrameBuilder.AppendUint8(ecn | (ip6HeaderBytes[1] & 0x0f)));
        SuccessOrExit(error = aFrameBuilder.AppendBytes(ip6HeaderBytes + 2, 2));
    }
    else
    {
        // Carry Flow Label and Traffic Class in-line.
        SuccessOrExit(error = aFrameBuilder.AppendUint8(ecn | dscp));
        SuccessOrExit(error = aFrameBuilder.AppendUint8(ip6HeaderBytes[1] & 0x0f));
        SuccessOrExit(error = aFrameBuilder.AppendBytes(ip6HeaderBytes + 2, 2));
    }

    // Next Header
    switch (ip6Header.GetNextHeader())
    {
    case Ip6::kProtoHopOpts:
    case Ip6::kProtoUdp:
    case Ip6::kProtoIp6:
        if (headerDepth + 1 < headerMaxDepth)
        {
            hcCtl |= kHcNextHeader;
            break;
        }
        OT_FALL_THROUGH;

    default:
        SuccessOrExit(error = aFrameBuilder.AppendUint8(static_cast<uint8_t>(ip6Header.GetNextHeader())));
        break;
    }

    // Hop Limit
    switch (ip6Header.GetHopLimit())
    {
    case 1:
        hcCtl |= kHcHopLimit1;
        break;

    case 64:
        hcCtl |= kHcHopLimit64;
        break;

    case 255:
        hcCtl |= kHcHopLimit255;
        break;

    default:
        SuccessOrExit(error = aFrameBuilder.AppendUint8(ip6Header.GetHopLimit()));
        break;
    }

    // Source Address
    if (ip6Header.GetSource().IsUnspecified())
    {
        hcCtl |= kHcSrcAddrContext;
    }
    else if (ip6Header.GetSource().IsLinkLocal())
    {
        SuccessOrExit(
            error = CompressSourceIid(aMacAddrs.mSource, ip6Header.GetSource(), srcContext, hcCtl, aFrameBuilder));
    }
    else if (srcContext.mIsValid)
    {
        hcCtl |= kHcSrcAddrContext;
        SuccessOrExit(
            error = CompressSourceIid(aMacAddrs.mSource, ip6Header.GetSource(), srcContext, hcCtl, aFrameBuilder));
    }
    else
    {
        SuccessOrExit(error = aFrameBuilder.Append(ip6Header.GetSource()));
    }

    // Destination Address
    if (ip6Header.GetDestination().IsMulticast())
    {
        SuccessOrExit(error = CompressMulticast(ip6Header.GetDestination(), hcCtl, aFrameBuilder));
    }
    else if (ip6Header.GetDestination().IsLinkLocal())
    {
        SuccessOrExit(error = CompressDestinationIid(aMacAddrs.mDestination, ip6Header.GetDestination(), dstContext,
                                                     hcCtl, aFrameBuilder));
    }
    else if (dstContext.mIsValid)
    {
        hcCtl |= kHcDstAddrContext;
        SuccessOrExit(error = CompressDestinationIid(aMacAddrs.mDestination, ip6Header.GetDestination(), dstContext,
                                                     hcCtl, aFrameBuilder));
    }
    else
    {
        SuccessOrExit(error = aFrameBuilder.Append(ip6Header.GetDestination()));
    }

    headerDepth++;

    aMessage.MoveOffset(sizeof(ip6Header));

    nextHeader = static_cast<uint8_t>(ip6Header.GetNextHeader());

    while (headerDepth < headerMaxDepth)
    {
        switch (nextHeader)
        {
        case Ip6::kProtoHopOpts:
            SuccessOrExit(error = CompressExtensionHeader(aMessage, aFrameBuilder, nextHeader));
            break;

        case Ip6::kProtoUdp:
            error = CompressUdp(aMessage, aFrameBuilder);
            ExitNow();

        case Ip6::kProtoIp6:
            // For IP-in-IP the NH bit of the LOWPAN_NHC encoding MUST be set to zero.
            SuccessOrExit(error = aFrameBuilder.AppendUint8(kExtHdrDispatch | kExtHdrEidIp6));

            error = Compress(aMessage, aMacAddrs, aFrameBuilder);

            OT_FALL_THROUGH;

        default:
            ExitNow();
        }

        headerDepth++;
    }

exit:
    aHeaderDepth = headerDepth;

    if (error == kErrorNone)
    {
        aFrameBuilder.Write<uint16_t>(hcCtlOffset, BigEndian::HostSwap16(hcCtl));
    }
    else
    {
        aMessage.SetOffset(startOffset);
    }

    return error;
}

Error Lowpan::CompressExtensionHeader(Message &aMessage, FrameBuilder &aFrameBuilder, uint8_t &aNextHeader)
{
    Error                error       = kErrorNone;
    uint16_t             startOffset = aMessage.GetOffset();
    Ip6::ExtensionHeader extHeader;
    uint16_t             len;
    uint16_t             padLength = 0;
    uint8_t              tmpByte;

    SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), extHeader));
    aMessage.MoveOffset(sizeof(extHeader));

    tmpByte = kExtHdrDispatch | kExtHdrEidHbh;

    switch (extHeader.GetNextHeader())
    {
    case Ip6::kProtoUdp:
    case Ip6::kProtoIp6:
        tmpByte |= kExtHdrNextHeader;
        break;

    default:
        SuccessOrExit(error = aFrameBuilder.AppendUint8(tmpByte));
        tmpByte = static_cast<uint8_t>(extHeader.GetNextHeader());
        break;
    }

    SuccessOrExit(error = aFrameBuilder.AppendUint8(tmpByte));

    len = extHeader.GetSize() - sizeof(extHeader);

    // RFC 6282 does not support compressing large extension headers
    VerifyOrExit(len <= kExtHdrMaxLength, error = kErrorFailed);

    // RFC 6282 says: "IPv6 Hop-by-Hop and Destination Options Headers may use a trailing
    // Pad1 or PadN to achieve 8-octet alignment. When there is a single trailing Pad1 or PadN
    // option of 7 octets or less and the containing header is a multiple of 8 octets, the trailing
    // Pad1 or PadN option MAY be elided by the compressor."
    if (aNextHeader == Ip6::kProtoHopOpts || aNextHeader == Ip6::kProtoDstOpts)
    {
        uint16_t    offset    = aMessage.GetOffset();
        uint16_t    endOffset = offset + len;
        bool        hasOption = false;
        Ip6::Option option;

        for (; offset < endOffset; offset += option.GetSize())
        {
            SuccessOrExit(error = option.ParseFrom(aMessage, offset, endOffset));
            hasOption = true;
        }

        // Check if the last option can be compressed.
        if (hasOption && option.IsPadding())
        {
            padLength = option.GetSize();
            len -= padLength;
        }
    }

    VerifyOrExit(aMessage.GetOffset() + len + padLength <= aMessage.GetLength(), error = kErrorParse);

    aNextHeader = static_cast<uint8_t>(extHeader.GetNextHeader());

    SuccessOrExit(error = aFrameBuilder.AppendUint8(static_cast<uint8_t>(len)));
    SuccessOrExit(error = aFrameBuilder.AppendBytesFromMessage(aMessage, aMessage.GetOffset(), len));
    aMessage.MoveOffset(len + padLength);

exit:
    if (error != kErrorNone)
    {
        aMessage.SetOffset(startOffset);
    }

    return error;
}

Error Lowpan::CompressUdp(Message &aMessage, FrameBuilder &aFrameBuilder)
{
    Error            error       = kErrorNone;
    uint16_t         startOffset = aMessage.GetOffset();
    Ip6::Udp::Header udpHeader;
    uint16_t         source;
    uint16_t         destination;

    SuccessOrExit(error = aMessage.Read(aMessage.GetOffset(), udpHeader));

    source      = udpHeader.GetSourcePort();
    destination = udpHeader.GetDestinationPort();

    if ((source & 0xfff0) == 0xf0b0 && (destination & 0xfff0) == 0xf0b0)
    {
        SuccessOrExit(error = aFrameBuilder.AppendUint8(kUdpDispatch | 3));
        SuccessOrExit(error = aFrameBuilder.AppendUint8((((source & 0xf) << 4) | (destination & 0xf)) & 0xff));
    }
    else if ((source & 0xff00) == 0xf000)
    {
        SuccessOrExit(error = aFrameBuilder.AppendUint8(kUdpDispatch | 2));
        SuccessOrExit(error = aFrameBuilder.AppendUint8(source & 0xff));
        SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(destination));
    }
    else if ((destination & 0xff00) == 0xf000)
    {
        SuccessOrExit(error = aFrameBuilder.AppendUint8(kUdpDispatch | 1));
        SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(source));
        SuccessOrExit(error = aFrameBuilder.AppendUint8(destination & 0xff));
    }
    else
    {
        SuccessOrExit(error = aFrameBuilder.AppendUint8(kUdpDispatch));
        SuccessOrExit(error = aFrameBuilder.AppendBytes(&udpHeader, Ip6::Udp::Header::kLengthFieldOffset));
    }

    SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(udpHeader.GetChecksum()));

    aMessage.MoveOffset(sizeof(udpHeader));

exit:
    if (error != kErrorNone)
    {
        aMessage.SetOffset(startOffset);
    }

    return error;
}

Error Lowpan::DispatchToNextHeader(uint8_t aDispatch, uint8_t &aNextHeader)
{
    Error error = kErrorNone;

    if ((aDispatch & kExtHdrDispatchMask) == kExtHdrDispatch)
    {
        switch (aDispatch & kExtHdrEidMask)
        {
        case kExtHdrEidHbh:
            aNextHeader = Ip6::kProtoHopOpts;
            ExitNow();

        case kExtHdrEidRouting:
            aNextHeader = Ip6::kProtoRouting;
            ExitNow();

        case kExtHdrEidFragment:
            aNextHeader = Ip6::kProtoFragment;
            ExitNow();

        case kExtHdrEidDst:
            aNextHeader = Ip6::kProtoDstOpts;
            ExitNow();

        case kExtHdrEidIp6:
            aNextHeader = Ip6::kProtoIp6;
            ExitNow();
        }
    }
    else if ((aDispatch & kUdpDispatchMask) == kUdpDispatch)
    {
        aNextHeader = Ip6::kProtoUdp;
        ExitNow();
    }

    error = kErrorParse;

exit:
    return error;
}

Error Lowpan::DecompressBaseHeader(Ip6::Header          &aIp6Header,
                                   bool                 &aCompressedNextHeader,
                                   const Mac::Addresses &aMacAddrs,
                                   FrameData            &aFrameData)
{
    Error    error = kErrorParse;
    uint16_t hcCtl;
    uint8_t  byte;
    uint8_t  srcContextId = 0;
    uint8_t  dstContextId = 0;
    Context  srcContext;
    Context  dstContext;
    uint8_t  nextHeader;

    SuccessOrExit(aFrameData.ReadBigEndianUint16(hcCtl));

    // check Dispatch bits
    VerifyOrExit((hcCtl & kHcDispatchMask) == kHcDispatch);

    // Context Identifier
    if ((hcCtl & kHcContextId) != 0)
    {
        SuccessOrExit(aFrameData.ReadUint8(byte));

        srcContextId = (byte >> 4);
        dstContextId = (byte & 0xf);
    }

    FindContextForId(srcContextId, srcContext);
    FindContextForId(dstContextId, dstContext);

    aIp6Header.Clear();
    aIp6Header.InitVersionTrafficClassFlow();

    // Traffic Class and Flow Label
    if ((hcCtl & kHcTrafficFlowMask) != kHcTrafficFlow)
    {
        uint8_t *ip6HeaderBytes = reinterpret_cast<uint8_t *>(&aIp6Header);

        VerifyOrExit(aFrameData.GetLength() > 0);

        ip6HeaderBytes[1] |= (aFrameData.GetBytes()[0] & 0xc0) >> 2;

        if ((hcCtl & kHcTrafficClass) == 0)
        {
            IgnoreError(aFrameData.ReadUint8(byte));
            ip6HeaderBytes[0] |= (byte >> 2) & 0x0f;
            ip6HeaderBytes[1] |= (byte << 6) & 0xc0;
        }

        if ((hcCtl & kHcFlowLabel) == 0)
        {
            VerifyOrExit(aFrameData.GetLength() >= 3);
            ip6HeaderBytes[1] |= aFrameData.GetBytes()[0] & 0x0f;
            ip6HeaderBytes[2] |= aFrameData.GetBytes()[1];
            ip6HeaderBytes[3] |= aFrameData.GetBytes()[2];
            aFrameData.SkipOver(3);
        }
    }

    // Next Header
    if ((hcCtl & kHcNextHeader) == 0)
    {
        SuccessOrExit(aFrameData.ReadUint8(byte));

        aIp6Header.SetNextHeader(byte);
        aCompressedNextHeader = false;
    }
    else
    {
        aCompressedNextHeader = true;
    }

    // Hop Limit
    switch (hcCtl & kHcHopLimitMask)
    {
    case kHcHopLimit1:
        aIp6Header.SetHopLimit(1);
        break;

    case kHcHopLimit64:
        aIp6Header.SetHopLimit(64);
        break;

    case kHcHopLimit255:
        aIp6Header.SetHopLimit(255);
        break;

    default:
        SuccessOrExit(aFrameData.ReadUint8(byte));
        aIp6Header.SetHopLimit(byte);
        break;
    }

    // Source Address
    switch (hcCtl & kHcSrcAddrModeMask)
    {
    case kHcSrcAddrMode0:
        if ((hcCtl & kHcSrcAddrContext) == 0)
        {
            SuccessOrExit(aFrameData.Read(aIp6Header.GetSource()));
        }

        break;

    case kHcSrcAddrMode1:
        SuccessOrExit(aFrameData.Read(aIp6Header.GetSource().GetIid()));
        break;

    case kHcSrcAddrMode2:
        aIp6Header.GetSource().mFields.m8[11] = 0xff;
        aIp6Header.GetSource().mFields.m8[12] = 0xfe;
        SuccessOrExit(aFrameData.ReadBytes(aIp6Header.GetSource().mFields.m8 + 14, 2));
        break;

    case kHcSrcAddrMode3:
        IgnoreError(ComputeIid(aMacAddrs.mSource, srcContext, aIp6Header.GetSource().GetIid()));
        break;
    }

    if ((hcCtl & kHcSrcAddrModeMask) != kHcSrcAddrMode0)
    {
        if ((hcCtl & kHcSrcAddrContext) == 0)
        {
            aIp6Header.GetSource().mFields.m16[0] = BigEndian::HostSwap16(0xfe80);
        }
        else
        {
            VerifyOrExit(srcContext.mIsValid);
            aIp6Header.GetSource().SetPrefix(srcContext.mPrefix);
        }
    }

    if ((hcCtl & kHcMulticast) == 0)
    {
        // Unicast Destination Address

        switch (hcCtl & kHcDstAddrModeMask)
        {
        case kHcDstAddrMode0:
            VerifyOrExit((hcCtl & kHcDstAddrContext) == 0);
            SuccessOrExit(aFrameData.Read(aIp6Header.GetDestination()));
            break;

        case kHcDstAddrMode1:
            SuccessOrExit(aFrameData.Read(aIp6Header.GetDestination().GetIid()));
            break;

        case kHcDstAddrMode2:
            aIp6Header.GetDestination().mFields.m8[11] = 0xff;
            aIp6Header.GetDestination().mFields.m8[12] = 0xfe;
            SuccessOrExit(aFrameData.ReadBytes(aIp6Header.GetDestination().mFields.m8 + 14, 2));
            break;

        case kHcDstAddrMode3:
            SuccessOrExit(ComputeIid(aMacAddrs.mDestination, dstContext, aIp6Header.GetDestination().GetIid()));
            break;
        }

        if ((hcCtl & kHcDstAddrContext) == 0)
        {
            if ((hcCtl & kHcDstAddrModeMask) != 0)
            {
                aIp6Header.GetDestination().mFields.m16[0] = BigEndian::HostSwap16(0xfe80);
            }
        }
        else
        {
            VerifyOrExit(dstContext.mIsValid);
            aIp6Header.GetDestination().SetPrefix(dstContext.mPrefix);
        }
    }
    else
    {
        // Multicast Destination Address

        aIp6Header.GetDestination().mFields.m8[0] = 0xff;

        if ((hcCtl & kHcDstAddrContext) == 0)
        {
            switch (hcCtl & kHcDstAddrModeMask)
            {
            case kHcDstAddrMode0:
                SuccessOrExit(aFrameData.Read(aIp6Header.GetDestination()));
                break;

            case kHcDstAddrMode1:
                SuccessOrExit(aFrameData.ReadUint8(aIp6Header.GetDestination().mFields.m8[1]));
                SuccessOrExit(aFrameData.ReadBytes(aIp6Header.GetDestination().mFields.m8 + 11, 5));
                break;

            case kHcDstAddrMode2:
                SuccessOrExit(aFrameData.ReadUint8(aIp6Header.GetDestination().mFields.m8[1]));
                SuccessOrExit(aFrameData.ReadBytes(aIp6Header.GetDestination().mFields.m8 + 13, 3));
                break;

            case kHcDstAddrMode3:
                aIp6Header.GetDestination().mFields.m8[1] = 0x02;
                SuccessOrExit(aFrameData.ReadUint8(aIp6Header.GetDestination().mFields.m8[15]));
                break;
            }
        }
        else
        {
            switch (hcCtl & kHcDstAddrModeMask)
            {
            case 0:
                VerifyOrExit(dstContext.mIsValid);
                SuccessOrExit(aFrameData.ReadBytes(aIp6Header.GetDestination().mFields.m8 + 1, 2));
                aIp6Header.GetDestination().mFields.m8[3] = dstContext.mPrefix.GetLength();
                memcpy(aIp6Header.GetDestination().mFields.m8 + 4, dstContext.mPrefix.GetBytes(), 8);
                SuccessOrExit(aFrameData.ReadBytes(aIp6Header.GetDestination().mFields.m8 + 12, 4));
                break;

            default:
                ExitNow();
            }
        }
    }

    if ((hcCtl & kHcNextHeader) != 0)
    {
        VerifyOrExit(aFrameData.GetLength() > 0);
        SuccessOrExit(DispatchToNextHeader(*aFrameData.GetBytes(), nextHeader));
        aIp6Header.SetNextHeader(nextHeader);
    }

    error = kErrorNone;

exit:
    return error;
}

Error Lowpan::DecompressExtensionHeader(Message &aMessage, FrameData &aFrameData)
{
    Error          error = kErrorParse;
    uint8_t        hdr[2];
    uint8_t        len;
    uint8_t        ctl;
    Ip6::PadOption padOption;

    SuccessOrExit(aFrameData.ReadUint8(ctl));

    // next header
    if (ctl & kExtHdrNextHeader)
    {
        SuccessOrExit(aFrameData.ReadUint8(len));

        VerifyOrExit(aFrameData.CanRead(len + 1));
        SuccessOrExit(DispatchToNextHeader(aFrameData.GetBytes()[len], hdr[0]));
    }
    else
    {
        SuccessOrExit(aFrameData.ReadUint8(hdr[0]));
        SuccessOrExit(aFrameData.ReadUint8(len));

        VerifyOrExit(aFrameData.CanRead(len));
    }

    // length
    hdr[1] = BytesForBitSize(sizeof(hdr) + len) - 1;

    SuccessOrExit(aMessage.AppendBytes(hdr, sizeof(hdr)));
    aMessage.MoveOffset(sizeof(hdr));

    // payload
    SuccessOrExit(aMessage.AppendBytes(aFrameData.GetBytes(), len));
    aMessage.MoveOffset(len);
    aFrameData.SkipOver(len);

    // The RFC6282 says: "The trailing Pad1 or PadN option MAY be elided by the compressor.
    // A decompressor MUST ensure that the containing header is padded out to a multiple of 8 octets
    // in length, using a Pad1 or PadN option if necessary."

    if (padOption.InitToPadHeaderWithSize(len + sizeof(hdr)) == kErrorNone)
    {
        SuccessOrExit(aMessage.AppendBytes(&padOption, padOption.GetSize()));
        aMessage.MoveOffset(padOption.GetSize());
    }

    error = kErrorNone;

exit:
    return error;
}

Error Lowpan::DecompressUdpHeader(Ip6::Udp::Header &aUdpHeader, FrameData &aFrameData)
{
    Error    error = kErrorParse;
    uint8_t  udpCtl;
    uint8_t  byte;
    uint16_t srcPort = 0;
    uint16_t dstPort = 0;

    SuccessOrExit(aFrameData.ReadUint8(udpCtl));

    VerifyOrExit((udpCtl & kUdpDispatchMask) == kUdpDispatch);

    aUdpHeader.Clear();

    switch (udpCtl & kUdpPortMask)
    {
    case 0:
        SuccessOrExit(aFrameData.ReadBigEndianUint16(srcPort));
        SuccessOrExit(aFrameData.ReadBigEndianUint16(dstPort));
        break;

    case 1:
        SuccessOrExit(aFrameData.ReadBigEndianUint16(srcPort));
        SuccessOrExit(aFrameData.ReadUint8(byte));
        dstPort = (0xf000 | byte);
        break;

    case 2:
        SuccessOrExit(aFrameData.ReadUint8(byte));
        srcPort = (0xf000 | byte);
        SuccessOrExit(aFrameData.ReadBigEndianUint16(dstPort));
        break;

    case 3:
        SuccessOrExit(aFrameData.ReadUint8(byte));
        srcPort = (0xf0b0 | (byte >> 4));
        dstPort = (0xf0b0 | (byte & 0xf));
        break;
    }

    aUdpHeader.SetSourcePort(srcPort);
    aUdpHeader.SetDestinationPort(dstPort);

    if ((udpCtl & kUdpChecksum) != 0)
    {
        ExitNow();
    }
    else
    {
        uint16_t checksum;

        SuccessOrExit(aFrameData.ReadBigEndianUint16(checksum));
        aUdpHeader.SetChecksum(checksum);
    }

    error = kErrorNone;

exit:
    return error;
}

Error Lowpan::DecompressUdpHeader(Message &aMessage, FrameData &aFrameData, uint16_t aDatagramLength)
{
    Error            error;
    Ip6::Udp::Header udpHeader;

    SuccessOrExit(error = DecompressUdpHeader(udpHeader, aFrameData));

    // length
    if (aDatagramLength == 0)
    {
        udpHeader.SetLength(sizeof(udpHeader) + aFrameData.GetLength());
    }
    else
    {
        udpHeader.SetLength(aDatagramLength - aMessage.GetOffset());
    }

    SuccessOrExit(error = aMessage.Append(udpHeader));
    aMessage.MoveOffset(sizeof(udpHeader));

exit:
    return error;
}

Error Lowpan::Decompress(Message              &aMessage,
                         const Mac::Addresses &aMacAddrs,
                         FrameData            &aFrameData,
                         uint16_t              aDatagramLength)
{
    Error       error = kErrorParse;
    Ip6::Header ip6Header;
    bool        compressed;
    uint16_t    ip6PayloadLength;
    uint16_t    currentOffset = aMessage.GetOffset();

    SuccessOrExit(DecompressBaseHeader(ip6Header, compressed, aMacAddrs, aFrameData));

    SuccessOrExit(aMessage.Append(ip6Header));
    aMessage.MoveOffset(sizeof(ip6Header));

    while (compressed)
    {
        uint8_t byte;

        VerifyOrExit(aFrameData.GetLength() > 0);
        byte = *aFrameData.GetBytes();

        if ((byte & kExtHdrDispatchMask) == kExtHdrDispatch)
        {
            if ((byte & kExtHdrEidMask) == kExtHdrEidIp6)
            {
                compressed = false;

                aFrameData.SkipOver(sizeof(uint8_t));

                SuccessOrExit(Decompress(aMessage, aMacAddrs, aFrameData, aDatagramLength));
            }
            else
            {
                compressed = (byte & kExtHdrNextHeader) != 0;
                SuccessOrExit(DecompressExtensionHeader(aMessage, aFrameData));
            }
        }
        else if ((byte & kUdpDispatchMask) == kUdpDispatch)
        {
            compressed = false;
            SuccessOrExit(DecompressUdpHeader(aMessage, aFrameData, aDatagramLength));
        }
        else
        {
            ExitNow();
        }
    }

    if (aDatagramLength)
    {
        ip6PayloadLength = BigEndian::HostSwap16(aDatagramLength - currentOffset - sizeof(Ip6::Header));
    }
    else
    {
        ip6PayloadLength =
            BigEndian::HostSwap16(aMessage.GetOffset() - currentOffset - sizeof(Ip6::Header) + aFrameData.GetLength());
    }

    aMessage.Write(currentOffset + Ip6::Header::kPayloadLengthFieldOffset, ip6PayloadLength);

    error = kErrorNone;

exit:
    return error;
}

Ip6::Ecn Lowpan::DecompressEcn(const Message &aMessage, uint16_t aOffset) const
{
    Ip6::Ecn ecn = Ip6::kEcnNotCapable;
    uint16_t hcCtl;
    uint8_t  byte;

    SuccessOrExit(aMessage.Read(aOffset, hcCtl));
    hcCtl = BigEndian::HostSwap16(hcCtl);

    VerifyOrExit((hcCtl & kHcDispatchMask) == kHcDispatch);
    aOffset += sizeof(uint16_t);

    if ((hcCtl & kHcTrafficFlowMask) == kHcTrafficFlow)
    {
        // ECN is elided and is zero (`kEcnNotCapable`).
        ExitNow();
    }

    // When ECN is not elided, it is always included as the
    // first two bits of the next byte.
    SuccessOrExit(aMessage.Read(aOffset, byte));
    ecn = static_cast<Ip6::Ecn>((byte & kEcnMask) >> kEcnOffset);

exit:
    return ecn;
}

void Lowpan::MarkCompressedEcn(Message &aMessage, uint16_t aOffset)
{
    uint8_t byte;

    aOffset += sizeof(uint16_t);
    IgnoreError(aMessage.Read(aOffset, byte));

    byte &= ~kEcnMask;
    byte |= static_cast<uint8_t>(Ip6::kEcnMarked << kEcnOffset);

    aMessage.Write(aOffset, byte);
}

//---------------------------------------------------------------------------------------------------------------------
// MeshHeader

void MeshHeader::Init(uint16_t aSource, uint16_t aDestination, uint8_t aHopsLeft)
{
    mSource      = aSource;
    mDestination = aDestination;
    mHopsLeft    = aHopsLeft;
}

bool MeshHeader::IsMeshHeader(const FrameData &aFrameData)
{
    return (aFrameData.GetLength() >= kMinHeaderLength) && ((*aFrameData.GetBytes() & kDispatchMask) == kDispatch);
}

Error MeshHeader::ParseFrom(FrameData &aFrameData)
{
    Error    error;
    uint16_t headerLength;

    SuccessOrExit(error = ParseFrom(aFrameData.GetBytes(), aFrameData.GetLength(), headerLength));
    aFrameData.SkipOver(headerLength);

exit:
    return error;
}

Error MeshHeader::ParseFrom(const uint8_t *aFrame, uint16_t aFrameLength, uint16_t &aHeaderLength)
{
    Error   error = kErrorParse;
    uint8_t dispatch;

    VerifyOrExit(aFrameLength >= kMinHeaderLength);
    dispatch = *aFrame++;

    VerifyOrExit((dispatch & (kDispatchMask | kSourceShort | kDestShort)) == (kDispatch | kSourceShort | kDestShort));

    mHopsLeft = (dispatch & kHopsLeftMask);

    if (mHopsLeft == kDeepHopsLeft)
    {
        VerifyOrExit(aFrameLength >= kDeepHopsHeaderLength);
        mHopsLeft     = *aFrame++;
        aHeaderLength = kDeepHopsHeaderLength;
    }
    else
    {
        aHeaderLength = kMinHeaderLength;
    }

    mSource      = BigEndian::ReadUint16(aFrame);
    mDestination = BigEndian::ReadUint16(aFrame + 2);

    error = kErrorNone;

exit:
    return error;
}

Error MeshHeader::ParseFrom(const Message &aMessage)
{
    uint16_t headerLength;

    return ParseFrom(aMessage, headerLength);
}

Error MeshHeader::ParseFrom(const Message &aMessage, uint16_t &aHeaderLength)
{
    uint8_t  frame[kDeepHopsHeaderLength];
    uint16_t frameLength;

    frameLength = aMessage.ReadBytes(/* aOffset */ 0, frame, sizeof(frame));

    return ParseFrom(frame, frameLength, aHeaderLength);
}

uint16_t MeshHeader::GetHeaderLength(void) const
{
    return (mHopsLeft >= kDeepHopsLeft) ? kDeepHopsHeaderLength : kMinHeaderLength;
}

void MeshHeader::DecrementHopsLeft(void)
{
    if (mHopsLeft > 0)
    {
        mHopsLeft--;
    }
}

Error MeshHeader::AppendTo(FrameBuilder &aFrameBuilder) const
{
    Error   error;
    uint8_t dispatch = (kDispatch | kSourceShort | kDestShort);

    if (mHopsLeft < kDeepHopsLeft)
    {
        SuccessOrExit(error = aFrameBuilder.AppendUint8(dispatch | mHopsLeft));
    }
    else
    {
        SuccessOrExit(error = aFrameBuilder.AppendUint8(dispatch | kDeepHopsLeft));
        SuccessOrExit(error = aFrameBuilder.AppendUint8(mHopsLeft));
    }

    SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(mSource));
    SuccessOrExit(error = aFrameBuilder.AppendBigEndianUint16(mDestination));

exit:
    return error;
}

Error MeshHeader::AppendTo(Message &aMessage) const
{
    uint8_t      frame[kDeepHopsHeaderLength];
    FrameBuilder frameBuilder;

    frameBuilder.Init(frame, sizeof(frame));

    IgnoreError(AppendTo(frameBuilder));

    return aMessage.AppendBytes(frameBuilder.GetBytes(), frameBuilder.GetLength());
}

//---------------------------------------------------------------------------------------------------------------------
// FragmentHeader

bool FragmentHeader::IsFragmentHeader(const FrameData &aFrameData)
{
    return IsFragmentHeader(aFrameData.GetBytes(), aFrameData.GetLength());
}

bool FragmentHeader::IsFragmentHeader(const uint8_t *aFrame, uint16_t aFrameLength)
{
    return (aFrameLength >= sizeof(FirstFrag)) && ((*aFrame & kDispatchMask) == kDispatch);
}

Error FragmentHeader::ParseFrom(FrameData &aFrameData)
{
    Error    error;
    uint16_t headerLength;

    SuccessOrExit(error = ParseFrom(aFrameData.GetBytes(), aFrameData.GetLength(), headerLength));
    aFrameData.SkipOver(headerLength);

exit:
    return error;
}

Error FragmentHeader::ParseFrom(const uint8_t *aFrame, uint16_t aFrameLength, uint16_t &aHeaderLength)
{
    Error error = kErrorParse;

    VerifyOrExit(IsFragmentHeader(aFrame, aFrameLength));

    mSize = BigEndian::ReadUint16(aFrame + kSizeIndex) & kSizeMask;
    mTag  = BigEndian::ReadUint16(aFrame + kTagIndex);

    if ((*aFrame & kOffsetFlag) == kOffsetFlag)
    {
        VerifyOrExit(aFrameLength >= sizeof(NextFrag));
        mOffset       = aFrame[kOffsetIndex] * 8;
        aHeaderLength = sizeof(NextFrag);
    }
    else
    {
        mOffset       = 0;
        aHeaderLength = sizeof(FirstFrag);
    }

    error = kErrorNone;

exit:
    return error;
}

Error FragmentHeader::ParseFrom(const Message &aMessage, uint16_t aOffset, uint16_t &aHeaderLength)
{
    uint8_t  frame[sizeof(NextFrag)];
    uint16_t frameLength;

    frameLength = aMessage.ReadBytes(aOffset, frame, sizeof(frame));

    return ParseFrom(frame, frameLength, aHeaderLength);
}

} // namespace Lowpan
} // namespace ot
