/*
 *
 *    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 <ble/Ble.h>
#include <lib/core/CHIPError.h>
#include <lib/core/ReferenceCounted.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/Pool.h>
#include <messaging/ReliableMessageProtocolConfig.h>
#include <system/SystemConfig.h>
#include <system/TimeSource.h>
#include <transport/PeerMessageCounter.h>
#include <transport/Session.h>
#include <transport/raw/PeerAddress.h>

namespace chip {
namespace Transport {

/**
 * @brief
 *   An UnauthenticatedSession stores the binding of TransportAddress, and message counters.
 */
class UnauthenticatedSession : public Session, public ReferenceCounted<UnauthenticatedSession, UnauthenticatedSession, 0>
{
public:
    enum class SessionRole
    {
        kInitiator,
        kResponder,
    };

protected:
    UnauthenticatedSession(SessionRole sessionRole, NodeId ephemeralInitiatorNodeID, const Transport::PeerAddress & peerAddress,
                           const ReliableMessageProtocolConfig & config) :
        mEphemeralInitiatorNodeId(ephemeralInitiatorNodeID),
        mSessionRole(sessionRole), mPeerAddress(peerAddress), mLastActivityTime(System::SystemClock().GetMonotonicTimestamp()),
        mLastPeerActivityTime(System::Clock::kZero), // Start at zero to default to IDLE state
        mRemoteSessionParams(config)
    {}
    ~UnauthenticatedSession() override { VerifyOrDie(GetReferenceCount() == 0); }

public:
    UnauthenticatedSession(const UnauthenticatedSession &)             = delete;
    UnauthenticatedSession & operator=(const UnauthenticatedSession &) = delete;
    UnauthenticatedSession(UnauthenticatedSession &&)                  = delete;
    UnauthenticatedSession & operator=(UnauthenticatedSession &&)      = delete;

    System::Clock::Timestamp GetLastActivityTime() const { return mLastActivityTime; }
    System::Clock::Timestamp GetLastPeerActivityTime() const { return mLastPeerActivityTime; }
    void MarkActive() { mLastActivityTime = System::SystemClock().GetMonotonicTimestamp(); }
    void MarkActiveRx()
    {
        mLastPeerActivityTime = System::SystemClock().GetMonotonicTimestamp();
        MarkActive();
    }

    Session::SessionType GetSessionType() const override { return Session::SessionType::kUnauthenticated; }

    void Retain() override { ReferenceCounted<UnauthenticatedSession, UnauthenticatedSession, 0>::Retain(); }
    void Release() override { ReferenceCounted<UnauthenticatedSession, UnauthenticatedSession, 0>::Release(); }

    bool IsActiveSession() const override { return true; }

    ScopedNodeId GetPeer() const override { return ScopedNodeId(GetPeerNodeId(), kUndefinedFabricIndex); }
    ScopedNodeId GetLocalScopedNodeId() const override { return ScopedNodeId(kUndefinedNodeId, kUndefinedFabricIndex); }

    Access::SubjectDescriptor GetSubjectDescriptor() const override
    {
        return Access::SubjectDescriptor(); // return an empty ISD for unauthenticated session.
    }

    bool AllowsMRP() const override { return GetPeerAddress().GetTransportType() == Transport::Type::kUdp; }

    bool AllowsLargePayload() const override { return GetPeerAddress().GetTransportType() == Transport::Type::kTcp; }

    System::Clock::Milliseconds32 GetAckTimeout() const override
    {
        switch (mPeerAddress.GetTransportType())
        {
        case Transport::Type::kUdp: {
            const ReliableMessageProtocolConfig & remoteMRPConfig = mRemoteSessionParams.GetMRPConfig();
            return GetRetransmissionTimeout(remoteMRPConfig.mActiveRetransTimeout, remoteMRPConfig.mIdleRetransTimeout,
                                            GetLastPeerActivityTime(), remoteMRPConfig.mActiveThresholdTime);
        }
        case Transport::Type::kTcp:
            return System::Clock::Seconds16(30);
        case Transport::Type::kBle:
            return System::Clock::Milliseconds32(BTP_ACK_TIMEOUT_MS);
        default:
            break;
        }
        return System::Clock::Timeout();
    }

    System::Clock::Milliseconds32 GetMessageReceiptTimeout(System::Clock::Timestamp ourLastActivity) const override
    {
        switch (mPeerAddress.GetTransportType())
        {
        case Transport::Type::kUdp: {
            const auto & maybeLocalMRPConfig = GetLocalMRPConfig();
            const auto & defaultMRRPConfig   = GetDefaultMRPConfig();
            const auto & localMRPConfig      = maybeLocalMRPConfig.ValueOr(defaultMRRPConfig);
            return GetRetransmissionTimeout(localMRPConfig.mActiveRetransTimeout, localMRPConfig.mIdleRetransTimeout,
                                            ourLastActivity, localMRPConfig.mActiveThresholdTime);
        }
        case Transport::Type::kTcp:
            return System::Clock::Seconds16(30);
        case Transport::Type::kBle:
            return System::Clock::Milliseconds32(BTP_ACK_TIMEOUT_MS);
        default:
            break;
        }
        return System::Clock::Timeout();
    }

    NodeId GetPeerNodeId() const
    {
        if (mSessionRole == SessionRole::kInitiator)
        {
            return kUndefinedNodeId;
        }

        return mEphemeralInitiatorNodeId;
    }

    SessionRole GetSessionRole() const { return mSessionRole; }
    NodeId GetEphemeralInitiatorNodeID() const { return mEphemeralInitiatorNodeId; }
    const PeerAddress & GetPeerAddress() const { return mPeerAddress; }
    void SetPeerAddress(const PeerAddress & peerAddress) { mPeerAddress = peerAddress; }

    bool IsPeerActive() const
    {
        return ((System::SystemClock().GetMonotonicTimestamp() - GetLastPeerActivityTime()) <
                GetRemoteMRPConfig().mActiveThresholdTime);
    }

    System::Clock::Timestamp GetMRPBaseTimeout() const override
    {
        return IsPeerActive() ? GetRemoteMRPConfig().mActiveRetransTimeout : GetRemoteMRPConfig().mIdleRetransTimeout;
    }

    void SetRemoteSessionParameters(const SessionParameters & sessionParams) { mRemoteSessionParams = sessionParams; }

    const SessionParameters & GetRemoteSessionParameters() const override { return mRemoteSessionParams; }

    PeerMessageCounter & GetPeerMessageCounter() { return mPeerMessageCounter; }

    static void Release(UnauthenticatedSession * obj)
    {
        // When using heap pools, we need to make sure to release ourselves back to
        // the pool.  When not using heap pools, we don't want the extra cost of the
        // table pointer here, and the table itself handles entry reuse and cleanup
        // as needed.
#if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
        obj->ReleaseSelfToPool();
#else
        // Just do nothing.
#endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
    }

#if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
    virtual void ReleaseSelfToPool() = 0;
#endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP

private:
    const NodeId mEphemeralInitiatorNodeId;
    const SessionRole mSessionRole;
    PeerAddress mPeerAddress;
    System::Clock::Timestamp mLastActivityTime;     ///< Timestamp of last tx or rx
    System::Clock::Timestamp mLastPeerActivityTime; ///< Timestamp of last rx
    SessionParameters mRemoteSessionParams;
    PeerMessageCounter mPeerMessageCounter;
};

template <size_t kMaxSessionCount>
class UnauthenticatedSessionTable;

namespace detail {

template <size_t kMaxSessionCount>
class UnauthenticatedSessionPoolEntry : public UnauthenticatedSession
{
public:
    UnauthenticatedSessionPoolEntry(SessionRole sessionRole, NodeId ephemeralInitiatorNodeID,
                                    const Transport::PeerAddress & peerAddress, const ReliableMessageProtocolConfig & config,
                                    UnauthenticatedSessionTable<kMaxSessionCount> & sessionTable) :
        UnauthenticatedSession(sessionRole, ephemeralInitiatorNodeID, peerAddress, config)
#if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
        ,
        mSessionTable(sessionTable)
#endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
    {}

private:
#if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
    virtual void ReleaseSelfToPool();

    UnauthenticatedSessionTable<kMaxSessionCount> & mSessionTable;
#endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
};

} // namespace detail

/*
 * @brief
 *   An table which manages UnauthenticatedSessions
 *
 *   The UnauthenticatedSession entries are rotated using LRU, but entry can be hold by using SessionHandle or
 *   SessionHolder, which increase the reference count by 1. If the reference count is not 0, the entry won't be pruned.
 */
template <size_t kMaxSessionCount>
class UnauthenticatedSessionTable
{
public:
    ~UnauthenticatedSessionTable()
    {
#if !CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
        // When not using heap pools, our entries never actually get released
        // back to the pool (which lets us make the entries 4 bytes smaller by
        // not storing a reference to the table in them) and we LRU reuse ones
        // that have 0 refcount.  But we should release them all here, to ensure
        // that we don't hit fatal asserts in our pool destructor.
        mEntries.ReleaseAll();
#endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
    }

    /**
     * Get a responder session with the given ephemeralInitiatorNodeID. If the session doesn't exist in the cache, allocate a new
     * entry for it.
     *
     * @return the session found or allocated, or Optional::Missing if not found and allocation failed.
     */
    CHECK_RETURN_VALUE
    Optional<SessionHandle> FindOrAllocateResponder(NodeId ephemeralInitiatorNodeID, const ReliableMessageProtocolConfig & config,
                                                    const Transport::PeerAddress & peerAddress)
    {
        UnauthenticatedSession * result =
            FindEntry(UnauthenticatedSession::SessionRole::kResponder, ephemeralInitiatorNodeID, peerAddress);
        if (result != nullptr)
            return MakeOptional<SessionHandle>(*result);

        CHIP_ERROR err =
            AllocEntry(UnauthenticatedSession::SessionRole::kResponder, ephemeralInitiatorNodeID, peerAddress, config, result);
        if (err == CHIP_NO_ERROR)
        {
            return MakeOptional<SessionHandle>(*result);
        }

        return Optional<SessionHandle>::Missing();
    }

    CHECK_RETURN_VALUE Optional<SessionHandle> FindInitiator(NodeId ephemeralInitiatorNodeID,
                                                             const Transport::PeerAddress & peerAddress)
    {
        UnauthenticatedSession * result =
            FindEntry(UnauthenticatedSession::SessionRole::kInitiator, ephemeralInitiatorNodeID, peerAddress);
        if (result != nullptr)
        {
            return MakeOptional<SessionHandle>(*result);
        }

        return Optional<SessionHandle>::Missing();
    }

    CHECK_RETURN_VALUE Optional<SessionHandle> AllocInitiator(NodeId ephemeralInitiatorNodeID, const PeerAddress & peerAddress,
                                                              const ReliableMessageProtocolConfig & config)
    {
        UnauthenticatedSession * result = nullptr;
        CHIP_ERROR err =
            AllocEntry(UnauthenticatedSession::SessionRole::kInitiator, ephemeralInitiatorNodeID, peerAddress, config, result);
        if (err == CHIP_NO_ERROR)
        {
            result->SetPeerAddress(peerAddress);
            return MakeOptional<SessionHandle>(*result);
        }

        return Optional<SessionHandle>::Missing();
    }

private:
    using EntryType = detail::UnauthenticatedSessionPoolEntry<kMaxSessionCount>;
    friend EntryType;

    /**
     * Allocates a new session out of the internal resource pool.
     *
     * @returns CHIP_NO_ERROR if new session created. May fail if maximum session count has been reached (with
     * CHIP_ERROR_NO_MEMORY).
     */
    CHECK_RETURN_VALUE
    CHIP_ERROR AllocEntry(UnauthenticatedSession::SessionRole sessionRole, NodeId ephemeralInitiatorNodeID,
                          const PeerAddress & peerAddress, const ReliableMessageProtocolConfig & config,
                          UnauthenticatedSession *& entry)
    {
        auto entryToUse = mEntries.CreateObject(sessionRole, ephemeralInitiatorNodeID, peerAddress, config, *this);
        if (entryToUse != nullptr)
        {
            entry = entryToUse;
            return CHIP_NO_ERROR;
        }

#if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
        // permanent failure if heap was insufficient
        return CHIP_ERROR_NO_MEMORY;
#else
        entryToUse = FindLeastRecentUsedEntry();
        VerifyOrReturnError(entryToUse != nullptr, CHIP_ERROR_NO_MEMORY);

        // Drop the least recent entry to allow for a new alloc.
        mEntries.ReleaseObject(entryToUse);
        entryToUse = mEntries.CreateObject(sessionRole, ephemeralInitiatorNodeID, peerAddress, config, *this);

        if (entryToUse == nullptr)
        {
            // this is NOT expected: we freed an object to have space
            return CHIP_ERROR_INTERNAL;
        }

        entry = entryToUse;
        return CHIP_NO_ERROR;
#endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
    }

    CHECK_RETURN_VALUE UnauthenticatedSession * FindEntry(UnauthenticatedSession::SessionRole sessionRole,
                                                          NodeId ephemeralInitiatorNodeID,
                                                          const Transport::PeerAddress & peerAddress)
    {
        UnauthenticatedSession * result = nullptr;
        mEntries.ForEachActiveObject([&](UnauthenticatedSession * entry) {
            if (entry->GetSessionRole() == sessionRole && entry->GetEphemeralInitiatorNodeID() == ephemeralInitiatorNodeID &&
                entry->GetPeerAddress().GetTransportType() == peerAddress.GetTransportType())
            {
                result = entry;
                return Loop::Break;
            }
            return Loop::Continue;
        });
        return result;
    }

    EntryType * FindLeastRecentUsedEntry()
    {
        EntryType * result                  = nullptr;
        System::Clock::Timestamp oldestTime = System::Clock::Timestamp(std::numeric_limits<System::Clock::Timestamp::rep>::max());

        mEntries.ForEachActiveObject([&](EntryType * entry) {
            if (entry->GetReferenceCount() == 0 && entry->GetLastActivityTime() < oldestTime)
            {
                result     = entry;
                oldestTime = entry->GetLastActivityTime();
            }
            return Loop::Continue;
        });

        return result;
    }

    void ReleaseEntry(EntryType * entry) { mEntries.ReleaseObject(entry); }

    ObjectPool<EntryType, kMaxSessionCount> mEntries;
};

#if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
template <size_t kMaxSessionCount>
void detail::UnauthenticatedSessionPoolEntry<kMaxSessionCount>::ReleaseSelfToPool()
{
    mSessionTable.ReleaseEntry(this);
}
#endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP

} // namespace Transport
} // namespace chip
