#ifndef MS_RTC_SCTP_STATE_COOKIE_HPP
#define MS_RTC_SCTP_STATE_COOKIE_HPP

#include "common.hpp"
#include "RTC/SCTP/association/NegotiatedCapabilities.hpp"
#include "RTC/SCTP/public/SctpTypes.hpp"
#include "RTC/Serializable.hpp"
#include "Utils.hpp"

namespace RTC
{
	namespace SCTP
	{
		/**
		 * This is the State Cookie we generate and put into a State Cookie
		 * parameter when we send INIT-ACK chunk to the remote peer.
		 *
		 * The syntax we use is as follows. Note that we use a fixed length of
		 * StateCookieLength bytes.
		 *
		 *  0                   1                   2                   3
		 *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |                            Magic 1                            |
		 * |                                                               |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |                    Local Verification Tag                     |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |                    Remote Verification Tag                    |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |                      Local Initial TSN                        |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |                      Remote Initial TSN                       |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |       Remote Advertised Receiver Window Credit (a_rwnd)       |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |                            Tie-Tag                            |
		 * |                                                               |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * \                                                               \
		 * /                     Negotiated Capabilities                   /
		 * \                                                               \
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 *
		 * Negotiated Capabilities are serialized as follows:
		 *
		 *  0                   1                   2                   3
		 *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |   (Reserved)  |       |D|C|B|A|            Magic 2            |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |      Max Outbound Streams     |       Max Inbound Streams     |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 *
		 * - Flag A (partialReliability): Partial Reliability Extension.
		 * - Flag B (messageInterleaving): Stream Schedulers and User Message
		 *   Interleaving (I-DATA).
		 * - Flag C (reconfig): Stream Reconfiguration.
		 * - Flag D (zeroChecksum): Zero Checksum.
		 *
		 * When State Cookie authentication is enabled (see
		 * `SctpOptions::requireAuthenticatedCookie`), two extra fields are appended
		 * after the Negotiated Capabilities so the receiver can verify that the
		 * cookie was generated by itself and is not stale:
		 *
		 *  0                   1                   2                   3
		 *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * |                       Creation Timestamp                      |
		 * |                                                               |
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 * \                                                               \
		 * /            MAC (HMAC-SHA1 over all preceding bytes)           /
		 * \                                                               \
		 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		 *
		 * The MAC is keyed with a per-association secret that never leaves the
		 * worker, making the cookie unforgeable. The Creation Timestamp (in ms) is
		 * used to detect stale cookies.
		 *
		 * @see RFC 9260 sections 5.1.3 and 5.1.4.
		 */
		class StateCookie : public Serializable
		{
		private:
			struct NegotiatedCapabilitiesField
			{
				uint8_t reserved;
#if defined(MS_LITTLE_ENDIAN)
				uint8_t bitA : 1;
				uint8_t bitB : 1;
				uint8_t bitC : 1;
				uint8_t bitD : 1;
				uint8_t unusedBits : 4;
#elif defined(MS_BIG_ENDIAN)
				uint8_t unusedBits : 4;
				uint8_t bitD : 1;
				uint8_t bitC : 1;
				uint8_t bitB : 1;
				uint8_t bitA : 1;
#endif
				uint16_t magic2;
				uint16_t negotiatedMaxOutboundStreams;
				uint16_t negotiatedMaxInboundStreams;
			};

		public:
			// Fixed length of our generated State Cookies when authentication is
			// disabled.
			static constexpr size_t StateCookieLength{ 44 };
			// Offset in the State Cookie where the Negotiated Capabilitied are
			// located.
			static constexpr size_t NegotiatedCapabilitiesOffset{ 36 };
			// Magic value we prefix the State Cookie with. Note that it is
			// "msworker" in ASCII bytes.
			static constexpr uint64_t Magic1{ 0x6D73776F726B6572 };
			static constexpr size_t Magic1Length{ 8 };
			// Magic value used within the Negotiated Capabilities block.
			static constexpr uint16_t Magic2{ 0xAD81 };
			// Offset of the creation timestamp present in authenticated State
			// Cookies.
			static constexpr size_t TimestampOffset{ StateCookie::StateCookieLength };
			// Length of the creation timestamp field (a uint64_t with the time in
			// milliseconds).
			static constexpr size_t TimestampLength{ 8 };
			// Offset of the MAC present in authenticated State Cookies.
			static constexpr size_t MacOffset{ StateCookie::TimestampOffset + StateCookie::TimestampLength };
			// Length of the MAC. We use HMAC-SHA1 so it's 20 bytes.
			static constexpr size_t MacLength{ 20 };
			// Fixed length of our generated State Cookies when authentication is
			// enabled.
			static constexpr size_t AuthenticatedStateCookieLength{ StateCookie::MacOffset +
				                                                      StateCookie::MacLength };
			// State Cookie lifespan (Valid.Cookie.Life) in milliseconds. Used to
			// reject stale authenticated cookies.
			//
			// @see RFC 9260 section 16.
			static constexpr uint64_t ValidCookieLifeMs{ 60000 };

		public:
			/**
			 * Parse a StateCookie supposely generated by mediasoup.
			 *
			 * @remarks
			 * `bufferLength` must be the exact length of the State Cookie.
			 */
			static StateCookie* Parse(const uint8_t* buffer, size_t bufferLength);

			/**
			 * Create a StateCookie.
			 *
			 * @remarks
			 * - `bufferLength` could be greater than the real length of the State
			 *   cookie.
			 * - If `macKey` is not nullptr, an authenticated cookie (with creation
			 *   timestamp and MAC) is generated. Otherwise a plain cookie is
			 *   generated and `creationTimestampMs`/`macKey` are ignored.
			 */
			static StateCookie* Factory(
			  uint8_t* buffer,
			  size_t bufferLength,
			  uint32_t localVerificationTag,
			  uint32_t remoteVerificationTag,
			  uint32_t localInitialTsn,
			  uint32_t remoteInitialTsn,
			  uint32_t remoteAdvertisedReceiverWindowCredit,
			  uint64_t tieTag,
			  const NegotiatedCapabilities& negotiatedCapabilities,
			  uint64_t creationTimestampMs = 0,
			  const uint8_t* macKey        = nullptr,
			  size_t macKeyLength          = 0);

			/**
			 * Serialize a StateCookie (based on given arguments) in the given buffer.
			 *
			 * @remarks
			 * - If `macKey` is not nullptr, an authenticated cookie (with creation
			 *   timestamp and MAC) is generated. Otherwise a plain cookie is
			 *   generated and `creationTimestampMs`/`macKey` are ignored.
			 */
			static void Write(
			  uint8_t* buffer,
			  size_t bufferLength,
			  uint32_t localVerificationTag,
			  uint32_t remoteVerificationTag,
			  uint32_t localInitialTsn,
			  uint32_t remoteInitialTsn,
			  uint32_t remoteAdvertisedReceiverWindowCredit,
			  uint64_t tieTag,
			  const NegotiatedCapabilities& negotiatedCapabilities,
			  uint64_t creationTimestampMs = 0,
			  const uint8_t* macKey        = nullptr,
			  size_t macKeyLength          = 0);

			/**
			 * Whether the given buffer is a StateCookie generated by mediasoup.
			 *
			 * @remarks
			 * - This only checks magic values and length. It does NOT verify the
			 *   MAC of authenticated cookies (which requires the per-association
			 *   secret). Use `VerifyMac()` for that.
			 */
			static bool IsMediasoupStateCookie(const uint8_t* buffer, size_t bufferLength);

			/**
			 * Verify the MAC of an authenticated StateCookie generated by mediasoup.
			 *
			 * @remarks
			 * - Returns false if the buffer is not an authenticated cookie (i.e. its
			 *   length is not `AuthenticatedStateCookieLength`) or if the MAC doesn't
			 *   match the one computed with the given secret key.
			 *
			 * @see RFC 9260 section 5.1.4.
			 */
			static bool VerifyMac(
			  const uint8_t* buffer, size_t bufferLength, const uint8_t* macKey, size_t macKeyLength);

			/**
			 * Determine the SCTP implementation of the generator of State Cookie
			 * given in the buffer.
			 */
			static Types::SctpImplementation DetermineSctpImplementation(
			  const uint8_t* buffer, size_t bufferLength);

		public:
			StateCookie(uint8_t* buffer, size_t bufferLength);

			~StateCookie() override;

			void Dump(int indentation = 0) const final;

			StateCookie* Clone(uint8_t* buffer, size_t bufferLength) const final;

			/**
			 * The value of the Initiate Tag field we put in our INIT or INIT-ACK
			 * chunk. Packets sent by the remote peer must include this value in
			 * their Verification Tag field.
			 */
			uint32_t GetLocalVerificationTag() const
			{
				return Utils::Byte::Get4Bytes(GetBuffer(), 8);
			}

			/**
			 * The value of the Initiate Tag field the peer put in its INIT or
			 * INIT-ACK chunk. Packets sent by us to the peer must include this value
			 * in their Verification Tag field.
			 */
			uint32_t GetRemoteVerificationTag() const
			{
				return Utils::Byte::Get4Bytes(GetBuffer(), 12);
			}

			/**
			 * The value of the Initial TSN field we put in our INIT or INIT-ACK
			 * chunk.
			 */
			uint32_t GetLocalInitialTsn() const
			{
				return Utils::Byte::Get4Bytes(GetBuffer(), 16);
			}

			/**
			 * The value of the Initial TSN field the peer put in its INIT or
			 * INIT-ACK chunk.
			 */
			uint32_t GetRemoteInitialTsn() const
			{
				return Utils::Byte::Get4Bytes(GetBuffer(), 20);
			}

			/**
			 * The value of the Advertised Receiver Window Credit field we put in our
			 * INIT or INIT-ACK chunk.
			 */
			uint32_t GetRemoteAdvertisedReceiverWindowCredit() const
			{
				return Utils::Byte::Get4Bytes(GetBuffer(), 24);
			}

			/**
			 * Tie-Tag used as a nonce when connecting.
			 */
			uint64_t GetTieTag() const
			{
				return Utils::Byte::Get8Bytes(GetBuffer(), 28);
			}

			/**
			 * Negotiated association capabilities.
			 */
			NegotiatedCapabilities GetNegotiatedCapabilities() const;

			/**
			 * Whether this is an authenticated StateCookie (i.e. it carries a
			 * creation timestamp and a MAC).
			 */
			bool IsAuthenticated() const
			{
				return GetLength() == StateCookie::AuthenticatedStateCookieLength;
			}

			/**
			 * The time (in milliseconds) at which this StateCookie was created.
			 *
			 * @remarks
			 * - Only meaningful in authenticated cookies (see `IsAuthenticated()`).
			 */
			uint64_t GetCreationTimestampMs() const
			{
				return Utils::Byte::Get8Bytes(GetBuffer(), StateCookie::TimestampOffset);
			}

		private:
			NegotiatedCapabilitiesField* GetNegotiatedCapabilitiesField() const
			{
				return reinterpret_cast<NegotiatedCapabilitiesField*>(
				  const_cast<uint8_t*>(GetBuffer()) + StateCookie::NegotiatedCapabilitiesOffset);
			}
		};
	} // namespace SCTP
} // namespace RTC

#endif
