/* Copyright (c) 2010 Michael Lidgren

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom
the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

*/
using System;
using System.Net;

namespace Lidgren.Network
{
	/// <summary>
	/// Partly immutable after NetPeer has been initialized
	/// </summary>
	public sealed class NetPeerConfiguration
	{
		// Maximum transmission unit
		// Ethernet can take 1500 bytes of payload, so lets stay below that.
		// The aim is for a max full packet to be 1440 bytes (30 x 48 bytes, lower than 1468)
		// -20 bytes IP header
		//  -8 bytes UDP header
		//  -4 bytes to be on the safe side and align to 8-byte boundary
		// Total 1408 bytes
		// Note that lidgren headers (5 bytes) are not included here; since it's part of the "mtu payload"

		/// <summary>
		/// Default MTU value in bytes
		/// </summary>
		public const int kDefaultMTU = 1408;

		private const string c_isLockedMessage = "You may not modify the NetPeerConfiguration after it has been used to initialize a NetPeer";

		private bool m_isLocked;
		private readonly string m_appIdentifier;
		private string m_networkThreadName;
		private IPAddress m_localAddress;
		private IPAddress m_broadcastAddress;
        private bool m_dualStack;

		internal bool m_acceptIncomingConnections;
		internal int m_maximumConnections;
		internal int m_defaultOutgoingMessageCapacity;
		internal float m_pingInterval;
		internal bool m_useMessageRecycling;
		internal int m_recycledCacheMaxCount;
		internal float m_connectionTimeout;
		internal bool m_enableUPnP;
		internal bool m_autoFlushSendQueue;
		private NetUnreliableSizeBehaviour m_unreliableSizeBehaviour;
		internal bool m_suppressUnreliableUnorderedAcks;

		internal NetIncomingMessageType m_disabledTypes;
		internal int m_port;
		internal int m_receiveBufferSize;
		internal int m_sendBufferSize;
		internal float m_resendHandshakeInterval;
		internal int m_maximumHandshakeAttempts;

		// bad network simulation
		internal float m_loss;
		internal float m_duplicates;
		internal float m_minimumOneWayLatency;
		internal float m_randomOneWayLatency;

		// MTU
		internal int m_maximumTransmissionUnit;
		internal bool m_autoExpandMTU;
		internal float m_expandMTUFrequency;
		internal int m_expandMTUFailAttempts;

		/// <summary>
		/// NetPeerConfiguration constructor
		/// </summary>
		public NetPeerConfiguration(string appIdentifier)
		{
			if (string.IsNullOrEmpty(appIdentifier))
				throw new NetException("App identifier must be at least one character long");
			m_appIdentifier = appIdentifier;

			//
			// default values
			//
			m_disabledTypes = NetIncomingMessageType.ConnectionApproval | NetIncomingMessageType.UnconnectedData | NetIncomingMessageType.VerboseDebugMessage | NetIncomingMessageType.ConnectionLatencyUpdated | NetIncomingMessageType.NatIntroductionSuccess;
			m_networkThreadName = "Lidgren network thread";
			m_localAddress = IPAddress.Any;
			m_broadcastAddress = IPAddress.Broadcast;
			var ip = NetUtility.GetBroadcastAddress();
			if (ip != null)
			{
				m_broadcastAddress = ip;
			}
			m_port = 0;
			m_receiveBufferSize = 131071;
			m_sendBufferSize = 131071;
			m_acceptIncomingConnections = false;
			m_maximumConnections = 32;
			m_defaultOutgoingMessageCapacity = 16;
			m_pingInterval = 4.0f;
			m_connectionTimeout = 25.0f;
			m_useMessageRecycling = true;
			m_recycledCacheMaxCount = 64;
			m_resendHandshakeInterval = 3.0f;
			m_maximumHandshakeAttempts = 5;
			m_autoFlushSendQueue = true;
			m_suppressUnreliableUnorderedAcks = false;

			m_maximumTransmissionUnit = kDefaultMTU;
			m_autoExpandMTU = false;
			m_expandMTUFrequency = 2.0f;
			m_expandMTUFailAttempts = 5;
			m_unreliableSizeBehaviour = NetUnreliableSizeBehaviour.IgnoreMTU;

			m_loss = 0.0f;
			m_minimumOneWayLatency = 0.0f;
			m_randomOneWayLatency = 0.0f;
			m_duplicates = 0.0f;

			m_isLocked = false;
		}

		internal void Lock()
		{
			m_isLocked = true;
		}

		/// <summary>
		/// Gets the identifier of this application; the library can only connect to matching app identifier peers
		/// </summary>
		public string AppIdentifier
		{
			get { return m_appIdentifier; }
		}

		/// <summary>
		/// Enables receiving of the specified type of message
		/// </summary>
		public void EnableMessageType(NetIncomingMessageType type)
		{
			m_disabledTypes &= (~type);
		}

		/// <summary>
		/// Disables receiving of the specified type of message
		/// </summary>
		public void DisableMessageType(NetIncomingMessageType type)
		{
			m_disabledTypes |= type;
		}

		/// <summary>
		/// Enables or disables receiving of the specified type of message
		/// </summary>
		public void SetMessageTypeEnabled(NetIncomingMessageType type, bool enabled)
		{
			if (enabled)
				m_disabledTypes &= (~type);
			else
				m_disabledTypes |= type;
		}

		/// <summary>
		/// Gets if receiving of the specified type of message is enabled
		/// </summary>
		public bool IsMessageTypeEnabled(NetIncomingMessageType type)
		{
			return !((m_disabledTypes & type) == type);
		}

		/// <summary>
		/// Gets or sets the behaviour of unreliable sends above MTU
		/// </summary>
		public NetUnreliableSizeBehaviour UnreliableSizeBehaviour
		{
			get { return m_unreliableSizeBehaviour; }
			set { m_unreliableSizeBehaviour = value; }
		}

		/// <summary>
		/// Gets or sets the name of the library network thread. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public string NetworkThreadName
		{
			get { return m_networkThreadName; }
			set
			{
				if (m_isLocked)
					throw new NetException("NetworkThreadName may not be set after the NetPeer which uses the configuration has been started");
				m_networkThreadName = value;
			}
		}

		/// <summary>
		/// Gets or sets the maximum amount of connections this peer can hold. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public int MaximumConnections
		{
			get { return m_maximumConnections; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_maximumConnections = value;
			}
		}

		/// <summary>
		/// Gets or sets the maximum amount of bytes to send in a single packet, excluding ip, udp and lidgren headers. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public int MaximumTransmissionUnit
		{
			get { return m_maximumTransmissionUnit; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				if (value < 1 || value >= ((ushort.MaxValue + 1) / 8))
					throw new NetException("MaximumTransmissionUnit must be between 1 and " + (((ushort.MaxValue + 1) / 8) - 1) + " bytes");
				m_maximumTransmissionUnit = value;
			}
		}

		/// <summary>
		/// Gets or sets the default capacity in bytes when NetPeer.CreateMessage() is called without argument
		/// </summary>
		public int DefaultOutgoingMessageCapacity
		{
			get { return m_defaultOutgoingMessageCapacity; }
			set { m_defaultOutgoingMessageCapacity = value; }
		}

		/// <summary>
		/// Gets or sets the time between latency calculating pings
		/// </summary>
		public float PingInterval
		{
			get { return m_pingInterval; }
			set { m_pingInterval = value; }
		}

		/// <summary>
		/// Gets or sets if the library should recycling messages to avoid excessive garbage collection. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public bool UseMessageRecycling
		{
			get { return m_useMessageRecycling; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_useMessageRecycling = value;
			}
		}

		/// <summary>
		/// Gets or sets the maximum number of incoming/outgoing messages to keep in the recycle cache.
		/// </summary>
		public int RecycledCacheMaxCount
		{
			get { return m_recycledCacheMaxCount; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_recycledCacheMaxCount = value;
			}
		}

		/// <summary>
		/// Gets or sets the number of seconds timeout will be postponed on a successful ping/pong
		/// </summary>
		public float ConnectionTimeout
		{
			get { return m_connectionTimeout; }
			set
			{
				if (value < m_pingInterval)
					throw new NetException("Connection timeout cannot be lower than ping interval!");
				m_connectionTimeout = value;
			}
		}

		/// <summary>
		/// Enables UPnP support; enabling port forwarding and getting external ip
		/// </summary>
		public bool EnableUPnP
		{
			get { return m_enableUPnP; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_enableUPnP = value;
			}
		}

		/// <summary>
		/// Enables or disables automatic flushing of the send queue. If disabled, you must manully call NetPeer.FlushSendQueue() to flush sent messages to network.
		/// </summary>
		public bool AutoFlushSendQueue
		{
			get { return m_autoFlushSendQueue; }
			set { m_autoFlushSendQueue = value; }
		}

		/// <summary>
		/// If true, will not send acks for unreliable unordered messages. This will save bandwidth, but disable flow control and duplicate detection for this type of messages.
		/// </summary>
		public bool SuppressUnreliableUnorderedAcks
		{
			get { return m_suppressUnreliableUnorderedAcks; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_suppressUnreliableUnorderedAcks = value;
			}
		}

		/// <summary>
		/// Gets or sets the local ip address to bind to. Defaults to IPAddress.Any. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public IPAddress LocalAddress
		{
			get { return m_localAddress; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_localAddress = value;
			}
		}

        /// <summary>
        /// Gets or sets a value indicating whether the library should use IPv6 dual stack mode.
        /// If you enable this you should make sure that the <see cref="LocalAddress"/> is an IPv6 address.
        /// Cannot be changed once NetPeer is initialized.
        /// </summary>
        public bool DualStack
        {
            get { return m_dualStack;  }
            set
            {
                if (m_isLocked)
                    throw new NetException(c_isLockedMessage);
                m_dualStack = value;
            }
        }

        /// <summary>
        /// Gets or sets the local broadcast address to use when broadcasting
        /// </summary>
        public IPAddress BroadcastAddress
		{
			get { return m_broadcastAddress; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_broadcastAddress = value;
			}
		}

		/// <summary>
		/// Gets or sets the local port to bind to. Defaults to 0. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public int Port
		{
			get { return m_port; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_port = value;
			}
		}

		/// <summary>
		/// Gets or sets the size in bytes of the receiving buffer. Defaults to 131071 bytes. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public int ReceiveBufferSize
		{
			get { return m_receiveBufferSize; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_receiveBufferSize = value;
			}
		}

		/// <summary>
		/// Gets or sets the size in bytes of the sending buffer. Defaults to 131071 bytes. Cannot be changed once NetPeer is initialized.
		/// </summary>
		public int SendBufferSize
		{
			get { return m_sendBufferSize; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_sendBufferSize = value;
			}
		}

		/// <summary>
		/// Gets or sets if the NetPeer should accept incoming connections. This is automatically set to true in NetServer and false in NetClient.
		/// </summary>
		public bool AcceptIncomingConnections
		{
			get { return m_acceptIncomingConnections; }
			set { m_acceptIncomingConnections = value; }
		}

		/// <summary>
		/// Gets or sets the number of seconds between handshake attempts
		/// </summary>
		public float ResendHandshakeInterval
		{
			get { return m_resendHandshakeInterval; }
			set { m_resendHandshakeInterval = value; }
		}

		/// <summary>
		/// Gets or sets the maximum number of handshake attempts before failing to connect
		/// </summary>
		public int MaximumHandshakeAttempts
		{
			get { return m_maximumHandshakeAttempts; }
			set
			{
				if (value < 1)
					throw new NetException("MaximumHandshakeAttempts must be at least 1");
				m_maximumHandshakeAttempts = value;
			}
		}

		/// <summary>
		/// Gets or sets if the NetPeer should send large messages to try to expand the maximum transmission unit size
		/// </summary>
		public bool AutoExpandMTU
		{
			get { return m_autoExpandMTU; }
			set
			{
				if (m_isLocked)
					throw new NetException(c_isLockedMessage);
				m_autoExpandMTU = value;
			}
		}

		/// <summary>
		/// Gets or sets how often to send large messages to expand MTU if AutoExpandMTU is enabled
		/// </summary>
		public float ExpandMTUFrequency
		{
			get { return m_expandMTUFrequency; }
			set { m_expandMTUFrequency = value; }
		}

		/// <summary>
		/// Gets or sets the number of failed expand mtu attempts to perform before setting final MTU
		/// </summary>
		public int ExpandMTUFailAttempts
		{
			get { return m_expandMTUFailAttempts; }
			set { m_expandMTUFailAttempts = value; }
		}

#if DEBUG
		/// <summary>
		/// Gets or sets the simulated amount of sent packets lost from 0.0f to 1.0f
		/// </summary>
		public float SimulatedLoss
		{
			get { return m_loss; }
			set { m_loss = value; }
		}

		/// <summary>
		/// Gets or sets the minimum simulated amount of one way latency for sent packets in seconds
		/// </summary>
		public float SimulatedMinimumLatency
		{
			get { return m_minimumOneWayLatency; }
			set { m_minimumOneWayLatency = value; }
		}

		/// <summary>
		/// Gets or sets the simulated added random amount of one way latency for sent packets in seconds
		/// </summary>
		public float SimulatedRandomLatency
		{
			get { return m_randomOneWayLatency; }
			set { m_randomOneWayLatency = value; }
		}

		/// <summary>
		/// Gets the average simulated one way latency in seconds
		/// </summary>
		public float SimulatedAverageLatency
		{
			get { return m_minimumOneWayLatency + (m_randomOneWayLatency * 0.5f); }
		}

		/// <summary>
		/// Gets or sets the simulated amount of duplicated packets from 0.0f to 1.0f
		/// </summary>
		public float SimulatedDuplicatesChance
		{
			get { return m_duplicates; }
			set { m_duplicates = value; }
		}
#endif

		/// <summary>
		/// Creates a memberwise shallow clone of this configuration
		/// </summary>
		public NetPeerConfiguration Clone()
		{
			NetPeerConfiguration retval = this.MemberwiseClone() as NetPeerConfiguration;
			retval.m_isLocked = false;
			return retval;
		}
	}

	/// <summary>
	/// Behaviour of unreliable sends above MTU
	/// </summary>
	public enum NetUnreliableSizeBehaviour
	{
		/// <summary>
		/// Sending an unreliable message will ignore MTU and send everything in a single packet; this is the new default
		/// </summary>
		IgnoreMTU = 0,

		/// <summary>
		/// Old behaviour; use normal fragmentation for unreliable messages - if a fragment is dropped, memory for received fragments are never reclaimed!
		/// </summary>
		NormalFragmentation = 1,

		/// <summary>
		/// Alternate behaviour; just drops unreliable messages above MTU
		/// </summary>
		DropAboveMTU = 2,
	}
}
