/*
 *
 *    Copyright (c) 2021-2022 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

#pragma once

#include <array>

#include <lib/core/CHIPConfig.h>
#include <lib/core/CHIPEncoding.h>
#include <lib/core/NodeId.h>
#include <lib/support/CodeUtils.h>

namespace chip {

typedef uint32_t CASEAuthTag;

static constexpr CASEAuthTag kUndefinedCAT     = 0;
static constexpr NodeId kTagIdentifierMask     = 0x0000'0000'FFFF'0000ULL;
static constexpr uint32_t kTagIdentifierShift  = 16;
static constexpr NodeId kTagVersionMask        = 0x0000'0000'0000'FFFFULL;
static constexpr uint16_t kAdminCATIdentifier  = 0xFFFF;
static constexpr uint16_t kAnchorCATIdentifier = 0xFFFE;

// Maximum number of CASE Authenticated Tags (CAT) in the CHIP certificate subject.
static constexpr size_t kMaxSubjectCATAttributeCount = CHIP_CONFIG_CERT_MAX_RDN_ATTRIBUTES - 2;

constexpr NodeId NodeIdFromCASEAuthTag(CASEAuthTag aCAT)
{
    return kMinCASEAuthTag | aCAT;
}

constexpr CASEAuthTag CASEAuthTagFromNodeId(NodeId aNodeId)
{
    return aNodeId & kMaskCASEAuthTag;
}

constexpr bool IsValidCASEAuthTag(CASEAuthTag aCAT)
{
    return (aCAT & kTagVersionMask) > 0;
}

constexpr uint16_t GetCASEAuthTagIdentifier(CASEAuthTag aCAT)
{
    return static_cast<uint16_t>((aCAT & kTagIdentifierMask) >> kTagIdentifierShift);
}

constexpr uint16_t GetCASEAuthTagVersion(CASEAuthTag aCAT)
{
    return static_cast<uint16_t>(aCAT & kTagVersionMask);
}

constexpr CASEAuthTag GetAdminCATWithVersion(uint16_t version)
{
    return ((static_cast<uint32_t>(kAdminCATIdentifier) << 16) | version);
}

constexpr CASEAuthTag GetAnchorCATWithVersion(uint16_t version)
{
    return ((static_cast<uint32_t>(kAnchorCATIdentifier) << 16) | version);
}

struct CATValues
{
    std::array<CASEAuthTag, kMaxSubjectCATAttributeCount> values = { kUndefinedCAT };

    /* @brief Returns maximum number of CAT values that the array can contain.
     */
    static constexpr size_t size() { return std::tuple_size<decltype(values)>::value; }

    /**
     * @return the number of CATs present in the set (values not equal to kUndefinedCAT)
     */
    size_t GetNumTagsPresent() const
    {
        size_t count = 0;
        for (auto cat : values)
        {
            count += (cat != kUndefinedCAT) ? 1 : 0;
        }
        return count;
    }

    /**
     * @return true if `tag` is in the set exactly, false otherwise.
     */
    bool Contains(CASEAuthTag tag) const
    {
        for (auto candidate : values)
        {
            if ((candidate != kUndefinedCAT) && (candidate == tag))
            {
                return true;
            }
        }

        return false;
    }

    bool AreValid() const
    {
        for (size_t idx = 0; idx < size(); ++idx)
        {
            const auto & candidate = values[idx];
            if (candidate == kUndefinedCAT)
            {
                continue;
            }

            // Every entry that is not empty must have version > 0
            if (!IsValidCASEAuthTag(candidate))
            {
                return false;
            }
            // Identifiers cannot collide in set (there cannot be more than 1 version of an identifier)
            for (size_t other_idx = 0; other_idx < size(); ++other_idx)
            {
                if (idx == other_idx)
                {
                    continue;
                }
                if (values[other_idx] == kUndefinedCAT)
                {
                    continue;
                }

                uint16_t other_identifier     = GetCASEAuthTagIdentifier(values[other_idx]);
                uint16_t candidate_identifier = GetCASEAuthTagIdentifier(candidate);
                if (other_identifier == candidate_identifier)
                {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @brief Returns true if this set contains any version of the `identifier`
     *
     * @param identifier - CAT identifier to find
     * @return true if the identifier is in the set, false otherwise
     */
    bool ContainsIdentifier(uint16_t identifier) const
    {
        for (auto candidate : values)
        {
            uint16_t candidate_identifier = GetCASEAuthTagIdentifier(candidate);
            if ((candidate != kUndefinedCAT) && (identifier == candidate_identifier))
            {
                return true;
            }
        }

        return false;
    }

    /* @brief Returns true if subject input checks against one of the CATs in the values array.
     */
    bool CheckSubjectAgainstCATs(NodeId subject) const
    {
        VerifyOrReturnError(IsCASEAuthTag(subject), false);
        CASEAuthTag catFromSubject = CASEAuthTagFromNodeId(subject);

        for (auto catFromNoc : values)
        {
            if ((catFromNoc != kUndefinedCAT) &&
                (GetCASEAuthTagIdentifier(catFromNoc) == GetCASEAuthTagIdentifier(catFromSubject)) &&
                (GetCASEAuthTagVersion(catFromSubject) > 0) &&
                (GetCASEAuthTagVersion(catFromNoc) >= GetCASEAuthTagVersion(catFromSubject)))
            {
                return true;
            }
        }
        return false;
    }

    bool operator==(const CATValues & other) const
    {
        // Two sets of CATs confer equal permissions if the sets are exactly equal
        // and the sets are valid.
        // Ignoring kUndefinedCAT values, evaluate this.
        if (this->GetNumTagsPresent() != other.GetNumTagsPresent())
        {
            return false;
        }
        if (!this->AreValid() || !other.AreValid())
        {
            return false;
        }
        for (auto cat : this->values)
        {
            if (cat == kUndefinedCAT)
            {
                continue;
            }

            if (!other.Contains(cat))
            {
                return false;
            }
        }
        return true;
    }
    bool operator!=(const CATValues & other) const { return !(*this == other); }

    static constexpr size_t kSerializedLength = kMaxSubjectCATAttributeCount * sizeof(CASEAuthTag);
    typedef uint8_t Serialized[kSerializedLength];

    CHIP_ERROR Serialize(Serialized & outSerialized) const
    {
        uint8_t * p = outSerialized;
        for (size_t i = 0; i < kMaxSubjectCATAttributeCount; i++)
        {
            Encoding::LittleEndian::Write32(p, values[i]);
        }
        return CHIP_NO_ERROR;
    }

    CHIP_ERROR Deserialize(const Serialized & inSerialized)
    {
        const uint8_t * p = inSerialized;
        for (size_t i = 0; i < kMaxSubjectCATAttributeCount; i++)
        {
            values[i] = Encoding::LittleEndian::Read32(p);
        }
        return CHIP_NO_ERROR;
    }
};

static constexpr CATValues kUndefinedCATs = { { kUndefinedCAT } };

} // namespace chip
