#define MS_CLASS "RTC::ICE::IceServer"
// #define MS_LOG_DEV_LEVEL 3

#include "RTC/ICE/IceServer.hpp"
#include "Logger.hpp"
#include <string_view>

namespace RTC
{
	namespace ICE
	{
		/* Static. */

		static constexpr size_t StunResponseFactoryBufferLength{ 65536 };
		thread_local uint8_t StunResponseFactoryBuffer[StunResponseFactoryBufferLength];
		static constexpr size_t MaxTuples{ 8 };
		static constexpr uint8_t ConsentCheckMinTimeoutSec{ 10u };
		static constexpr uint8_t ConsentCheckMaxTimeoutSec{ 60u };

		/* Class variables. */

		// clang-format off
		std::unordered_map<IceServer::IceState, std::string> IceServer::iceStateToString =
		{
			{ IceServer::IceState::NEW,          "new"          },
			{ IceServer::IceState::CONNECTED,    "connected"    },
			{ IceServer::IceState::COMPLETED,    "completed"    },
			{ IceServer::IceState::DISCONNECTED, "disconnected" },
		};
		// clang-format on

		/* Class methods. */

		const std::string& IceServer::IceStateToString(IceState iceState)
		{
			MS_TRACE();

			return IceServer::iceStateToString.at(iceState);
		}

		FBS::WebRtcTransport::IceState IceServer::IceStateToFbs(IceServer::IceState state)
		{
			MS_TRACE();

			switch (state)
			{
				case IceServer::IceState::NEW:
				{
					return FBS::WebRtcTransport::IceState::NEW;
				}

				case IceServer::IceState::CONNECTED:
				{
					return FBS::WebRtcTransport::IceState::CONNECTED;
				}

				case IceServer::IceState::COMPLETED:
				{
					return FBS::WebRtcTransport::IceState::COMPLETED;
				}

				case IceServer::IceState::DISCONNECTED:
				{
					return FBS::WebRtcTransport::IceState::DISCONNECTED;
				}

					NO_DEFAULT_GCC();
			}
		}

		/* Instance methods. */

		IceServer::IceServer(
		  Listener* listener,
		  const std::string& usernameFragment,
		  const std::string& password,
		  uint8_t consentTimeoutSec)
		  : listener(listener), usernameFragment(usernameFragment), password(password)
		{
			MS_TRACE();

			if (consentTimeoutSec == 0u)
			{
				// 0 means disabled so it's a valid value.
			}
			else if (consentTimeoutSec < ConsentCheckMinTimeoutSec)
			{
				MS_WARN_TAG(
				  ice,
				  "consentTimeoutSec cannot be lower than %" PRIu8 " seconds, fixing it",
				  ConsentCheckMinTimeoutSec);

				consentTimeoutSec = ConsentCheckMinTimeoutSec;
			}
			else if (consentTimeoutSec > ConsentCheckMaxTimeoutSec)
			{
				MS_WARN_TAG(
				  ice,
				  "consentTimeoutSec cannot be higher than %" PRIu8 " seconds, fixing it",
				  ConsentCheckMaxTimeoutSec);

				consentTimeoutSec = ConsentCheckMaxTimeoutSec;
			}

			this->consentTimeoutMs = consentTimeoutSec * 1000;

			// Notify the listener.
			this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment);
		}

		IceServer::~IceServer()
		{
			MS_TRACE();

			// Here we must notify the listener about the removal of current
			// usernameFragments (and also the old one if any) and all tuples.

			this->listener->OnIceServerLocalUsernameFragmentRemoved(this, usernameFragment);

			if (!this->oldUsernameFragment.empty())
			{
				this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
			}

			// Clear all tuples.
			this->isRemovingTuples = true;

			for (const auto& it : this->tuples)
			{
				auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));

				// Notify the listener.
				this->listener->OnIceServerTupleRemoved(this, storedTuple);
			}

			this->isRemovingTuples = false;

			// Clear all tuples.
			// NOTE: Do it after notifying the listener since the listener may need to
			// use/read the tuple being removed so we cannot free it yet.
			this->tuples.clear();

			// Unset selected tuple.
			this->selectedTuple = nullptr;

			// Delete the ICE consent check timer.
			delete this->consentCheckTimer;
			this->consentCheckTimer = nullptr;
		}

		void IceServer::Dump(int indentation) const
		{
			MS_TRACE();

			MS_DUMP_CLEAN(indentation, "<IceServer>");
			MS_DUMP_CLEAN(indentation, "  state: %s", IceServer::IceStateToString(this->state).c_str());
			MS_DUMP_CLEAN(indentation, "  tuples:");
			for (const auto& tuple : this->tuples)
			{
				tuple.Dump(indentation + 2);
			}
			if (this->selectedTuple)
			{
				MS_DUMP_CLEAN(indentation, "  selected tuple:");
				this->selectedTuple->Dump(indentation + 2);
			}
			MS_DUMP_CLEAN(indentation, "  consent timeout (ms): %" PRIu16, this->consentTimeoutMs);
			MS_DUMP_CLEAN(indentation, "  remote nomination: %" PRIu32, this->remoteNomination);
			MS_DUMP_CLEAN(indentation, "</IceServer>");
		}

		void IceServer::ProcessStunPacket(const RTC::ICE::StunPacket* packet, RTC::TransportTuple* tuple)
		{
			MS_TRACE();

			switch (packet->GetClass())
			{
				case RTC::ICE::StunPacket::Class::REQUEST:
				{
					ProcessStunRequest(packet, tuple);

					break;
				}

				case RTC::ICE::StunPacket::Class::INDICATION:
				{
					ProcessStunIndication(packet);

					break;
				}

				case RTC::ICE::StunPacket::Class::SUCCESS_RESPONSE:
				case RTC::ICE::StunPacket::Class::ERROR_RESPONSE:
				{
					ProcessStunResponse(packet);

					break;
				}

				default:
				{
					MS_WARN_TAG(
					  ice,
					  "unknown STUN class %" PRIu16 ", discarded",
					  static_cast<uint16_t>(packet->GetClass()));
				}
			}
		}

		void IceServer::RestartIce(const std::string& usernameFragment, const std::string& password)
		{
			MS_TRACE();

			if (!this->oldUsernameFragment.empty())
			{
				this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);
			}

			this->oldUsernameFragment = this->usernameFragment;
			this->usernameFragment    = usernameFragment;

			this->oldPassword = this->password;
			this->password    = password;

			this->remoteNomination = 0u;

			// Notify the listener.
			this->listener->OnIceServerLocalUsernameFragmentAdded(this, usernameFragment);

			// NOTE: Do not call listener->OnIceServerLocalUsernameFragmentRemoved()
			// yet with old usernameFragment. Wait until we receive a STUN packet
			// with the new one.

			// Restart ICE consent check (if running) to give some time to the
			// client to establish ICE again.
			if (IsConsentCheckSupported() && IsConsentCheckRunning())
			{
				RestartConsentCheck();
			}
		}

		bool IceServer::IsValidTuple(const RTC::TransportTuple* tuple) const
		{
			MS_TRACE();

			return HasTuple(tuple) != nullptr;
		}

		void IceServer::RemoveTuple(RTC::TransportTuple* tuple)
		{
			MS_TRACE();

			// If IceServer is removing a tuple or all tuples (for instance in the
			// destructor), the OnIceServerTupleRemoved() callback may end triggering
			// new calls to RemoveTuple(). We must ignore it to avoid double-free issues.
			if (this->isRemovingTuples)
			{
				return;
			}

			RTC::TransportTuple* removedTuple{ nullptr };

			// Find the removed tuple.
			auto it = this->tuples.begin();

			for (; it != this->tuples.end(); ++it)
			{
				RTC::TransportTuple* storedTuple = std::addressof(*it);

				if (storedTuple->Compare(tuple))
				{
					removedTuple = storedTuple;

					break;
				}
			}

			// If not found, ignore.
			if (!removedTuple)
			{
				return;
			}

			// Notify the listener.
			this->isRemovingTuples = true;
			this->listener->OnIceServerTupleRemoved(this, removedTuple);
			this->isRemovingTuples = false;

			// Remove it from the list of tuples.
			// NOTE: Do it after notifying the listener since the listener may need to
			// use/read the tuple being removed so we cannot free it yet.
			this->tuples.erase(it);

			// If this is the selected tuple, do things.
			if (removedTuple == this->selectedTuple)
			{
				this->selectedTuple = nullptr;

				// Mark the first tuple as selected tuple (if any) but only if state was
				// 'connected' or 'completed'.
				if (
				  (this->state == IceState::CONNECTED || this->state == IceState::COMPLETED) &&
				  this->tuples.begin() != this->tuples.end())
				{
					SetSelectedTuple(std::addressof(*this->tuples.begin()));

					// Restart ICE consent check to let the client send new consent requests
					// on the new selected tuple.
					if (IsConsentCheckSupported())
					{
						RestartConsentCheck();
					}
				}
				// Or just emit 'disconnected'.
				else
				{
					// Update state.
					this->state = IceState::DISCONNECTED;

					// Reset remote nomination.
					this->remoteNomination = 0u;

					// Notify the listener.
					this->listener->OnIceServerDisconnected(this);

					if (IsConsentCheckSupported() && IsConsentCheckRunning())
					{
						StopConsentCheck();
					}
				}
			}
		}

		void IceServer::ProcessStunRequest(const RTC::ICE::StunPacket* request, RTC::TransportTuple* tuple)
		{
			MS_TRACE();

			MS_DEBUG_DEV("processing STUN request");

			// Must be a Binding method.
			if (request->GetMethod() != RTC::ICE::StunPacket::Method::BINDING)
			{
				MS_WARN_TAG(
				  ice,
				  "STUN request with unknown method %#.3x => 400",
				  static_cast<unsigned int>(request->GetMethod()));

				// Reply 400.
				auto* response = request->CreateErrorResponse(
				  StunResponseFactoryBuffer, sizeof(StunResponseFactoryBuffer), 400, "unknown method");

				response->Protect();

				this->listener->OnIceServerSendStunPacket(this, response, tuple);

				delete response;

				return;
			}

			// Must have FINGERPRINT attribute.
			if (!request->HasAttribute(StunPacket::AttributeType::FINGERPRINT))
			{
				MS_WARN_TAG(ice, "STUN Binding request without FINGERPRINT attribute => 400");

				// Reply 400.
				auto* response = request->CreateErrorResponse(
				  StunResponseFactoryBuffer,
				  sizeof(StunResponseFactoryBuffer),
				  400,
				  "missing FINGERPRINT attribute in STUN Binding request");

				response->Protect();

				this->listener->OnIceServerSendStunPacket(this, response, tuple);

				delete response;

				return;
			}

			// PRIORITY attribute is required.
			if (!request->HasAttribute(StunPacket::AttributeType::PRIORITY))
			{
				MS_WARN_TAG(ice, "STUN Binding request without PRIORITY attribute => 400");

				// Reply 400.
				auto* response = request->CreateErrorResponse(
				  StunResponseFactoryBuffer,
				  sizeof(StunResponseFactoryBuffer),
				  400,
				  "missing PRIORITY attribute in STUN Binding request");

				response->Protect();

				this->listener->OnIceServerSendStunPacket(this, response, tuple);

				delete response;

				return;
			}

			// Check authentication.
			switch (request->CheckAuthentication(this->usernameFragment, this->password))
			{
				case RTC::ICE::StunPacket::AuthenticationResult::OK:
				{
					if (!this->oldUsernameFragment.empty() && !this->oldPassword.empty())
					{
						MS_DEBUG_TAG(ice, "new ICE credentials applied");

						// Notify the listener.
						this->listener->OnIceServerLocalUsernameFragmentRemoved(this, this->oldUsernameFragment);

						this->oldUsernameFragment.clear();
						this->oldPassword.clear();
					}

					break;
				}

				case RTC::ICE::StunPacket::AuthenticationResult::UNAUTHORIZED:
				{
					// We may have changed our usernameFragment and password, so check the
					// old ones.
					if (
					  !this->oldUsernameFragment.empty() && !this->oldPassword.empty() &&
					  request->CheckAuthentication(this->oldUsernameFragment, this->oldPassword) ==
					    RTC::ICE::StunPacket::AuthenticationResult::OK)
					{
						MS_DEBUG_TAG(ice, "using old ICE credentials");

						break;
					}

					MS_WARN_TAG(ice, "wrong authentication in STUN Binding request => 401");

					// Reply 401.
					auto* response = request->CreateErrorResponse(
					  StunResponseFactoryBuffer,
					  sizeof(StunResponseFactoryBuffer),
					  401,
					  "wrong authentication in STUN Binding request");

					response->Protect();

					this->listener->OnIceServerSendStunPacket(this, response, tuple);

					delete response;

					return;
				}

				case RTC::ICE::StunPacket::AuthenticationResult::BAD_MESSAGE:
				{
					MS_WARN_TAG(ice, "cannot check authentication in STUN Binding request => 400");

					// Reply 400.
					auto* response = request->CreateErrorResponse(
					  StunResponseFactoryBuffer,
					  sizeof(StunResponseFactoryBuffer),
					  400,
					  "cannot check authentication in STUN Binding request");

					response->Protect();

					this->listener->OnIceServerSendStunPacket(this, response, tuple);

					delete response;

					return;
				}
			}

			// The remote peer must be ICE controlling.
			if (request->GetIceControlled())
			{
				MS_WARN_TAG(ice, "peer indicates ICE-CONTROLLED in STUN Binding request => 487");

				// Reply 487 (Role Conflict).
				auto* response = request->CreateErrorResponse(
				  StunResponseFactoryBuffer,
				  sizeof(StunResponseFactoryBuffer),
				  487,
				  "invalid ICE-CONTROLLED attribute in STUN Binding request");

				response->Protect();

				this->listener->OnIceServerSendStunPacket(this, response, tuple);

				delete response;

				return;
			}

			MS_DEBUG_DEV(
			  "valid STUN Binding request [priority:%" PRIu32 ", useCandidate:%s]",
			  static_cast<uint32_t>(request->GetPriority()),
			  request->HasAttribute(RTC::ICE::StunPacket::AttributeType::USE_CANDIDATE) ? "true" : "false");

			// Create a success response.
			auto* response =
			  request->CreateSuccessResponse(StunResponseFactoryBuffer, sizeof(StunResponseFactoryBuffer));

			// Add XOR-MAPPED-ADDRESS.
			response->AddXorMappedAddress(tuple->GetRemoteAddress());

			if (this->oldPassword.empty())
			{
				response->Protect(this->password);
			}
			else
			{
				response->Protect(this->oldPassword);
			}

			this->listener->OnIceServerSendStunPacket(this, response, tuple);

			delete response;

			// Handle the tuple.
			HandleTuple(
			  tuple,
			  request->HasAttribute(StunPacket::AttributeType::USE_CANDIDATE),
			  request->HasAttribute(StunPacket::AttributeType::NOMINATION),
			  request->GetNomination());

			// If state is 'connected' or 'completed' after handling the tuple, then
			// start or restart ICE consent check (if supported).
			if (IsConsentCheckSupported() && (this->state == IceState::CONNECTED || this->state == IceState::COMPLETED))
			{
				if (IsConsentCheckRunning())
				{
					RestartConsentCheck();
				}
				else
				{
					StartConsentCheck();
				}
			}
		}

		void IceServer::ProcessStunIndication(const RTC::ICE::StunPacket* /*indication*/)
		{
			MS_TRACE();

			MS_DEBUG_DEV("STUN indication received, ignored");

			// Nothig else to do. We just ignore STUN indications.
		}

		void IceServer::ProcessStunResponse(const RTC::ICE::StunPacket* response)
		{
			MS_TRACE();

			if (response->GetClass() == RTC::ICE::StunPacket::Class::SUCCESS_RESPONSE)
			{
				MS_DEBUG_DEV("ignoring received STUN successs response", responseType.c_str());
			}
			else
			{
				thread_local std::string_view errorReasonPhrase;

				response->GetErrorCode(errorReasonPhrase);

				MS_DEBUG_DEV("ignoring received STUN error response [errorCode:%" PRIu16 ", reasonPhrase:\"%.*s\""], static_cast<int>(errorReasonPhrase.size()), errorReasonPhrase.data());
			}

			// Nothig else to do. We just ignore STUN responses because we do not
			// generate STUN requests.
		}

		void IceServer::MayForceSelectedTuple(const RTC::TransportTuple* tuple)
		{
			MS_TRACE();

			if (this->state != IceState::CONNECTED && this->state != IceState::COMPLETED)
			{
				MS_WARN_TAG(ice, "cannot force selected tuple if not in state 'connected' or 'completed'");

				return;
			}

			auto* storedTuple = HasTuple(tuple);

			if (!storedTuple)
			{
				MS_WARN_TAG(ice, "cannot force selected tuple if the given tuple was not already a valid one");

				return;
			}

			SetSelectedTuple(storedTuple);
		}

		void IceServer::HandleTuple(
		  RTC::TransportTuple* tuple, bool hasUseCandidate, bool hasNomination, uint32_t nomination)
		{
			MS_TRACE();

			switch (this->state)
			{
				case IceState::NEW:
				{
					// There shouldn't be a selected tuple.
					MS_ASSERT(!this->selectedTuple, "state is 'new' but there is selected tuple");

					if (!hasUseCandidate && !hasNomination)
					{
						MS_DEBUG_TAG(
						  ice,
						  "transition from state 'new' to 'connected' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
						  "]",
						  hasUseCandidate ? "true" : "false",
						  hasNomination ? "true" : "false",
						  nomination);

						// Store the tuple.
						auto* storedTuple = AddTuple(tuple);

						// Update state.
						this->state = IceState::CONNECTED;

						// Mark it as selected tuple.
						SetSelectedTuple(storedTuple);

						// Notify the listener.
						this->listener->OnIceServerConnected(this);
					}
					else
					{
						// Store the tuple.
						auto* storedTuple          = AddTuple(tuple);
						const auto isNewNomination = hasNomination && nomination > this->remoteNomination;

						if (isNewNomination || !hasNomination)
						{
							MS_DEBUG_TAG(
							  ice,
							  "transition from state 'new' to 'completed' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
							  "]",
							  hasUseCandidate ? "true" : "false",
							  hasNomination ? "true" : "false",
							  nomination);

							// Update state.
							this->state = IceState::COMPLETED;

							// Mark it as selected tuple.
							SetSelectedTuple(storedTuple);

							// Update nomination.
							if (isNewNomination)
							{
								this->remoteNomination = nomination;
							}

							// Notify the listener.
							this->listener->OnIceServerCompleted(this);
						}
					}

					break;
				}

				case IceState::DISCONNECTED:
				{
					// There shouldn't be a selected tuple.
					MS_ASSERT(!this->selectedTuple, "state is 'disconnected' but there is selected tuple");

					if (!hasUseCandidate && !hasNomination)
					{
						MS_DEBUG_TAG(
						  ice,
						  "transition from state 'disconnected' to 'connected' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
						  "]",
						  hasUseCandidate ? "true" : "false",
						  hasNomination ? "true" : "false",
						  nomination);

						// Store the tuple.
						auto* storedTuple = AddTuple(tuple);

						// Update state.
						this->state = IceState::CONNECTED;

						// Mark it as selected tuple.
						SetSelectedTuple(storedTuple);

						// Notify the listener.
						this->listener->OnIceServerConnected(this);
					}
					else
					{
						// Store the tuple.
						auto* storedTuple          = AddTuple(tuple);
						const auto isNewNomination = hasNomination && nomination > this->remoteNomination;

						if (isNewNomination || !hasNomination)
						{
							MS_DEBUG_TAG(
							  ice,
							  "transition from state 'disconnected' to 'completed' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
							  "]",
							  hasUseCandidate ? "true" : "false",
							  hasNomination ? "true" : "false",
							  nomination);

							// Update state.
							this->state = IceState::COMPLETED;

							// Mark it as selected tuple.
							SetSelectedTuple(storedTuple);

							// Update nomination.
							if (isNewNomination)
							{
								this->remoteNomination = nomination;
							}

							// Notify the listener.
							this->listener->OnIceServerCompleted(this);
						}
					}

					break;
				}

				case IceState::CONNECTED:
				{
					// There should be some tuples.
					MS_ASSERT(!this->tuples.empty(), "state is 'connected' but there are no tuples");

					// There should be a selected tuple.
					MS_ASSERT(this->selectedTuple, "state is 'connected' but there is not selected tuple");

					if (!hasUseCandidate && !hasNomination)
					{
						// Store the tuple.
						AddTuple(tuple);
					}
					else
					{
						MS_DEBUG_TAG(
						  ice,
						  "transition from state 'connected' to 'completed' [hasUseCandidate:%s, hasNomination:%s, nomination:%" PRIu32
						  "]",
						  hasUseCandidate ? "true" : "false",
						  hasNomination ? "true" : "false",
						  nomination);

						// Store the tuple.
						auto* storedTuple          = AddTuple(tuple);
						const auto isNewNomination = hasNomination && nomination > this->remoteNomination;

						if (isNewNomination || !hasNomination)
						{
							// Update state.
							this->state = IceState::COMPLETED;

							// Mark it as selected tuple.
							SetSelectedTuple(storedTuple);

							// Update nomination.
							if (isNewNomination)
							{
								this->remoteNomination = nomination;
							}

							// Notify the listener.
							this->listener->OnIceServerCompleted(this);
						}
					}

					break;
				}

				case IceState::COMPLETED:
				{
					// There should be some tuples.
					MS_ASSERT(!this->tuples.empty(), "state is 'completed' but there are no tuples");

					// There should be a selected tuple.
					MS_ASSERT(this->selectedTuple, "state is 'completed' but there is not selected tuple");

					if (!hasUseCandidate && !hasNomination)
					{
						// Store the tuple.
						AddTuple(tuple);
					}
					else
					{
						// Store the tuple.
						auto* storedTuple          = AddTuple(tuple);
						const auto isNewNomination = hasNomination && nomination > this->remoteNomination;

						// When in completed state, update selected tuple if there is ICE
						// nomination or useCandidate.
						if (isNewNomination || hasUseCandidate)
						{
							// Mark it as selected tuple.
							SetSelectedTuple(storedTuple);

							// Update nomination.
							if (isNewNomination)
							{
								this->remoteNomination = nomination;
							}
						}
					}

					break;
				}
			}
		}

		RTC::TransportTuple* IceServer::AddTuple(RTC::TransportTuple* tuple)
		{
			MS_TRACE();

			auto* storedTuple = HasTuple(tuple);

			if (storedTuple)
			{
				MS_DEBUG_DEV("tuple already exists");

				return storedTuple;
			}

			// Add the new tuple at the beginning of the list.
			this->tuples.push_front(*tuple);

			storedTuple = std::addressof(*this->tuples.begin());

			// If it is UDP then we must store the remote address (until now it is
			// just a pointer that will be freed soon).
			if (storedTuple->GetProtocol() == TransportTuple::Protocol::UDP)
			{
				storedTuple->StoreUdpRemoteAddress();
			}

			// Notify the listener.
			this->listener->OnIceServerTupleAdded(this, storedTuple);

			// Don't allow more than MaxTuples.
			if (this->tuples.size() > MaxTuples)
			{
				MS_WARN_TAG(ice, "too too many tuples, removing the oldest non selected one");

				// Find the oldest tuple which is neither the added one nor the selected
				// one (if any), and remove it.
				RTC::TransportTuple* removedTuple{ nullptr };
				auto it = this->tuples.rbegin();

				for (; it != this->tuples.rend(); ++it)
				{
					RTC::TransportTuple* otherStoredTuple = std::addressof(*it);

					if (otherStoredTuple != storedTuple && otherStoredTuple != this->selectedTuple)
					{
						removedTuple = otherStoredTuple;

						break;
					}
				}

				// This should not happen by design.
				MS_ASSERT(removedTuple, "couldn't find any tuple to be removed");

				// Notify the listener.
				this->isRemovingTuples = true;
				this->listener->OnIceServerTupleRemoved(this, removedTuple);
				this->isRemovingTuples = false;

				// Remove it from the list of tuples.
				// NOTE: Do it after notifying the listener since the listener may need to
				// use/read the tuple being removed so we cannot free it yet.
				// NOTE: This trick is needed since it is a reverse_iterator and
				// erase() requires a iterator, const_iterator or bidirectional_iterator.
				this->tuples.erase(std::next(it).base());
			}

			// Return the address of the inserted tuple.
			return storedTuple;
		}

		RTC::TransportTuple* IceServer::HasTuple(const RTC::TransportTuple* tuple) const
		{
			MS_TRACE();

			// Check the current selected tuple (if any).
			if (this->selectedTuple && this->selectedTuple->Compare(tuple))
			{
				return this->selectedTuple;
			}

			// Otherwise check other stored tuples.
			for (const auto& it : this->tuples)
			{
				auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));

				if (storedTuple->Compare(tuple))
				{
					return storedTuple;
				}
			}

			return nullptr;
		}

		void IceServer::SetSelectedTuple(RTC::TransportTuple* storedTuple)
		{
			MS_TRACE();

			// If already the selected tuple do nothing.
			if (storedTuple == this->selectedTuple)
			{
				return;
			}

			this->selectedTuple = storedTuple;

			// Notify the listener.
			this->listener->OnIceServerSelectedTuple(this, this->selectedTuple);
		}

		void IceServer::StartConsentCheck()
		{
			MS_TRACE();

			MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");
			MS_ASSERT(!IsConsentCheckRunning(), "ICE consent check already running");
			MS_ASSERT(this->selectedTuple, "no selected tuple");

			// Create the ICE consent check timer if it doesn't exist.
			if (!this->consentCheckTimer)
			{
				this->consentCheckTimer = new TimerHandle(this);
			}

			this->consentCheckTimer->Start(this->consentTimeoutMs);
		}

		void IceServer::RestartConsentCheck()
		{
			MS_TRACE();

			MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");
			MS_ASSERT(IsConsentCheckRunning(), "ICE consent check not running");
			MS_ASSERT(this->selectedTuple, "no selected tuple");

			this->consentCheckTimer->Restart();
		}

		void IceServer::StopConsentCheck()
		{
			MS_TRACE();

			MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");
			MS_ASSERT(IsConsentCheckRunning(), "ICE consent check not running");

			this->consentCheckTimer->Stop();
		}

		inline void IceServer::OnTimer(TimerHandle* timer)
		{
			MS_TRACE();

			if (timer == this->consentCheckTimer)
			{
				MS_ASSERT(IsConsentCheckSupported(), "ICE consent check not supported");

				// State must be 'connected' or 'completed'.
				MS_ASSERT(
				  this->state == IceState::COMPLETED || this->state == IceState::CONNECTED,
				  "ICE consent check timer fired but state is neither 'completed' nor 'connected'");

				// There should be a selected tuple.
				MS_ASSERT(
				  this->selectedTuple, "ICE consent check timer fired but there is not selected tuple");

				MS_WARN_TAG(ice, "ICE consent expired due to timeout, moving to 'disconnected' state");

				// Update state.
				this->state = IceState::DISCONNECTED;

				// Reset remote nomination.
				this->remoteNomination = 0u;

				// Clear all tuples.
				this->isRemovingTuples = true;

				for (const auto& it : this->tuples)
				{
					auto* storedTuple = const_cast<RTC::TransportTuple*>(std::addressof(it));

					// Notify the listener.
					this->listener->OnIceServerTupleRemoved(this, storedTuple);
				}

				this->isRemovingTuples = false;

				// Clear all tuples.
				// NOTE: Do it after notifying the listener since the listener may need to
				// use/read the tuple being removed so we cannot free it yet.
				this->tuples.clear();

				// Unset selected tuple.
				this->selectedTuple = nullptr;

				// Notify the listener.
				this->listener->OnIceServerDisconnected(this);
			}
		}
	} // namespace ICE
} // namespace RTC
