Router::Router(const std::string& id) : id(id)
	{
		MS_TRACE();
	}

#define MS_CLASS "RTC::Router"

#include "RTC/Router.hpp"
#include "Logger.hpp"
#include "MediaSoupErrors.hpp"
#include "Utils.hpp"
#include "RTC/ActiveSpeakerObserver.hpp"
#include "RTC/AudioLevelObserver.hpp"
#include "RTC/DirectTransport.hpp"
#include "RTC/PipeTransport.hpp"
#include "RTC/PlainTransport.hpp"
#include "RTC/WebRtcTransport.hpp"

namespace RTC
{
	/* Instance methods. */

	Router::~Router()
	{
		MS_TRACE();

		// Close all Transports.
		for (auto& kv : this->mapTransports)
		{
			auto* transport = kv.second;

			delete transport;
		}
		this->mapTransports.clear();

		// Close all RtpObservers.
		for (auto& kv : this->mapRtpObservers)
		{
			auto* rtpObserver = kv.second;

			delete rtpObserver;
		}
		this->mapRtpObservers.clear();

		// Clear other maps.
		this->mapProducerConsumers.clear();
		this->mapConsumerProducer.clear();
		this->mapProducerRtpObservers.clear();
		this->mapProducers.clear();
		this->mapDataProducerDataConsumers.clear();
		this->mapDataConsumerDataProducer.clear();
		this->mapDataProducers.clear();
	}

	void Router::FillJson(json& jsonObject) const
	{
		MS_TRACE();

		// Add id.
		jsonObject["id"] = this->id;

		// Add transportIds.
		jsonObject["transportIds"] = json::array();
		auto jsonTransportIdsIt    = jsonObject.find("transportIds");

		for (const auto& kv : this->mapTransports)
		{
			const auto& transportId = kv.first;

			jsonTransportIdsIt->emplace_back(transportId);
		}

		// Add rtpObserverIds.
		jsonObject["rtpObserverIds"] = json::array();
		auto jsonRtpObserverIdsIt    = jsonObject.find("rtpObserverIds");

		for (const auto& kv : this->mapRtpObservers)
		{
			const auto& rtpObserverId = kv.first;

			jsonRtpObserverIdsIt->emplace_back(rtpObserverId);
		}

		// Add mapProducerIdConsumerIds.
		jsonObject["mapProducerIdConsumerIds"] = json::object();
		auto jsonMapProducerConsumersIt        = jsonObject.find("mapProducerIdConsumerIds");

		for (const auto& kv : this->mapProducerConsumers)
		{
			auto* producer        = kv.first;
			const auto& consumers = kv.second;

			(*jsonMapProducerConsumersIt)[producer->id] = json::array();
			auto jsonProducerIdIt                       = jsonMapProducerConsumersIt->find(producer->id);

			for (auto* consumer : consumers)
			{
				jsonProducerIdIt->emplace_back(consumer->id);
			}
		}

		// Add mapConsumerIdProducerId.
		jsonObject["mapConsumerIdProducerId"] = json::object();
		auto jsonMapConsumerProducerIt        = jsonObject.find("mapConsumerIdProducerId");

		for (const auto& kv : this->mapConsumerProducer)
		{
			auto* consumer = kv.first;
			auto* producer = kv.second;

			(*jsonMapConsumerProducerIt)[consumer->id] = producer->id;
		}

		// Add mapProducerIdObserverIds.
		jsonObject["mapProducerIdObserverIds"] = json::object();
		auto jsonMapProducerRtpObserversIt     = jsonObject.find("mapProducerIdObserverIds");

		for (const auto& kv : this->mapProducerRtpObservers)
		{
			auto* producer           = kv.first;
			const auto& rtpObservers = kv.second;

			(*jsonMapProducerRtpObserversIt)[producer->id] = json::array();
			auto jsonProducerIdIt = jsonMapProducerRtpObserversIt->find(producer->id);

			for (auto* rtpObserver : rtpObservers)
			{
				jsonProducerIdIt->emplace_back(rtpObserver->id);
			}
		}

		// Add mapDataProducerIdDataConsumerIds.
		jsonObject["mapDataProducerIdDataConsumerIds"] = json::object();
		auto jsonMapDataProducerDataConsumersIt = jsonObject.find("mapDataProducerIdDataConsumerIds");

		for (const auto& kv : this->mapDataProducerDataConsumers)
		{
			auto* dataProducer        = kv.first;
			const auto& dataConsumers = kv.second;

			(*jsonMapDataProducerDataConsumersIt)[dataProducer->id] = json::array();
			auto jsonDataProducerIdIt = jsonMapDataProducerDataConsumersIt->find(dataProducer->id);

			for (auto* dataConsumer : dataConsumers)
			{
				jsonDataProducerIdIt->emplace_back(dataConsumer->id);
			}
		}

		// Add mapDataConsumerIdDataProducerId.
		jsonObject["mapDataConsumerIdDataProducerId"] = json::object();
		auto jsonMapDataConsumerDataProducerIt = jsonObject.find("mapDataConsumerIdDataProducerId");

		for (const auto& kv : this->mapDataConsumerDataProducer)
		{
			auto* dataConsumer = kv.first;
			auto* dataProducer = kv.second;

			(*jsonMapDataConsumerDataProducerIt)[dataConsumer->id] = dataProducer->id;
		}
	}

	void Router::HandleRequest(Channel::ChannelRequest* request)
	{
		MS_TRACE();

		switch (request->methodId)
		{
			case Channel::ChannelRequest::MethodId::ROUTER_DUMP:
			{
				json data = json::object();

				FillJson(data);

				request->Accept(data);

				break;
			}

			case Channel::ChannelRequest::MethodId::ROUTER_CREATE_WEBRTC_TRANSPORT:
			{
				std::string transportId;

				SetNewTransportIdFromInternal(request->internal, transportId);

				auto* webRtcTransport = new RTC::WebRtcTransport(transportId, this, request->data);

				// Insert into the map.
				this->mapTransports[transportId] = webRtcTransport;

				MS_DEBUG_DEV("WebRtcTransport created [transportId:%s]", transportId.c_str());

				json data = json::object();

				webRtcTransport->FillJson(data);

				request->Accept(data);

				break;
			}

			case Channel::ChannelRequest::MethodId::ROUTER_CREATE_PLAIN_TRANSPORT:
			{
				std::string transportId;

				// This may throw
				SetNewTransportIdFromInternal(request->internal, transportId);

				auto* plainTransport = new RTC::PlainTransport(transportId, this, request->data);

				// Insert into the map.
				this->mapTransports[transportId] = plainTransport;

				MS_DEBUG_DEV("PlainTransport created [transportId:%s]", transportId.c_str());

				json data = json::object();

				plainTransport->FillJson(data);

				request->Accept(data);

				break;
			}

			case Channel::ChannelRequest::MethodId::ROUTER_CREATE_PIPE_TRANSPORT:
			{
				std::string transportId;

				// This may throw
				SetNewTransportIdFromInternal(request->internal, transportId);

				auto* pipeTransport = new RTC::PipeTransport(transportId, this, request->data);

				// Insert into the map.
				this->mapTransports[transportId] = pipeTransport;

				MS_DEBUG_DEV("PipeTransport created [transportId:%s]", transportId.c_str());

				json data = json::object();

				pipeTransport->FillJson(data);

				request->Accept(data);

				break;
			}

			case Channel::ChannelRequest::MethodId::ROUTER_CREATE_DIRECT_TRANSPORT:
			{
				std::string transportId;

				// This may throw
				SetNewTransportIdFromInternal(request->internal, transportId);

				auto* directTransport = new RTC::DirectTransport(transportId, this, request->data);

				// Insert into the map.
				this->mapTransports[transportId] = directTransport;

				MS_DEBUG_DEV("DirectTransport created [transportId:%s]", transportId.c_str());

				json data = json::object();

				directTransport->FillJson(data);

				request->Accept(data);

				break;
			}

			case Channel::ChannelRequest::MethodId::ROUTER_CREATE_ACTIVE_SPEAKER_OBSERVER:
			{
				std::string rtpObserverId;

				SetNewRtpObserverIdFromInternal(request->internal, rtpObserverId);

				auto* activeSpeakerObserver = new RTC::ActiveSpeakerObserver(rtpObserverId, request->data);

				// Insert into the map.
				this->mapRtpObservers[rtpObserverId] = activeSpeakerObserver;

				MS_DEBUG_DEV("ActiveSpeakerObserver created [rtpObserverId:%s]", rtpObserverId.c_str());

				request->Accept();

				break;
			}

			case Channel::ChannelRequest::MethodId::ROUTER_CREATE_AUDIO_LEVEL_OBSERVER:
			{
				std::string rtpObserverId;

				// This may throw
				SetNewRtpObserverIdFromInternal(request->internal, rtpObserverId);

				auto* audioLevelObserver = new RTC::AudioLevelObserver(rtpObserverId, request->data);

				// Insert into the map.
				this->mapRtpObservers[rtpObserverId] = audioLevelObserver;

				MS_DEBUG_DEV("AudioLevelObserver created [rtpObserverId:%s]", rtpObserverId.c_str());

				request->Accept();

				break;
			}

			case Channel::ChannelRequest::MethodId::TRANSPORT_CLOSE:
			{
				RTC::Transport* transport = GetTransportFromInternal(request->internal);

				// Tell the Transport to close all its Producers and Consumers so it will
				// notify us about their closures.
				transport->CloseProducersAndConsumers();

				// Remove it from the map and delete it.
				this->mapTransports.erase(transport->id);

				MS_DEBUG_DEV("Transport closed [transportId:%s]", transport->id.c_str());

				// Delete it.
				delete transport;

				request->Accept();

				break;
			}

			case Channel::ChannelRequest::MethodId::RTP_OBSERVER_CLOSE:
			{
				RTC::RtpObserver* rtpObserver = GetRtpObserverFromInternal(request->internal);

				// Remove it from the map.
				this->mapRtpObservers.erase(rtpObserver->id);

				// Iterate all entries in mapProducerRtpObservers and remove the closed one.
				for (auto& kv : this->mapProducerRtpObservers)
				{
					auto& rtpObservers = kv.second;

					rtpObservers.erase(rtpObserver);
				}

				MS_DEBUG_DEV("RtpObserver closed [rtpObserverId:%s]", rtpObserver->id.c_str());

				// Delete it.
				delete rtpObserver;

				request->Accept();

				break;
			}

			case Channel::ChannelRequest::MethodId::RTP_OBSERVER_PAUSE:
			{
				RTC::RtpObserver* rtpObserver = GetRtpObserverFromInternal(request->internal);

				rtpObserver->Pause();

				request->Accept();

				break;
			}

			case Channel::ChannelRequest::MethodId::RTP_OBSERVER_RESUME:
			{
				RTC::RtpObserver* rtpObserver = GetRtpObserverFromInternal(request->internal);

				rtpObserver->Resume();

				request->Accept();

				break;
			}

			case Channel::ChannelRequest::MethodId::RTP_OBSERVER_ADD_PRODUCER:
			{
				RTC::RtpObserver* rtpObserver = GetRtpObserverFromInternal(request->internal);
				RTC::Producer* producer       = GetProducerFromData(request->data);

				rtpObserver->AddProducer(producer);

				// Add to the map.
				this->mapProducerRtpObservers[producer].insert(rtpObserver);

				request->Accept();

				break;
			}

			case Channel::ChannelRequest::MethodId::RTP_OBSERVER_REMOVE_PRODUCER:
			{
				RTC::RtpObserver* rtpObserver = GetRtpObserverFromInternal(request->internal);
				RTC::Producer* producer       = GetProducerFromData(request->data);

				rtpObserver->RemoveProducer(producer);

				// Remove from the map.
				this->mapProducerRtpObservers[producer].erase(rtpObserver);

				request->Accept();

				break;
			}

			// Any other request must be delivered to the corresponding Transport.
			default:
			{
				RTC::Transport* transport = GetTransportFromInternal(request->internal);

				transport->HandleRequest(request);

				break;
			}
		}
	}

	void Router::HandleRequest(PayloadChannel::PayloadChannelRequest* request)
	{
		MS_TRACE();

		RTC::Transport* transport = GetTransportFromInternal(request->internal);

		transport->HandleRequest(request);
	}

	void Router::HandleNotification(PayloadChannel::Notification* notification)
	{
		MS_TRACE();

		RTC::Transport* transport = GetTransportFromInternal(notification->internal);

		transport->HandleNotification(notification);
	}

	void Router::SetNewTransportIdFromInternal(json& internal, std::string& transportId) const
	{
		MS_TRACE();

		auto jsonTransportIdIt = internal.find("transportId");

		if (jsonTransportIdIt == internal.end() || !jsonTransportIdIt->is_string())
			MS_THROW_ERROR("missing internal.transportId");

		transportId.assign(jsonTransportIdIt->get<std::string>());

		if (this->mapTransports.find(transportId) != this->mapTransports.end())
			MS_THROW_ERROR("a Transport with same transportId already exists");
	}

	RTC::Transport* Router::GetTransportFromInternal(json& internal) const
	{
		MS_TRACE();

		auto jsonTransportIdIt = internal.find("transportId");

		if (jsonTransportIdIt == internal.end() || !jsonTransportIdIt->is_string())
			MS_THROW_ERROR("missing internal.transportId");

		auto it = this->mapTransports.find(jsonTransportIdIt->get<std::string>());

		if (it == this->mapTransports.end())
			MS_THROW_ERROR("Transport not found");

		RTC::Transport* transport = it->second;

		return transport;
	}

	void Router::SetNewRtpObserverIdFromInternal(json& internal, std::string& rtpObserverId) const
	{
		MS_TRACE();

		auto jsonRtpObserverIdIt = internal.find("rtpObserverId");

		if (jsonRtpObserverIdIt == internal.end() || !jsonRtpObserverIdIt->is_string())
			MS_THROW_ERROR("missing internal.rtpObserverId");

		rtpObserverId.assign(jsonRtpObserverIdIt->get<std::string>());

		if (this->mapRtpObservers.find(rtpObserverId) != this->mapRtpObservers.end())
			MS_THROW_ERROR("an RtpObserver with same rtpObserverId already exists");
	}

	RTC::RtpObserver* Router::GetRtpObserverFromInternal(json& internal) const
	{
		MS_TRACE();

		auto jsonRtpObserverIdIt = internal.find("rtpObserverId");

		if (jsonRtpObserverIdIt == internal.end() || !jsonRtpObserverIdIt->is_string())
			MS_THROW_ERROR("missing internal.rtpObserverId");

		auto it = this->mapRtpObservers.find(jsonRtpObserverIdIt->get<std::string>());

		if (it == this->mapRtpObservers.end())
			MS_THROW_ERROR("RtpObserver not found");

		RTC::RtpObserver* rtpObserver = it->second;

		return rtpObserver;
	}

	RTC::Producer* Router::GetProducerFromData(json& data) const
	{
		MS_TRACE();

		auto jsonProducerIdIt = data.find("producerId");

		if (jsonProducerIdIt == data.end() || !jsonProducerIdIt->is_string())
			MS_THROW_ERROR("missing data.producerId");

		auto it = this->mapProducers.find(jsonProducerIdIt->get<std::string>());

		if (it == this->mapProducers.end())
			MS_THROW_ERROR("Producer not found");

		RTC::Producer* producer = it->second;

		return producer;
	}

	inline void Router::OnTransportNewProducer(RTC::Transport* /*transport*/, RTC::Producer* producer)
	{
		MS_TRACE();

		MS_ASSERT(
		  this->mapProducerConsumers.find(producer) == this->mapProducerConsumers.end(),
		  "Producer already present in mapProducerConsumers");

		if (this->mapProducers.find(producer->id) != this->mapProducers.end())
		{
			MS_THROW_ERROR("Producer already present in mapProducers [producerId:%s]", producer->id.c_str());
		}

		// Insert the Producer in the maps.
		this->mapProducers[producer->id] = producer;
		this->mapProducerConsumers[producer];
		this->mapProducerRtpObservers[producer];
	}

	inline void Router::OnTransportProducerClosed(RTC::Transport* /*transport*/, RTC::Producer* producer)
	{
		MS_TRACE();

		auto mapProducerConsumersIt    = this->mapProducerConsumers.find(producer);
		auto mapProducersIt            = this->mapProducers.find(producer->id);
		auto mapProducerRtpObserversIt = this->mapProducerRtpObservers.find(producer);

		MS_ASSERT(
		  mapProducerConsumersIt != this->mapProducerConsumers.end(),
		  "Producer not present in mapProducerConsumers");
		MS_ASSERT(mapProducersIt != this->mapProducers.end(), "Producer not present in mapProducers");
		MS_ASSERT(
		  mapProducerRtpObserversIt != this->mapProducerRtpObservers.end(),
		  "Producer not present in mapProducerRtpObservers");

		// Close all Consumers associated to the closed Producer.
		auto& consumers = mapProducerConsumersIt->second;

		// NOTE: While iterating the set of Consumers, we call ProducerClosed() on each
		// one, which will end calling Router::OnTransportConsumerProducerClosed(),
		// which will remove the Consumer from mapConsumerProducer but won't remove the
		// closed Consumer from the set of Consumers in mapProducerConsumers (here will
		// erase the complete entry in that map).
		for (auto* consumer : consumers)
		{
			// Call consumer->ProducerClosed() so the Consumer will notify the Node process,
			// will notify its Transport, and its Transport will delete the Consumer.
			consumer->ProducerClosed();
		}

		// Tell all RtpObservers that the Producer has been closed.
		auto& rtpObservers = mapProducerRtpObserversIt->second;

		for (auto* rtpObserver : rtpObservers)
		{
			rtpObserver->RemoveProducer(producer);
		}

		// Remove the Producer from the maps.
		this->mapProducers.erase(mapProducersIt);
		this->mapProducerConsumers.erase(mapProducerConsumersIt);
		this->mapProducerRtpObservers.erase(mapProducerRtpObserversIt);
	}

	inline void Router::OnTransportProducerPaused(RTC::Transport* /*transport*/, RTC::Producer* producer)
	{
		MS_TRACE();

		auto& consumers = this->mapProducerConsumers.at(producer);

		for (auto* consumer : consumers)
		{
			consumer->ProducerPaused();
		}

		auto it = this->mapProducerRtpObservers.find(producer);

		if (it != this->mapProducerRtpObservers.end())
		{
			auto& rtpObservers = it->second;

			for (auto* rtpObserver : rtpObservers)
			{
				rtpObserver->ProducerPaused(producer);
			}
		}
	}

	inline void Router::OnTransportProducerResumed(RTC::Transport* /*transport*/, RTC::Producer* producer)
	{
		MS_TRACE();

		auto& consumers = this->mapProducerConsumers.at(producer);

		for (auto* consumer : consumers)
		{
			consumer->ProducerResumed();
		}

		auto it = this->mapProducerRtpObservers.find(producer);

		if (it != this->mapProducerRtpObservers.end())
		{
			auto& rtpObservers = it->second;

			for (auto* rtpObserver : rtpObservers)
			{
				rtpObserver->ProducerResumed(producer);
			}
		}
	}

	inline void Router::OnTransportProducerNewRtpStream(
	  RTC::Transport* /*transport*/, RTC::Producer* producer, RTC::RtpStream* rtpStream, uint32_t mappedSsrc)
	{
		MS_TRACE();

		auto& consumers = this->mapProducerConsumers.at(producer);

		for (auto* consumer : consumers)
		{
			consumer->ProducerNewRtpStream(rtpStream, mappedSsrc);
		}
	}

	inline void Router::OnTransportProducerRtpStreamScore(
	  RTC::Transport* /*transport*/,
	  RTC::Producer* producer,
	  RTC::RtpStream* rtpStream,
	  uint8_t score,
	  uint8_t previousScore)
	{
		MS_TRACE();

		auto& consumers = this->mapProducerConsumers.at(producer);

		for (auto* consumer : consumers)
		{
			consumer->ProducerRtpStreamScore(rtpStream, score, previousScore);
		}
	}

	inline void Router::OnTransportProducerRtcpSenderReport(
	  RTC::Transport* /*transport*/, RTC::Producer* producer, RTC::RtpStream* rtpStream, bool first)
	{
		MS_TRACE();

		auto& consumers = this->mapProducerConsumers.at(producer);

		for (auto* consumer : consumers)
		{
			consumer->ProducerRtcpSenderReport(rtpStream, first);
		}
	}

	inline void Router::OnTransportProducerRtpPacketReceived(
	  RTC::Transport* /*transport*/, RTC::Producer* producer, RTC::RtpPacket* packet)
	{
		MS_TRACE();

		auto& consumers = this->mapProducerConsumers.at(producer);

		for (auto* consumer : consumers)
		{
			// Update MID RTP extension value.
			const auto& mid = consumer->GetRtpParameters().mid;

			if (!mid.empty())
				packet->UpdateMid(mid);

			consumer->SendRtpPacket(packet);
		}

		auto it = this->mapProducerRtpObservers.find(producer);

		if (it != this->mapProducerRtpObservers.end())
		{
			auto& rtpObservers = it->second;

			for (auto* rtpObserver : rtpObservers)
			{
				rtpObserver->ReceiveRtpPacket(producer, packet);
			}
		}
	}

	inline void Router::OnTransportNeedWorstRemoteFractionLost(
	  RTC::Transport* /*transport*/,
	  RTC::Producer* producer,
	  uint32_t mappedSsrc,
	  uint8_t& worstRemoteFractionLost)
	{
		MS_TRACE();

		auto& consumers = this->mapProducerConsumers.at(producer);

		for (auto* consumer : consumers)
		{
			consumer->NeedWorstRemoteFractionLost(mappedSsrc, worstRemoteFractionLost);
		}
	}

	inline void Router::OnTransportNewConsumer(
	  RTC::Transport* /*transport*/, RTC::Consumer* consumer, std::string& producerId)
	{
		MS_TRACE();

		auto mapProducersIt = this->mapProducers.find(producerId);

		if (mapProducersIt == this->mapProducers.end())
			MS_THROW_ERROR("Producer not found [producerId:%s]", producerId.c_str());

		auto* producer              = mapProducersIt->second;
		auto mapProducerConsumersIt = this->mapProducerConsumers.find(producer);

		MS_ASSERT(
		  mapProducerConsumersIt != this->mapProducerConsumers.end(),
		  "Producer not present in mapProducerConsumers");
		MS_ASSERT(
		  this->mapConsumerProducer.find(consumer) == this->mapConsumerProducer.end(),
		  "Consumer already present in mapConsumerProducer");

		// Update the Consumer status based on the Producer status.
		if (producer->IsPaused())
			consumer->ProducerPaused();

		// Insert the Consumer in the maps.
		auto& consumers = mapProducerConsumersIt->second;

		consumers.insert(consumer);
		this->mapConsumerProducer[consumer] = producer;

		// Get all streams in the Producer and provide the Consumer with them.
		for (const auto& kv : producer->GetRtpStreams())
		{
			auto* rtpStream     = kv.first;
			uint32_t mappedSsrc = kv.second;

			consumer->ProducerRtpStream(rtpStream, mappedSsrc);
		}

		// Provide the Consumer with the scores of all streams in the Producer.
		consumer->ProducerRtpStreamScores(producer->GetRtpStreamScores());
	}

	inline void Router::OnTransportConsumerClosed(RTC::Transport* /*transport*/, RTC::Consumer* consumer)
	{
		MS_TRACE();

		// This callback is called when the Consumer has been closed but its Producer
		// remains alive, so the entry in mapProducerConsumers still exists and must
		// be removed.

		auto mapConsumerProducerIt = this->mapConsumerProducer.find(consumer);

		MS_ASSERT(
		  mapConsumerProducerIt != this->mapConsumerProducer.end(),
		  "Consumer not present in mapConsumerProducer");

		// Get the associated Producer.
		auto* producer = mapConsumerProducerIt->second;

		MS_ASSERT(
		  this->mapProducerConsumers.find(producer) != this->mapProducerConsumers.end(),
		  "Producer not present in mapProducerConsumers");

		// Remove the Consumer from the set of Consumers of the Producer.
		auto& consumers = this->mapProducerConsumers.at(producer);

		consumers.erase(consumer);

		// Remove the Consumer from the map.
		this->mapConsumerProducer.erase(mapConsumerProducerIt);
	}

	inline void Router::OnTransportConsumerProducerClosed(
	  RTC::Transport* /*transport*/, RTC::Consumer* consumer)
	{
		MS_TRACE();

		// This callback is called when the Consumer has been closed because its
		// Producer was closed, so the entry in mapProducerConsumers has already been

		auto mapConsumerProducerIt = this->mapConsumerProducer.find(consumer);

		MS_ASSERT(
		  mapConsumerProducerIt != this->mapConsumerProducer.end(),
		  "Consumer not present in mapConsumerProducer");

		// Remove the Consumer from the map.
		this->mapConsumerProducer.erase(mapConsumerProducerIt);
	}

	inline void Router::OnTransportConsumerKeyFrameRequested(
	  RTC::Transport* /*transport*/, RTC::Consumer* consumer, uint32_t mappedSsrc)
	{
		MS_TRACE();

		auto* producer = this->mapConsumerProducer.at(consumer);

		producer->RequestKeyFrame(mappedSsrc);
	}

	inline void Router::OnTransportNewDataProducer(
	  RTC::Transport* /*transport*/, RTC::DataProducer* dataProducer)
	{
		MS_TRACE();

		MS_ASSERT(
		  this->mapDataProducerDataConsumers.find(dataProducer) ==
		    this->mapDataProducerDataConsumers.end(),
		  "DataProducer already present in mapDataProducerDataConsumers");

		if (this->mapDataProducers.find(dataProducer->id) != this->mapDataProducers.end())
		{
			MS_THROW_ERROR(
			  "DataProducer already present in mapDataProducers [dataProducerId:%s]",
			  dataProducer->id.c_str());
		}

		// Insert the DataProducer in the maps.
		this->mapDataProducers[dataProducer->id] = dataProducer;
		this->mapDataProducerDataConsumers[dataProducer];
	}

	inline void Router::OnTransportDataProducerClosed(
	  RTC::Transport* /*transport*/, RTC::DataProducer* dataProducer)
	{
		MS_TRACE();

		auto mapDataProducerDataConsumersIt = this->mapDataProducerDataConsumers.find(dataProducer);
		auto mapDataProducersIt             = this->mapDataProducers.find(dataProducer->id);

		MS_ASSERT(
		  mapDataProducerDataConsumersIt != this->mapDataProducerDataConsumers.end(),
		  "DataProducer not present in mapDataProducerDataConsumers");
		MS_ASSERT(
		  mapDataProducersIt != this->mapDataProducers.end(),
		  "DataProducer not present in mapDataProducers");

		// Close all DataConsumers associated to the closed DataProducer.
		auto& dataConsumers = mapDataProducerDataConsumersIt->second;

		// NOTE: While iterating the set of DataConsumers, we call DataProducerClosed()
		// on each one, which will end calling
		// Router::OnTransportDataConsumerDataProducerClosed(), which will remove the
		// DataConsumer from mapDataConsumerDataProducer but won't remove the closed
		// DataConsumer from the set of DataConsumers in mapDataProducerDataConsumers
		// (here will erase the complete entry in that map).
		for (auto* dataConsumer : dataConsumers)
		{
			// Call dataConsumer->DataProducerClosed() so the DataConsumer will notify the Node
			// process, will notify its Transport, and its Transport will delete the DataConsumer.
			dataConsumer->DataProducerClosed();
		}

		// Remove the DataProducer from the maps.
		this->mapDataProducers.erase(mapDataProducersIt);
		this->mapDataProducerDataConsumers.erase(mapDataProducerDataConsumersIt);
	}

	inline void Router::OnTransportDataProducerMessageReceived(
	  RTC::Transport* /*transport*/,
	  RTC::DataProducer* dataProducer,
	  uint32_t ppid,
	  const uint8_t* msg,
	  size_t len)
	{
		MS_TRACE();

		auto& dataConsumers = this->mapDataProducerDataConsumers.at(dataProducer);

		for (auto* consumer : dataConsumers)
		{
			consumer->SendMessage(ppid, msg, len);
		}
	}

	inline void Router::OnTransportNewDataConsumer(
	  RTC::Transport* /*transport*/, RTC::DataConsumer* dataConsumer, std::string& dataProducerId)
	{
		MS_TRACE();

		auto mapDataProducersIt = this->mapDataProducers.find(dataProducerId);

		if (mapDataProducersIt == this->mapDataProducers.end())
			MS_THROW_ERROR("DataProducer not found [dataProducerId:%s]", dataProducerId.c_str());

		auto* dataProducer                  = mapDataProducersIt->second;
		auto mapDataProducerDataConsumersIt = this->mapDataProducerDataConsumers.find(dataProducer);

		MS_ASSERT(
		  mapDataProducerDataConsumersIt != this->mapDataProducerDataConsumers.end(),
		  "DataProducer not present in mapDataProducerDataConsumers");
		MS_ASSERT(
		  this->mapDataConsumerDataProducer.find(dataConsumer) == this->mapDataConsumerDataProducer.end(),
		  "DataConsumer already present in mapDataConsumerDataProducer");

		// Insert the DataConsumer in the maps.
		auto& dataConsumers = mapDataProducerDataConsumersIt->second;

		dataConsumers.insert(dataConsumer);
		this->mapDataConsumerDataProducer[dataConsumer] = dataProducer;
	}

	inline void Router::OnTransportDataConsumerClosed(
	  RTC::Transport* /*transport*/, RTC::DataConsumer* dataConsumer)
	{
		MS_TRACE();

		// This callback is called when the DataConsumer has been closed but its DataProducer
		// remains alive, so the entry in mapDataProducerDataConsumers still exists and must
		// be removed.

		auto mapDataConsumerDataProducerIt = this->mapDataConsumerDataProducer.find(dataConsumer);

		MS_ASSERT(
		  mapDataConsumerDataProducerIt != this->mapDataConsumerDataProducer.end(),
		  "DataConsumer not present in mapDataConsumerDataProducer");

		// Get the associated DataProducer.
		auto* dataProducer = mapDataConsumerDataProducerIt->second;

		MS_ASSERT(
		  this->mapDataProducerDataConsumers.find(dataProducer) !=
		    this->mapDataProducerDataConsumers.end(),
		  "DataProducer not present in mapDataProducerDataConsumers");

		// Remove the DataConsumer from the set of DataConsumers of the DataProducer.
		auto& dataConsumers = this->mapDataProducerDataConsumers.at(dataProducer);

		dataConsumers.erase(dataConsumer);

		// Remove the DataConsumer from the map.
		this->mapDataConsumerDataProducer.erase(mapDataConsumerDataProducerIt);
	}

	inline void Router::OnTransportDataConsumerDataProducerClosed(
	  RTC::Transport* /*transport*/, RTC::DataConsumer* dataConsumer)
	{
		MS_TRACE();

		// This callback is called when the DataConsumer has been closed because its
		// DataProducer was closed, so the entry in mapDataProducerDataConsumers has already
		// been removed.

		auto mapDataConsumerDataProducerIt = this->mapDataConsumerDataProducer.find(dataConsumer);

		MS_ASSERT(
		  mapDataConsumerDataProducerIt != this->mapDataConsumerDataProducer.end(),
		  "DataConsumer not present in mapDataConsumerDataProducer");

		// Remove the DataConsumer from the map.
		this->mapDataConsumerDataProducer.erase(mapDataConsumerDataProducerIt);
	}
} // namespace RTC


void PlayStationDisc::close(void)
{
	RP_D(PlayStationDisc);

	// NOTE: Don't delete these. They have rp_image objects
	// that may be used by the UI later.
	if (d->bootExeData) {
		d->bootExeData->close();
	}

	UNREF_AND_NULL(d->isoPartition);
	UNREF_AND_NULL(d->discReader);

	super::close();
}

/***************************************************************************
 * ROM Properties Page shell extension. (libromdata)                       *
 * PlayStationDisc.cpp: PlayStation 1 and 2 disc image reader.             *
 *                                                                         *
 ***************************************************************************/

#include "stdafx.h"
#include "PlayStationDisc.hpp"
#include "ps2_structs.h"

// librpbase, librpfile
#include "librpfile/RpFile.hpp"
using namespace LibRpBase;
using LibRpFile::IRpFile;
using LibRpFile::RpFile;

// IsoPartition
#include "../cdrom_structs.h"
#include "../iso_structs.h"
#include "../disc/Cdrom2352Reader.hpp"
#include "../disc/IsoPartition.hpp"

// Other RomData subclasses
#include "Other/ISO.hpp"
#include "Console/PlayStationEXE.hpp"
#include "Other/ELF.hpp"

// inih for SYSTEM.CNF
#include "ini.h"

using std::string;
using std::unordered_map;
using std::vector;

namespace LibRomData {

class PlayStationDiscPrivate final : public RomDataPrivate
{
	public:
		PlayStationDiscPrivate(PlayStationDisc *q, LibRpFile::IRpFile *file);
		virtual ~PlayStationDiscPrivate();

	private:
		typedef RomDataPrivate super;
		RP_DISABLE_COPY(PlayStationDiscPrivate)

	public:
		/** RomDataInfo **/
		static const char *const exts[];
		static const char *const mimeTypes[];
		static const RomDataInfo romDataInfo;

	public:
		ISO_Primary_Volume_Descriptor pvd;

		// SYSTEM.CNF contents
		// Keys are stored in upper-case.
		unordered_map<string, string> system_cnf;

		/**
		 * ini.h callback for parsing SYSTEM.CNF.
		 * @param user		[in] User data parameter (this)
		 * @param section	[in] Section name
		 * @param name		[in] Value name
		 * @param value		[in] Value
		 * @return 0 to continue; 1 to stop.
		 */
		static int parse_system_cnf(void *user, const char *section, const char *name, const char *value);

		/**
		 * Load SYSTEM.CNF.
		 * @param pt IPartition containing SYSTEM.CNF.
		 * @return 0 on success; negative POSIX error code on error.
		 */
		int loadSystemCnf(IsoPartition *pt);

		// IsoPartition
		IDiscReader *discReader;
		IsoPartition *isoPartition;

		// Boot executable
		RomData *bootExeData;

		// Boot filename.
		// - ISO version number removed.
		string boot_filename;

		// Optional boot argument.
		string boot_argument;

		/**
		 * Open the boot executable.
		 * @return RomData* on success; nullptr on error.
		 */
		RomData *openBootExe(void);

		enum class ConsoleType {
			Unknown	= -1,

			PS1	= 0,	// PlayStation
			PS2	= 1,	// PlayStation 2

			Max
		};
		ConsoleType consoleType;
};

ROMDATA_IMPL(PlayStationDisc)

/** PlayStationDiscPrivate **/

/* RomDataInfo */
const char *const PlayStationDiscPrivate::exts[] = {
	".iso",		// ISO
	".bin",		// BIN/CUE
	".img",		// CCD/IMG

	nullptr
};
const char *const PlayStationDiscPrivate::mimeTypes[] = {
	// Unofficial MIME types from FreeDesktop.org.
	"application/x-cd-image",
	"application/x-iso9660-image",

	nullptr
};
const RomDataInfo PlayStationDiscPrivate::romDataInfo = {
	"PlayStationDisc", exts, mimeTypes
};

PlayStationDiscPrivate::PlayStationDiscPrivate(PlayStationDisc *q, IRpFile *file)
	: super(q, file, &romDataInfo)
	, discReader(nullptr)
	, isoPartition(nullptr)
	, bootExeData(nullptr)
	, consoleType(ConsoleType::Unknown)
{
	// Clear the structs.
	memset(&pvd, 0, sizeof(pvd));
}

PlayStationDiscPrivate::~PlayStationDiscPrivate()
{
	UNREF(bootExeData);
	UNREF(isoPartition);
	UNREF(discReader);
}

/**
 * ini.h callback for parsing SYSTEM.CNF.
 * @param user		[in] User data parameter (this)
 * @param section	[in] Section name
 * @param name		[in] Value name
 * @param value		[in] Value
 * @return 0 to continue; 1 to stop.
 */
int PlayStationDiscPrivate::parse_system_cnf(void *user, const char *section, const char *name, const char *value)
{
	if (section[0] != '\0') {
		// Sections aren't expected here...
		return 1;
	}

	// Save the value for later.
	string s_name(name);
	std::transform(s_name.begin(), s_name.end(), s_name.begin(),
		[](unsigned char c) { return std::toupper(c); });

	PlayStationDiscPrivate *const d = static_cast<PlayStationDiscPrivate*>(user);
	auto ret = d->system_cnf.emplace(std::move(s_name), value);
	return (ret.second ? 0 : 1);
}

/**
 * Load SYSTEM.CNF.
 * @param pt IPartition containing SYSTEM.CNF.
 * @return 0 on success; negative POSIX error code on error.
 */
int PlayStationDiscPrivate::loadSystemCnf(IsoPartition *pt)
{
	if (!system_cnf.empty()) {
		// Already loaded.
		return 0;
	}

	IRpFile *const f_system_cnf = pt->open("SYSTEM.CNF");
	if (!f_system_cnf) {
		// SYSTEM.CNF might not be present.
		// If it isn't, but PSX.EXE is present, use default values.
		int ret = pt->lastError();
		if (ret == ENOENT) {
			// SYSTEM.CNF not found. Check for PSX.EXE.
			IRpFile *const f_psx_exe = pt->open("PSX.EXE");
			if (f_psx_exe && f_psx_exe->isOpen()) {
				// Found PSX.EXE.
				boot_filename = "PSX.EXE";
				system_cnf.emplace("BOOT", boot_filename);
				if (f_psx_exe) {
					f_psx_exe->unref();
				}
				// Pretend that we did find SYSTEM.CNF.
				return 0;
			} else {
				// Not found.
				return -ENOENT;
			}
		} else {
			return (ret == 0 ? -EIO : -ret);
		}
	} else if (!f_system_cnf->isOpen()) {
		int ret = -f_system_cnf->lastError();
		if (ret == 0) {
			ret = -EIO;
		}
		f_system_cnf->unref();
		return ret;
	}

	// CNF file should be less than 2048 bytes.
	const int64_t fileSize = f_system_cnf->size();
	if (fileSize > 2048) {
		f_system_cnf->unref();
		return -ENOMEM;
	}

	// Read the entire file into memory.
	char buf[2049];
	size_t size = f_system_cnf->read(buf, 2048);
	if (size != static_cast<size_t>(fileSize)) {
		// Short read.
		f_system_cnf->unref();
		return -EIO;
	}
	buf[static_cast<size_t>(fileSize)] = '\0';

	// Process the file.
	// TODO: Fail on error?
	ini_parse_string(buf, parse_system_cnf, this);

	f_system_cnf->unref();
	return (!system_cnf.empty() ? 0 : -EIO);
}

/**
 * Open the boot executable.
 * @return RomData* on success; nullptr on error.
 */
RomData *PlayStationDiscPrivate::openBootExe(void)
{
	if (bootExeData) {
		// The boot executable is already open.
		return bootExeData;
	}

	if (!isoPartition || !isoPartition->isOpen()) {
		// ISO partition is not open.
		return nullptr;
	}

	if (boot_filename.empty()) {
		// No boot filename...
		return nullptr;
	}

	// Open the boot file.
	// TODO: Do we need a leading slash?
	IRpFile *f_bootExe = isoPartition->open(boot_filename.c_str());
	if (f_bootExe) {
		RomData *exeData = nullptr;
		switch (consoleType) {
			case ConsoleType::PS1: {
				// Check if we have a STACK override in SYSTEM.CNF.
				uint32_t sp_override = 0;
				auto iter = system_cnf.find("STACK");
				if (iter != system_cnf.end() && !iter->second.empty()) {
					// Validate the value.
					char *endptr = nullptr;
					sp_override = strtoul(iter->second.c_str(), &endptr, 16);
					if (*endptr != '\0') {
						sp_override = 0;
					}
				}
				exeData = new PlayStationEXE(f_bootExe, sp_override);
				break;
			}
			case ConsoleType::PS2:
				exeData = new ELF(f_bootExe);
				break;
			default:
				assert(!"Console type not supported.");
				break;
		}
		f_bootExe->unref();
		if (exeData && exeData->isValid()) {
			// Boot executable is open and valid.
			bootExeData = exeData;
			return exeData;
		}

		// Unable to open the executable.
		UNREF(exeData);
	}

	// Unable to open the default executable.
	return nullptr;
}

/** PlayStationDisc **/

/**
 * Read a Sony PlayStation 1 or 2 disc image.
 *
 * A ROM file must be opened by the caller. The file handle
 * will be ref()'d and must be kept open in order to load
 * data from the ROM.
 *
 * To close the file, either delete this object or call close().
 *
 * NOTE: Check isValid() to determine if this is a valid ROM.
 *
 * @param file Open ROM image.
 */
PlayStationDisc::PlayStationDisc(IRpFile *file)
	: super(new PlayStationDiscPrivate(this, file))
{
	// This class handles disc images.
	RP_D(PlayStationDisc);
	d->mimeType = "application/x-cd-image";	// unofficial
	d->fileType = FileType::DiscImage;

	if (!d->file) {
		// Could not ref() the file handle.
		return;
	}

	IDiscReader *discReader = nullptr;

	// Check for a PVD with 2048-byte sectors.
	size_t size = d->file->seekAndRead(ISO_PVD_ADDRESS_2048, &d->pvd, sizeof(d->pvd));
	if (size != sizeof(d->pvd)) {
		UNREF_AND_NULL_NOCHK(d->file);
		return;
	}
	if (ISO::checkPVD(reinterpret_cast<const uint8_t*>(&d->pvd)) >= 0) {
		discReader = new DiscReader(d->file);
	} else {
		// Check for a PVD with 2352-byte or 2448-byte sectors.
		static const unsigned int sector_sizes[] = {2352, 2448, 0};
		CDROM_2352_Sector_t sector;

		for (const unsigned int *p = sector_sizes; *p != 0; p++) {
			size_t size = d->file->seekAndRead(*p * ISO_PVD_LBA, &sector, sizeof(sector));
			if (size != sizeof(d->pvd)) {
				UNREF_AND_NULL_NOCHK(d->file);
				return;
			}

			const uint8_t *const pData = cdromSectorDataPtr(&sector);
			if (ISO::checkPVD(pData) >= 0) {
				// Found the correct sector size.
				memcpy(&d->pvd, pData, sizeof(d->pvd));
				discReader = new Cdrom2352Reader(d->file, *p);
				break;
			}
		}
	}

	if (!discReader || !discReader->isOpen()) {
		// Error opening the DiscReader.
		UNREF(discReader);
		UNREF_AND_NULL_NOCHK(d->file);
		return;
	}

	// Try to open the ISO partition.
	IsoPartition *const isoPartition = new IsoPartition(discReader, 0, 0);
	if (!isoPartition->isOpen()) {
		// Error opening the ISO partition.
		UNREF(isoPartition);
		UNREF(discReader);
		d->file->unref();
		d->file = nullptr;
		return;
	}

	// ISO-9660 partition is open.
	// Load SYSTEM.CNF.
	int ret = d->loadSystemCnf(isoPartition);
	if (ret != 0) {
		// Error loading SYSTEM.CNF.
		UNREF(isoPartition);
		UNREF(discReader);
		UNREF_AND_NULL_NOCHK(d->file);
		return;
	}

	// Check if we have a boot filename.
	PlayStationDiscPrivate::ConsoleType consoleType = PlayStationDiscPrivate::ConsoleType::Unknown;
	auto iter = d->system_cnf.find("BOOT2");
	if (iter != d->system_cnf.end()) {
		consoleType = PlayStationDiscPrivate::ConsoleType::PS2;
	} else {
		iter = d->system_cnf.find("BOOT");
		if (iter != d->system_cnf.end()) {
			consoleType = PlayStationDiscPrivate::ConsoleType::PS1;
		} else {
			// Not valid.
			UNREF(isoPartition);
			UNREF(discReader);
			UNREF_AND_NULL_NOCHK(d->file);
			return;
		}
	}

	// Boot filename should start with:
	// There's usually a backslash after the colon, but some
	// prototypes don't have it.
	const auto &bf_str = iter->second;
	if (!bf_str.empty()) {
		size_t pos = 0;
		if (!strncasecmp(bf_str.c_str(), "cdrom", 5)) {
			pos = 5;
			if (bf_str[pos] == '0') {
				pos++;
			}
			if (bf_str[pos] == ':') {
				pos++;
				// Remove one or more backslashes.
				while (bf_str[pos] == '\\') {
					pos++;
				}
			}
		}
		d->boot_filename = bf_str.substr(pos);
	}
	if (d->boot_filename.empty()) {
		// No boot filename specified.
		// Use the console-specific default.
		switch (consoleType) {
			default:
			case PlayStationDiscPrivate::ConsoleType::PS1:
				d->boot_filename = "PSX.EXE";
				break;
			case PlayStationDiscPrivate::ConsoleType::PS2:
				// TODO: Default PS2 boot filename?
				break;
		}
	}

	// Check if there is a space.
	size_t pos = bf_str.find(' ');
	if (pos != string::npos && pos > 0) {
		// Found a space.
		// Everything after the space is a boot argument.
		d->boot_argument = d->boot_filename.substr(pos+1);
		d->boot_filename.resize(pos-1);
	}

	// Remove the ISO version number.
	size_t len = d->boot_filename.size();
	if (len > 2) {
		if (ISDIGIT(d->boot_filename[len-1]) && d->boot_filename[len-2] == ';') {
			d->boot_filename.resize(len-2);
		}
	}

	// Disc image is ready.
	d->consoleType = consoleType;
	d->discReader = discReader;
	d->isoPartition = isoPartition;
	d->isValid = true;
}

/**
 * Close the opened file.
 */

/** ROM detection functions. **/

/**
 * Is a ROM image supported by this class?
 * @param info DetectInfo containing ROM detection information.
 * @return Class-specific system ID (>= 0) if supported; -1 if not.
 */
int PlayStationDisc::isRomSupported_static(const DetectInfo *info)
{
	// NOTE: This version is NOT supported for PlayStationDisc.
	// Use the ISO-9660 PVD check instead.
	RP_UNUSED(info);
	assert(!"Use the ISO-9660 PVD check instead.");
	return -1;
}

/**
 * Is a ROM image supported by this class?
 * @param pvd ISO-9660 Primary Volume Descriptor.
 * @return Class-specific system ID (>= 0) if supported; -1 if not.
 */
int PlayStationDisc::isRomSupported_static(
	const ISO_Primary_Volume_Descriptor *pvd)
{
	assert(pvd != nullptr);
	if (!pvd) {
		return -1;
	}

	// PlayStation 1 and 2 discs have the system ID "PLAYSTATION".
	// NOTE: Some PS2 prototypes have incorrect system IDs. We'll
	// check for those here, and then verify SYSTEM.CNF later.
	int pos = -1;
	if (!strncmp(pvd->sysID, "PLAYSTATION ", 12)) {
		pos = 12;
	} else if (!strncmp(pvd->sysID, "CD-RTOS CD-BRIDGE ", 18)) {
		// CD-i system ID
		// Some PS2 prototypes have this for some reason.
		pos = 18;
	} else if (!strncmp(pvd->sysID, "Win32 ", 6)) {
		// No idea why some PS2 prototypes have this one...
		pos = 6;
	}

	if (pos < 0) {
		// Not valid.
		return -1;
	}

	// Make sure the rest of the system ID is either spaces or NULLs.
	const char *p = &pvd->sysID[pos];
	const char *const p_end = &pvd->sysID[sizeof(pvd->sysID)];
	bool isOK = true;
	for (; p < p_end; p++) {
		if (*p != ' ' && *p != '\0') {
			isOK = false;
			break;
		}
	}

	if (isOK) {
		// Valid PVD.
		// Caller will need to check for the sector size.
		return 0;
	}

	// Not a PlayStation 1 or 2 disc.
	return -1;
}

/**
 * Get the name of the system the loaded ROM is designed for.
 * @param type System name type. (See the SystemName enum.)
 * @return System name, or nullptr if type is invalid.
 */
const char *PlayStationDisc::systemName(unsigned int type) const
{
	RP_D(const PlayStationDisc);
	if (!d->isValid || !isSystemNameTypeValid(type))
		return nullptr;

	// PlayStationDisc has the same name worldwide, so we can
	// ignore the region selection.
	static_assert(SYSNAME_TYPE_MASK == 3,
		"PlayStationDisc::systemName() array index optimization needs to be updated.");

	switch (d->consoleType) {
		default:
		case PlayStationDiscPrivate::ConsoleType::PS1: {
			static const char *const sysNames_PS1[4] = {
				"Sony PlayStation", "PlayStation", "PS1", nullptr
			};
			return sysNames_PS1[type & SYSNAME_TYPE_MASK];
		}

		case PlayStationDiscPrivate::ConsoleType::PS2: {
			static const char *const sysNames_PS2[4] = {
				"Sony PlayStation 2", "PlayStation 2", "PS2", nullptr
			};
			return sysNames_PS2[type & SYSNAME_TYPE_MASK];
		}
	}

	// Should not get here...
	assert(!"PlayStationDisc::systemName(): Invalid system name.");
	return nullptr;
}

/**
 * Load field data.
 * Called by RomData::fields() if the field data hasn't been loaded yet.
 * @return Number of fields read on success; negative POSIX error code on error.
 */
int PlayStationDisc::loadFieldData(void)
{
	RP_D(PlayStationDisc);
	if (!d->fields->empty()) {
		return 0;
	} else if (!d->file || !d->file->isOpen()) {
		// File isn't open.
		return -EBADF;
	} else if (!d->isValid) {
		// Unknown disc type.
		return -EIO;
	}

	d->fields->reserve(6);	// Maximum of 6 fields.

	const char *s_tab_name;
	switch (d->consoleType) {
		default:
		case PlayStationDiscPrivate::ConsoleType::PS1:
			s_tab_name = "PS1";
			break;
		case PlayStationDiscPrivate::ConsoleType::PS2:
			s_tab_name = "PS2";
			break;
	}
	d->fields->setTabName(0, s_tab_name);

	// Boot filename
	d->fields->addField_string(C_("PlayStationDisc", "Boot Filename"), d->boot_filename);

	// Boot argument, if present
	if (!d->boot_argument.empty()) {
		d->fields->addField_string(C_("PlayStationDisc", "Boot Argument"), d->boot_argument);
	}

	// Console-specific fields
	switch (d->consoleType) {
		default:
		case PlayStationDiscPrivate::ConsoleType::PS1: {
			// Max thread count
			auto iter = d->system_cnf.find("TCB");
			if (iter != d->system_cnf.end() && !iter->second.empty()) {
				d->fields->addField_string(C_("PlayStationDisc", "Max Thread Count"), iter->second);
			}

			// Max event count
			iter = d->system_cnf.find("EVENT");
			if (iter != d->system_cnf.end() && !iter->second.empty()) {
				d->fields->addField_string(C_("PlayStationDisc", "Max Event Count"), iter->second);
			}
			break;
		}

		case PlayStationDiscPrivate::ConsoleType::PS2: {
			// Version
			auto iter = d->system_cnf.find("VER");
			if (iter != d->system_cnf.end() && !iter->second.empty()) {
				d->fields->addField_string(C_("PlayStationDisc", "Version"), iter->second);
			}

			// Video mode
			iter = d->system_cnf.find("VMODE");
			if (iter != d->system_cnf.end() && !iter->second.empty()) {
				d->fields->addField_string(C_("PlayStationDisc", "Video Mode"), iter->second);
			}
			break;
		}
	}

	// Boot file timestamp
	time_t boot_file_timestamp = -1;
	if (!d->boot_filename.empty()) {
		// TODO: Do we need a leading slash?
		boot_file_timestamp = d->isoPartition->get_mtime(d->boot_filename.c_str());
	}
	d->fields->addField_dateTime(C_("PlayStationDisc", "Boot File Time"),
		boot_file_timestamp,
		RomFields::RFT_DATETIME_HAS_DATE |
		RomFields::RFT_DATETIME_HAS_TIME);

	// Show a tab for the boot file.
	RomData *const bootExeData = d->openBootExe();
	if (bootExeData) {
		// Add the fields.
		// NOTE: Adding tabs manually so we can show the disc info in
		// the primary tab.
		const RomFields *const exeFields = bootExeData->fields();
		if (exeFields) {
			int exeTabCount = exeFields->tabCount();
			for (int i = 1; i < exeTabCount; i++) {
				d->fields->setTabName(i, exeFields->tabName(i));
			}
			d->fields->setTabIndex(0);
			d->fields->addFields_romFields(exeFields, 0);
			d->fields->setTabIndex(exeTabCount - 1);
		}
	}

	PS2_CDVDGEN_t cdvdgen;
	size_t size = d->discReader->seekAndRead(
		PS2_CDVDGEN_LBA * ISO_SECTOR_SIZE_MODE1_COOKED, &cdvdgen, sizeof(cdvdgen));
	if (size == sizeof(cdvdgen) && !memcmp(cdvdgen.sw_version, "CDVDGEN ", 8)) {
		// CDVDGEN data found.
		d->fields->addTab("CDVDGEN");
		d->fields->reserve(d->fields->count() + 9);

		// CDVDGEN version
		d->fields->addField_string(C_("PlayStationDisc", "CDVDGEN Version"),
			cp1252_to_utf8(&cdvdgen.sw_version[8], sizeof(cdvdgen.disc_name)-8),
			RomFields::STRF_TRIM_END);

		// Disc name
		d->fields->addField_string(C_("PlayStationDisc", "Disc Name"),
			cp1252_to_utf8(cdvdgen.disc_name, sizeof(cdvdgen.disc_name)),
			RomFields::STRF_TRIM_END);

		// Producer name
		d->fields->addField_string(C_("PlayStationDisc", "Producer Name"),
			cp1252_to_utf8(cdvdgen.producer_name, sizeof(cdvdgen.producer_name)),
			RomFields::STRF_TRIM_END);

		// Copyright holder
		d->fields->addField_string(C_("PlayStationDisc", "Copyright Holder"),
			cp1252_to_utf8(cdvdgen.copyright_holder, sizeof(cdvdgen.copyright_holder)),
			RomFields::STRF_TRIM_END);

		// Creation date
		// NOTE: Marking as UTC because there's no timezone information.
		d->fields->addField_dateTime(C_("PlayStationDisc", "Creation Date"),
			d->ascii_yyyymmdd_to_unix_time(cdvdgen.creation_date),
			RomFields::RFT_DATETIME_HAS_DATE |
			RomFields::RFT_DATETIME_IS_UTC);

		// TODO: Show the master disc ID?

		// Disc drive information
		d->fields->addField_string(C_("PlayStationDisc", "Drive Vendor"),
			cp1252_to_utf8(cdvdgen.drive.vendor, sizeof(cdvdgen.drive.vendor)),
			RomFields::STRF_TRIM_END);
		d->fields->addField_string(C_("PlayStationDisc", "Drive Model"),
			cp1252_to_utf8(cdvdgen.drive.model, sizeof(cdvdgen.drive.model)),
			RomFields::STRF_TRIM_END);
		d->fields->addField_string(C_("PlayStationDisc", "Drive Firmware"),
			cp1252_to_utf8(cdvdgen.drive.revision, sizeof(cdvdgen.drive.revision)),
			RomFields::STRF_TRIM_END);
		d->fields->addField_string(C_("PlayStationDisc", "Drive Notes"),
			cp1252_to_utf8(cdvdgen.drive.notes, sizeof(cdvdgen.drive.notes)),
			RomFields::STRF_TRIM_END);
	}

	// ISO object for ISO-9660 PVD
	ISO *const isoData = new ISO(d->file);
	if (isoData->isOpen()) {
		// Add the fields.
		const RomFields *const isoFields = isoData->fields();
		if (isoFields) {
			d->fields->addFields_romFields(isoFields,
				RomFields::TabOffset_AddTabs);
		}
	}
	isoData->unref();

	// Finished reading the field data.
	return static_cast<int>(d->fields->count());
}

/**
 * Load metadata properties.
 * Called by RomData::metaData() if the field data hasn't been loaded yet.
 * @return Number of metadata properties read on success; negative POSIX error code on error.
 */
int PlayStationDisc::loadMetaData(void)
{
	RP_D(PlayStationDisc);
	if (d->metaData != nullptr) {
		return 0;
	} else if (!d->isValid) {
		// Unknown disc image type.
		return -EIO;
	}

	// Create the metadata object.
	d->metaData = new RomMetaData();
	d->metaData->reserve(3);	// Maximum of 3 metadata properties.

	// Add the PVD metadata.
	// TODO: PlayStationDisc-specific metadata?
	ISO::addMetaData_PVD(d->metaData, &d->pvd);

	// Finished reading the metadata.
	return static_cast<int>(d->metaData->count());
}

}


void adam_ram_expansion_device::adam_bd_w(offs_t offset, uint8_t data, int bmreq, int biorq, int aux_rom_cs, int cas1, int cas2)
{
	if (!cas2)
	{
		m_ram[offset] = data;
	}
}

// copyright-holders:Curt Coder
/**********************************************************************

    Coleco Adam Internal 64KB RAM Expansion emulation

**********************************************************************/

#include "emu.h"
#include "ram.h"

//  DEVICE DEFINITIONS

DEFINE_DEVICE_TYPE(ADAM_RAM, adam_ram_expansion_device, "adam_ram", "Adam 64KB RAM expansion")

//  LIVE DEVICE

adam_ram_expansion_device::adam_ram_expansion_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) :
	device_t(mconfig, ADAM_RAM, tag, owner, clock),
	device_adam_expansion_slot_card_interface(mconfig, *this),
	m_ram(*this, "ram", 0x10000, ENDIANNESS_LITTLE)
{
}

//  device_start - device-specific startup

void adam_ram_expansion_device::device_start()
{
}

//  adam_bd_r - buffered data read

uint8_t adam_ram_expansion_device::adam_bd_r(offs_t offset, uint8_t data, int bmreq, int biorq, int aux_rom_cs, int cas1, int cas2)
{
	if (!cas2)
	{
		data = m_ram[offset];
	}

	return data;
}

//  adam_bd_w - buffered data write


void BoardDesignRules::setStopMaskClearanceBounds(const UnsignedLength& min,
                                                  const UnsignedLength& max) {
  if (max >= min) {
    mStopMaskClearanceMin = min;
    mStopMaskClearanceMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Stop mask clearance: MAX must be >= MIN"));
  }
}

/*
 * LibrePCB - Professional EDA for everyone!
 * http://example.com
 *
 */

/*******************************************************************************
 *  Includes
 ******************************************************************************/
#include "boarddesignrules.h"

#include <QtCore>

/*******************************************************************************
 *  Namespace
 ******************************************************************************/
namespace librepcb {

/*******************************************************************************
 *  Constructors / Destructor
 ******************************************************************************/

BoardDesignRules::BoardDesignRules() noexcept
  :  // general attributes
    mName(tr("LibrePCB Default Design Rules")),
    mDescription(),
    // stop mask
    mStopMaskClearanceRatio(Ratio::percent0()),  // 0%
    mStopMaskClearanceMin(100000),  // 0.1mm
    mStopMaskClearanceMax(100000),  // 0.1mm
    mStopMaskMaxViaDrillDiameter(500000),  // 0.5mm
    // cream mask
    mCreamMaskClearanceRatio(Ratio::percent100() / 10),  // 10%
    mCreamMaskClearanceMin(0),  // 0.0mm
    mCreamMaskClearanceMax(1000000),  // 1.0mm
    // restring
    mRestringPadRatio(Ratio::percent100() / 4),  // 25%
    mRestringPadMin(250000),  // 0.25mm
    mRestringPadMax(2000000),  // 2.0mm
    mRestringViaRatio(Ratio::percent100() / 4),  // 25%
    mRestringViaMin(200000),  // 0.2mm
    mRestringViaMax(2000000)  // 2.0mm
{
}

BoardDesignRules::BoardDesignRules(const BoardDesignRules& other)
  : BoardDesignRules() {
  *this = other;
}

BoardDesignRules::BoardDesignRules(const SExpression& node,
                                   const Version& fileFormat)
  : BoardDesignRules()  // this loads all default values!
{
  // general attributes
  mName = deserialize<ElementName>(node.getChild("name/@0"), fileFormat);
  mDescription = node.getChild("description/@0").getValue();
  // stop mask
  if (const SExpression* e = node.tryGetChild("stopmask_clearance_ratio")) {
    mStopMaskClearanceRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("stopmask_clearance_min")) {
    mStopMaskClearanceMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("stopmask_clearance_max")) {
    mStopMaskClearanceMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e =
          node.tryGetChild("stopmask_max_via_drill_diameter")) {
    mStopMaskMaxViaDrillDiameter =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  // cream mask
  if (const SExpression* e = node.tryGetChild("creammask_clearance_ratio")) {
    mCreamMaskClearanceRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("creammask_clearance_min")) {
    mCreamMaskClearanceMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("creammask_clearance_max")) {
    mCreamMaskClearanceMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  // restring
  if (const SExpression* e = node.tryGetChild("restring_pad_ratio")) {
    mRestringPadRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_pad_min")) {
    mRestringPadMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_pad_max")) {
    mRestringPadMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_via_ratio")) {
    mRestringViaRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_via_min")) {
    mRestringViaMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_via_max")) {
    mRestringViaMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }

  // force validating properties, throw exception on error
  try {
    setStopMaskClearanceBounds(mStopMaskClearanceMin, mStopMaskClearanceMax);
    setCreamMaskClearanceBounds(mCreamMaskClearanceMin, mCreamMaskClearanceMax);
    setRestringPadBounds(mRestringPadMin, mRestringPadMax);
    setRestringViaBounds(mRestringViaMin, mRestringViaMax);
  } catch (const Exception& e) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Invalid design rules: %1").arg(e.getMsg()));
  }
}

BoardDesignRules::~BoardDesignRules() noexcept {
}

/*******************************************************************************
 *  Setters
 ******************************************************************************/

void BoardDesignRules::setCreamMaskClearanceBounds(const UnsignedLength& min,
                                                   const UnsignedLength& max) {
  if (max >= min) {
    mCreamMaskClearanceMin = min;
    mCreamMaskClearanceMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Cream mask clearance: MAX must be >= MIN"));
  }
}

void BoardDesignRules::setRestringPadBounds(const UnsignedLength& min,
                                            const UnsignedLength& max) {
  if (max >= min) {
    mRestringPadMin = min;
    mRestringPadMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Restring pads: MAX must be >= MIN"));
  }
}

void BoardDesignRules::setRestringViaBounds(const UnsignedLength& min,
                                            const UnsignedLength& max) {
  if (max >= min) {
    mRestringViaMin = min;
    mRestringViaMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Restring vias: MAX must be >= MIN"));
  }
}

/*******************************************************************************
 *  General Methods
 ******************************************************************************/

void BoardDesignRules::restoreDefaults() noexcept {
  *this = BoardDesignRules();
}

void BoardDesignRules::serialize(SExpression& root) const {
  // general attributes
  root.appendChild("name", mName, true);
  root.appendChild("description", mDescription, true);
  // stop mask
  root.appendChild("stopmask_clearance_ratio", mStopMaskClearanceRatio, true);
  root.appendChild("stopmask_clearance_min", mStopMaskClearanceMin, true);
  root.appendChild("stopmask_clearance_max", mStopMaskClearanceMax, true);
  root.appendChild("stopmask_max_via_drill_diameter",
                   mStopMaskMaxViaDrillDiameter, true);
  // cream mask
  root.appendChild("creammask_clearance_ratio", mCreamMaskClearanceRatio, true);
  root.appendChild("creammask_clearance_min", mCreamMaskClearanceMin, true);
  root.appendChild("creammask_clearance_max", mCreamMaskClearanceMax, true);
  // restring
  root.appendChild("restring_pad_ratio", mRestringPadRatio, true);
  root.appendChild("restring_pad_min", mRestringPadMin, true);
  root.appendChild("restring_pad_max", mRestringPadMax, true);
  root.appendChild("restring_via_ratio", mRestringViaRatio, true);
  root.appendChild("restring_via_min", mRestringViaMin, true);
  root.appendChild("restring_via_max", mRestringViaMax, true);
}

/*******************************************************************************
 *  Helper Methods
 ******************************************************************************/

bool BoardDesignRules::doesViaRequireStopMask(const Length& drillDia) const
    noexcept {
  return (drillDia > *mStopMaskMaxViaDrillDiameter ? true : false);
}

UnsignedLength BoardDesignRules::calcStopMaskClearance(
    const Length& padSize) const noexcept {
  return UnsignedLength(
      qBound(*mStopMaskClearanceMin,
             padSize.scaled(mStopMaskClearanceRatio->toNormalized()),
             *mStopMaskClearanceMax));
}

UnsignedLength BoardDesignRules::calcCreamMaskClearance(
    const Length& padSize) const noexcept {
  return UnsignedLength(
      qBound(*mCreamMaskClearanceMin,
             padSize.scaled(mCreamMaskClearanceRatio->toNormalized()),
             *mCreamMaskClearanceMax));
}

UnsignedLength BoardDesignRules::calcPadRestring(const Length& drillDia) const
    noexcept {
  return UnsignedLength(qBound(
      *mRestringPadMin, drillDia.scaled(mRestringPadRatio->toNormalized()),
      *mRestringPadMax));
}

UnsignedLength BoardDesignRules::calcViaRestring(const Length& drillDia) const
    noexcept {
  return UnsignedLength(qBound(
      *mRestringViaMin, drillDia.scaled(mRestringViaRatio->toNormalized()),
      *mRestringViaMax));
}

/*******************************************************************************
 *  Operator Overloadings
 ******************************************************************************/

BoardDesignRules& BoardDesignRules::operator=(
    const BoardDesignRules& rhs) noexcept {
  // general attributes
  mName = rhs.mName;
  mDescription = rhs.mDescription;
  // stop mask
  mStopMaskClearanceRatio = rhs.mStopMaskClearanceRatio;
  mStopMaskClearanceMin = rhs.mStopMaskClearanceMin;
  mStopMaskClearanceMax = rhs.mStopMaskClearanceMax;
  mStopMaskMaxViaDrillDiameter = rhs.mStopMaskMaxViaDrillDiameter;
  // cream mask
  mCreamMaskClearanceRatio = rhs.mCreamMaskClearanceRatio;
  mCreamMaskClearanceMin = rhs.mCreamMaskClearanceMin;
  mCreamMaskClearanceMax = rhs.mCreamMaskClearanceMax;
  // restring
  mRestringPadRatio = rhs.mRestringPadRatio;
  mRestringPadMin = rhs.mRestringPadMin;
  mRestringPadMax = rhs.mRestringPadMax;
  mRestringViaRatio = rhs.mRestringViaRatio;
  mRestringViaMin = rhs.mRestringViaMin;
  mRestringViaMax = rhs.mRestringViaMax;
  return *this;
}

/*******************************************************************************
 *  End of File
 ******************************************************************************/

}  // namespace librepcb


try {
    setStopMaskClearanceBounds(mStopMaskClearanceMin, mStopMaskClearanceMax);
    setCreamMaskClearanceBounds(mCreamMaskClearanceMin, mCreamMaskClearanceMax);
    setRestringPadBounds(mRestringPadMin, mRestringPadMax);
    setRestringViaBounds(mRestringViaMin, mRestringViaMax);
  } catch (const Exception& e) {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Invalid design rules: %1").arg(e.getMsg()));
  }

/*
 * LibrePCB - Professional EDA for everyone!
 * http://example.com
 *
 */

/*******************************************************************************
 *  Includes
 ******************************************************************************/
#include "boarddesignrules.h"

#include <QtCore>

/*******************************************************************************
 *  Namespace
 ******************************************************************************/
namespace librepcb {

/*******************************************************************************
 *  Constructors / Destructor
 ******************************************************************************/

BoardDesignRules::BoardDesignRules() noexcept
  :  // general attributes
    mName(tr("LibrePCB Default Design Rules")),
    mDescription(),
    // stop mask
    mStopMaskClearanceRatio(Ratio::percent0()),  // 0%
    mStopMaskClearanceMin(100000),  // 0.1mm
    mStopMaskClearanceMax(100000),  // 0.1mm
    mStopMaskMaxViaDrillDiameter(500000),  // 0.5mm
    // cream mask
    mCreamMaskClearanceRatio(Ratio::percent100() / 10),  // 10%
    mCreamMaskClearanceMin(0),  // 0.0mm
    mCreamMaskClearanceMax(1000000),  // 1.0mm
    // restring
    mRestringPadRatio(Ratio::percent100() / 4),  // 25%
    mRestringPadMin(250000),  // 0.25mm
    mRestringPadMax(2000000),  // 2.0mm
    mRestringViaRatio(Ratio::percent100() / 4),  // 25%
    mRestringViaMin(200000),  // 0.2mm
    mRestringViaMax(2000000)  // 2.0mm
{
}

BoardDesignRules::BoardDesignRules(const BoardDesignRules& other)
  : BoardDesignRules() {
  *this = other;
}

BoardDesignRules::BoardDesignRules(const SExpression& node,
                                   const Version& fileFormat)
  : BoardDesignRules()  // this loads all default values!
{
  // general attributes
  mName = deserialize<ElementName>(node.getChild("name/@0"), fileFormat);
  mDescription = node.getChild("description/@0").getValue();
  // stop mask
  if (const SExpression* e = node.tryGetChild("stopmask_clearance_ratio")) {
    mStopMaskClearanceRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("stopmask_clearance_min")) {
    mStopMaskClearanceMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("stopmask_clearance_max")) {
    mStopMaskClearanceMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e =
          node.tryGetChild("stopmask_max_via_drill_diameter")) {
    mStopMaskMaxViaDrillDiameter =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  // cream mask
  if (const SExpression* e = node.tryGetChild("creammask_clearance_ratio")) {
    mCreamMaskClearanceRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("creammask_clearance_min")) {
    mCreamMaskClearanceMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("creammask_clearance_max")) {
    mCreamMaskClearanceMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  // restring
  if (const SExpression* e = node.tryGetChild("restring_pad_ratio")) {
    mRestringPadRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_pad_min")) {
    mRestringPadMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_pad_max")) {
    mRestringPadMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_via_ratio")) {
    mRestringViaRatio =
        deserialize<UnsignedRatio>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_via_min")) {
    mRestringViaMin =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }
  if (const SExpression* e = node.tryGetChild("restring_via_max")) {
    mRestringViaMax =
        deserialize<UnsignedLength>(e->getChild("@0"), fileFormat);
  }

  // force validating properties, throw exception on error

}

BoardDesignRules::~BoardDesignRules() noexcept {
}

/*******************************************************************************
 *  Setters
 ******************************************************************************/

void BoardDesignRules::setStopMaskClearanceBounds(const UnsignedLength& min,
                                                  const UnsignedLength& max) {
  if (max >= min) {
    mStopMaskClearanceMin = min;
    mStopMaskClearanceMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Stop mask clearance: MAX must be >= MIN"));
  }
}

void BoardDesignRules::setCreamMaskClearanceBounds(const UnsignedLength& min,
                                                   const UnsignedLength& max) {
  if (max >= min) {
    mCreamMaskClearanceMin = min;
    mCreamMaskClearanceMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Cream mask clearance: MAX must be >= MIN"));
  }
}

void BoardDesignRules::setRestringPadBounds(const UnsignedLength& min,
                                            const UnsignedLength& max) {
  if (max >= min) {
    mRestringPadMin = min;
    mRestringPadMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Restring pads: MAX must be >= MIN"));
  }
}

void BoardDesignRules::setRestringViaBounds(const UnsignedLength& min,
                                            const UnsignedLength& max) {
  if (max >= min) {
    mRestringViaMin = min;
    mRestringViaMax = max;
  } else {
    throw RuntimeError(__FILE__, __LINE__,
                       tr("Restring vias: MAX must be >= MIN"));
  }
}

/*******************************************************************************
 *  General Methods
 ******************************************************************************/

void BoardDesignRules::restoreDefaults() noexcept {
  *this = BoardDesignRules();
}

void BoardDesignRules::serialize(SExpression& root) const {
  // general attributes
  root.appendChild("name", mName, true);
  root.appendChild("description", mDescription, true);
  // stop mask
  root.appendChild("stopmask_clearance_ratio", mStopMaskClearanceRatio, true);
  root.appendChild("stopmask_clearance_min", mStopMaskClearanceMin, true);
  root.appendChild("stopmask_clearance_max", mStopMaskClearanceMax, true);
  root.appendChild("stopmask_max_via_drill_diameter",
                   mStopMaskMaxViaDrillDiameter, true);
  // cream mask
  root.appendChild("creammask_clearance_ratio", mCreamMaskClearanceRatio, true);
  root.appendChild("creammask_clearance_min", mCreamMaskClearanceMin, true);
  root.appendChild("creammask_clearance_max", mCreamMaskClearanceMax, true);
  // restring
  root.appendChild("restring_pad_ratio", mRestringPadRatio, true);
  root.appendChild("restring_pad_min", mRestringPadMin, true);
  root.appendChild("restring_pad_max", mRestringPadMax, true);
  root.appendChild("restring_via_ratio", mRestringViaRatio, true);
  root.appendChild("restring_via_min", mRestringViaMin, true);
  root.appendChild("restring_via_max", mRestringViaMax, true);
}

/*******************************************************************************
 *  Helper Methods
 ******************************************************************************/

bool BoardDesignRules::doesViaRequireStopMask(const Length& drillDia) const
    noexcept {
  return (drillDia > *mStopMaskMaxViaDrillDiameter ? true : false);
}

UnsignedLength BoardDesignRules::calcStopMaskClearance(
    const Length& padSize) const noexcept {
  return UnsignedLength(
      qBound(*mStopMaskClearanceMin,
             padSize.scaled(mStopMaskClearanceRatio->toNormalized()),
             *mStopMaskClearanceMax));
}

UnsignedLength BoardDesignRules::calcCreamMaskClearance(
    const Length& padSize) const noexcept {
  return UnsignedLength(
      qBound(*mCreamMaskClearanceMin,
             padSize.scaled(mCreamMaskClearanceRatio->toNormalized()),
             *mCreamMaskClearanceMax));
}

UnsignedLength BoardDesignRules::calcPadRestring(const Length& drillDia) const
    noexcept {
  return UnsignedLength(qBound(
      *mRestringPadMin, drillDia.scaled(mRestringPadRatio->toNormalized()),
      *mRestringPadMax));
}

UnsignedLength BoardDesignRules::calcViaRestring(const Length& drillDia) const
    noexcept {
  return UnsignedLength(qBound(
      *mRestringViaMin, drillDia.scaled(mRestringViaRatio->toNormalized()),
      *mRestringViaMax));
}

/*******************************************************************************
 *  Operator Overloadings
 ******************************************************************************/

BoardDesignRules& BoardDesignRules::operator=(
    const BoardDesignRules& rhs) noexcept {
  // general attributes
  mName = rhs.mName;
  mDescription = rhs.mDescription;
  // stop mask
  mStopMaskClearanceRatio = rhs.mStopMaskClearanceRatio;
  mStopMaskClearanceMin = rhs.mStopMaskClearanceMin;
  mStopMaskClearanceMax = rhs.mStopMaskClearanceMax;
  mStopMaskMaxViaDrillDiameter = rhs.mStopMaskMaxViaDrillDiameter;
  // cream mask
  mCreamMaskClearanceRatio = rhs.mCreamMaskClearanceRatio;
  mCreamMaskClearanceMin = rhs.mCreamMaskClearanceMin;
  mCreamMaskClearanceMax = rhs.mCreamMaskClearanceMax;
  // restring
  mRestringPadRatio = rhs.mRestringPadRatio;
  mRestringPadMin = rhs.mRestringPadMin;
  mRestringPadMax = rhs.mRestringPadMax;
  mRestringViaRatio = rhs.mRestringViaRatio;
  mRestringViaMin = rhs.mRestringViaMin;
  mRestringViaMax = rhs.mRestringViaMax;
  return *this;
}

/*******************************************************************************
 *  End of File
 ******************************************************************************/

}  // namespace librepcb