/*
 *
 *    Copyright (c) 2020 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 <bitset>
#include <cstddef>
#include <type_traits>

#include <lib/core/CHIPError.h>

namespace chip {
namespace Retransmit {

/**
 * Handles lifetimes of payloads.
 *
 * E.g. for buffers, handles reference counting.
 */
template <typename PayloadType>
struct Lifetime
{
    static PayloadType Acquire(PayloadType & payload);
    static void Release(PayloadType & payload);
};

/**
 * This class maintains a cache of data that is sufficient to retransmit.
 *
 * Typical use is to keep track of unacknowledged packets and resend them
 * as needed.
 *
 * @tparam KeyType the key to identify a single message
 * @tparam PayloadType the type of payload to cache for the given peer address
 * @tparam N size of the available cache
 *
 * PayloadType MUST provide a way to reference count, as the data will
 * preserved until the cache is freed:
 *
 *    PayloadType chip::Retransmit::Acquire(PayloadType&);
 *    chip::Retransmit::Release(PayloadType&);
 */
template <typename KeyType, typename PayloadType, size_t N>
class Cache
{
public:
    Cache() {}
    Cache(const Cache &)             = delete;
    Cache & operator=(const Cache &) = delete;

    ~Cache()
    {
        for (size_t i = 0; i < N; i++)
        {
            if (mInUse.test(i))
            {
                Lifetime<PayloadType>::Release(mEntries[i].payload);
            }
        }
    }

    /**
     * Add a payload to the cache.
     */
    CHIP_ERROR Add(const KeyType & key, PayloadType & payload)
    {
        if (mInUse.count() >= N)
        {
            return CHIP_ERROR_NO_MEMORY;
        }

        for (size_t i = 0; i < N; i++)
        {
            if (!mInUse.test(i))
            {
                mInUse.set(i);
                mEntries[i].key     = key;
                mEntries[i].payload = Lifetime<PayloadType>::Acquire(payload);
                break;
            }
        }

        return CHIP_NO_ERROR;
    }

    /**
     * Remove a payload from the cache given the key.
     */
    CHIP_ERROR Remove(const KeyType & key)
    {
        for (size_t i = 0; i < N; i++)
        {
            if (mInUse.test(i) && (mEntries[i].key == key))
            {
                mInUse.reset(i);
                Lifetime<PayloadType>::Release(mEntries[i].payload);
                return CHIP_NO_ERROR;
            }
        }

        return CHIP_ERROR_KEY_NOT_FOUND;
    }

    /**
     * Remove any matching payloads. Used for mass removal, e.g. when a connection
     * is closed, relevant payloads need/can be cleared for the entire connection.
     *
     * @tparam Matcher is a generic matcher object defining a bool Matches method.
     */
    template <typename Matcher>
    void RemoveMatching(const Matcher & matcher)
    {
        for (size_t i = 0; i < N; i++)
        {
            if (mInUse.test(i) && matcher.Matches(mEntries[i].key))
            {
                mInUse.reset(i);
                Lifetime<PayloadType>::Release(mEntries[i].payload);
            }
        }
    }

    /**
     * Search for a specific entry within the cache.
     *
     * @tparam Matcher is a generic macher object defining a bool Maches method.
     *
     * @param matcher the entry to find
     * @param key - out set the key if found
     * @param payload - the payload if found
     *
     * Key and payload are only valid as long as no remove methods
     * are called on the class.
     */
    template <typename Matcher>
    bool Find(const Matcher & matcher, const KeyType ** key, const PayloadType ** payload)
    {
        *key     = nullptr;
        *payload = nullptr;

        for (size_t i = 0; i < N; i++)
        {
            if (mInUse.test(i) && matcher.Matches(mEntries[i].key))
            {
                *key     = &mEntries[i].key;
                *payload = &mEntries[i].payload;
                return true;
            }
        }
        return false;
    }

private:
    struct Entry
    {
        KeyType key;
        PayloadType payload;
    };

    Entry mEntries[N];     // payload entries
    std::bitset<N> mInUse; // compact 'in use' marker for payloads
};                         // namespace Retransmit

} // namespace Retransmit
} // namespace chip
