/*
 *
 *    Copyright (c) 2020-2021 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 <lib/core/CHIPError.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/Pool.h>
#include <lib/support/SortUtils.h>
#include <system/TimeSource.h>
#include <transport/SecureSession.h>

namespace chip {
namespace Transport {

inline constexpr uint16_t kMaxSessionID       = UINT16_MAX;
inline constexpr uint16_t kUnsecuredSessionId = 0;

/**
 * Handles a set of sessions.
 *
 * Intended for:
 *   - handle session active time and expiration
 *   - allocate and free space for sessions.
 */
class SecureSessionTable
{
public:
    ~SecureSessionTable() { mEntries.ReleaseAll(); }

    void Init() { mNextSessionId = chip::Crypto::GetRandU16(); }

    /**
     * Allocate a new secure session out of the internal resource pool.
     *
     * @param secureSessionType secure session type
     * @param localSessionId unique identifier for the local node's secure unicast session context
     * @param localNodeId represents the local Node ID for this node
     * @param peerNodeId represents peer Node's ID
     * @param peerCATs represents peer CASE Authenticated Tags
     * @param peerSessionId represents the encryption key ID assigned by peer node
     * @param fabricIndex represents fabric index for the session
     * @param config represents the reliable message protocol configuration
     *
     * @note the newly created state will have an 'active' time set based on the current time source.
     *
     * @returns CHIP_NO_ERROR if state could be initialized. May fail if maximum session count
     *          has been reached (with CHIP_ERROR_NO_MEMORY).
     */
    CHECK_RETURN_VALUE
    Optional<SessionHandle> CreateNewSecureSessionForTest(SecureSession::Type secureSessionType, uint16_t localSessionId,
                                                          NodeId localNodeId, NodeId peerNodeId, CATValues peerCATs,
                                                          uint16_t peerSessionId, FabricIndex fabricIndex,
                                                          const ReliableMessageProtocolConfig & config);
    /**
     * Allocate a new secure session out of the internal resource pool with a
     * non-colliding session ID and increments mNextSessionId to give a clue to
     * the allocator for the next allocation.  The secure session session will
     * not become active until the call to SecureSession::Activate.
     *
     * @returns allocated session, or NullOptional on failure
     */
    CHECK_RETURN_VALUE
    Optional<SessionHandle> CreateNewSecureSession(SecureSession::Type secureSessionType, ScopedNodeId sessionEvictionHint);

    void ReleaseSession(SecureSession * session) { mEntries.ReleaseObject(session); }

    template <typename Function>
    Loop ForEachSession(Function && function)
    {
        return mEntries.ForEachActiveObject(std::forward<Function>(function));
    }

    /**
     * Get a secure session given its session ID.
     *
     * @param localSessionId the identifier of a secure unicast session context within the local node
     *
     * @return the session if found, NullOptional if not found
     */
    CHECK_RETURN_VALUE
    Optional<SessionHandle> FindSecureSessionByLocalKey(uint16_t localSessionId);

    // Select SessionHolders which are pointing to a session with the same peer as the given session. Shift them to the given
    // session.
    // This is an internal API, using raw pointer to a session is allowed here.
    void NewerSessionAvailable(SecureSession * session)
    {
        VerifyOrDie(session->GetSecureSessionType() == SecureSession::Type::kCASE);
        mEntries.ForEachActiveObject([&](SecureSession * oldSession) {
            if (session == oldSession)
                return Loop::Continue;

            SessionHandle ref(*oldSession);

            // This will give all SessionHolders pointing to oldSession a chance to switch to the provided session
            //
            // See documentation for SessionDelegate::GetNewSessionHandlingPolicy about how session auto-shifting works, and how
            // to disable it for a specific SessionHolder in a specific scenario.
            if (oldSession->GetSecureSessionType() == SecureSession::Type::kCASE && oldSession->GetPeer() == session->GetPeer() &&
                oldSession->GetPeerCATs() == session->GetPeerCATs())
            {
                oldSession->NewerSessionAvailable(SessionHandle(*session));
            }

            return Loop::Continue;
        });
    }

private:
    friend class TestSecureSessionTable;

    /**
     * This provides a sortable wrapper for a SecureSession object. A SecureSession
     * isn't directly sortable since it is not swappable (i.e meet criteria for ValueSwappable).
     *
     * However, this wrapper has a stable pointer to a SecureSession while being swappable with
     * another instance of it.
     *
     */
    struct SortableSession
    {
    public:
        void swap(SortableSession & other)
        {
            SortableSession tmp(other);
            other.mSession = mSession;
            mSession       = tmp.mSession;
        }

        const Transport::SecureSession * operator->() const { return mSession; }
        auto GetNumMatchingOnFabric() { return mNumMatchingOnFabric; }
        auto GetNumMatchingOnPeer() { return mNumMatchingOnPeer; }

    private:
        SecureSession * mSession;
        uint16_t mNumMatchingOnFabric;
        uint16_t mNumMatchingOnPeer;

        static_assert(CHIP_CONFIG_SECURE_SESSION_POOL_SIZE <= std::numeric_limits<decltype(mNumMatchingOnFabric)>::max(),
                      "mNumMatchingOnFabric must be able to count up to CHIP_CONFIG_SECURE_SESSION_POOL_SIZE!");
        static_assert(CHIP_CONFIG_SECURE_SESSION_POOL_SIZE <= std::numeric_limits<decltype(mNumMatchingOnPeer)>::max(),
                      "mNumMatchingOnPeer must be able to count up to CHIP_CONFIG_SECURE_SESSION_POOL_SIZE!");

        friend class SecureSessionTable;
    };

    /**
     *
     * Encapsulates all the necessary context for an eviction policy callback
     * to implement its specific policy. The context is provided to the callee
     * with the expectation that it'll call Sort() with a comparator function provided
     * to get the list of sessions sorted in the desired order.
     *
     */
    class EvictionPolicyContext
    {
    public:
        /*
         * Called by the policy implementor to sort the list of sessions given a comparator
         * function. The provided function shall have the following signature:
         *
         * bool CompareFunc(const SortableSession &a, const SortableSession &b);
         *
         * If a is a better candidate than b, true should be returned. Else, return false.
         *
         * NOTE: Sort() can be called multiple times.
         *
         */
        template <typename CompareFunc>
        void Sort(CompareFunc func)
        {
            Sorting::InsertionSort(mSessionList.begin(), mSessionList.size(), func);
        }

        const ScopedNodeId & GetSessionEvictionHint() const { return mSessionEvictionHint; }

    private:
        EvictionPolicyContext(Span<SortableSession> sessionList, ScopedNodeId sessionEvictionHint)
        {
            mSessionList         = sessionList;
            mSessionEvictionHint = sessionEvictionHint;
        }

        friend class SecureSessionTable;
        Span<SortableSession> mSessionList;
        ScopedNodeId mSessionEvictionHint;
    };

    /**
     *
     * This implements an eviction policy by sorting sessions using the following sorting keys and selecting
     * the session that is most ahead as the best candidate for eviction:
     *
     *  - Key1:  Sessions on fabrics that have more sessions in the table are placed ahead of sessions on fabrics
     *           with fewer sessions. We conclusively know that if a particular fabric has more sessions in the table
     *           than another, then that fabric is definitely over minimas (assuming a minimally sized session table
     *           conformant to spec minimas).
     *
     *    Key2:  Sessions that match the eviction hint's fabric are placed ahead of those that don't. This ensures that
     *           if Key1 is even (i.e two fabrics are tied in count), that you attempt to select sessions that match
     *           the eviction hint's fabric to ensure we evict sessions within the fabric that a new session might be about
     *           to be created within. This is essential to preventing cross-fabric denial of service possibilities.
     *
     *    Key3:  Sessions with a higher mNumMatchingOnPeer are placed ahead of those with a lower one. This ensures
     *           we pick sessions that have a higher number of duplicated sessions to a peer over those with lower since
     *           evicting a duplicated session will have less of an impact to that peer.
     *
     *    Key4:  Sessions whose target peer's ScopedNodeId matches the eviction hint are placed ahead of those who don't. This
     * ensures that all things equal, a session that already exists to the peer is refreshed ahead of another to another peer.
     *
     *    Key5:  Sessions that are in defunct state are placed ahead of those in the active state, ahead of any other state.
     *           This ensures that we prioritize evicting defunct sessions (since they have been deemed non-functional anyways)
     *           over active, healthy ones, over those are currently in the process of establishment.
     *
     *    Key6:  Sessions that have a less recent activity time are placed ahead of those with a more recent activity time. This
     *           is the canonical sorting criteria for basic LRU.
     *
     */
    void DefaultEvictionPolicy(EvictionPolicyContext & evictionContext);

    /**
     *
     * Evicts a session from the session table using the DefaultEvictionPolicy implementation.
     *
     */
    SecureSession * EvictAndAllocate(uint16_t localSessionId, SecureSession::Type secureSessionType,
                                     const ScopedNodeId & sessionEvictionHint);

    /**
     * Find an available session ID that is unused in the secure session table.
     *
     * The search algorithm iterates over the session ID space in the outer loop
     * and the session table in the inner loop to locate an available session ID
     * from the starting mNextSessionId clue.
     *
     * The outer-loop considers 64 session IDs in each iteration to give a
     * runtime complexity of O(CHIP_CONFIG_PEER_CONNECTION_POOL_SIZE^2/64).  Speed up could be
     * achieved with a sorted session table or additional storage.
     *
     * @return an unused session ID if any is found, else NullOptional
     */
    CHECK_RETURN_VALUE
    Optional<uint16_t> FindUnusedSessionId();

    bool mRunningEvictionLogic = false;
    ObjectPool<SecureSession, CHIP_CONFIG_SECURE_SESSION_POOL_SIZE> mEntries;

    size_t GetMaxSessionTableSize() const
    {
#if CONFIG_BUILD_FOR_HOST_UNIT_TEST
        return mMaxSessionTableSize;
#else
        return CHIP_CONFIG_SECURE_SESSION_POOL_SIZE;
#endif
    }

#if CONFIG_BUILD_FOR_HOST_UNIT_TEST
    size_t mMaxSessionTableSize = CHIP_CONFIG_SECURE_SESSION_POOL_SIZE;
    void SetMaxSessionTableSize(size_t size) { mMaxSessionTableSize = size; }
#endif

    uint16_t mNextSessionId = 0;
};

} // namespace Transport
} // namespace chip
