using Steamworks.Data;
using System;

namespace Steamworks
{
	public class ConnectionManager
	{
		/// <summary>
		/// An optional interface to use instead of deriving
		/// </summary>
		public IConnectionManager? Interface { get; set; }

		/// <summary>
		/// The actual connection we're managing
		/// </summary>
		public Connection Connection;

		/// <summary>
		/// The last received ConnectionInfo
		/// </summary>
		public ConnectionInfo ConnectionInfo { get; internal set; }

		public bool Connected = false;
		public bool Connecting = true;

		public string ConnectionName
		{
			get => Connection.ConnectionName;
			set => Connection.ConnectionName = value;
		}

		public long UserData 
		{
			get => Connection.UserData;
			set => Connection.UserData = value;
		}

		public void Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" )
		{
			Connection.Close( linger, reasonCode, debugString );
		}

		public override string ToString() => Connection.ToString();

		public virtual void OnConnectionChanged( ConnectionInfo info )
		{
			ConnectionInfo = info;

			//
			// Some notes:
			// - Update state before the callbacks, in case an exception is thrown
			// - ConnectionState.None happens when a connection is destroyed, even if it was already disconnected (ClosedByPeer / ProblemDetectedLocally)
			//
			switch ( info.State )
			{
				case ConnectionState.Connecting:
					if ( !Connecting && !Connected )
					{
						Connecting = true;

						OnConnecting( info );
					}
					break;
				case ConnectionState.Connected:
					if ( Connecting && !Connected )
					{
						Connecting = false;
						Connected = true;

						OnConnected( info );
					}
					break;
				case ConnectionState.ClosedByPeer:
				case ConnectionState.ProblemDetectedLocally:
				case ConnectionState.None:
					if ( Connecting || Connected )
					{
						Connecting = false;
						Connected = false;

						OnDisconnected( info );
					}
					break;
			}
		}

		/// <summary>
		/// We're trying to connect!
		/// </summary>
		public virtual void OnConnecting( ConnectionInfo info )
		{
			Interface?.OnConnecting( info );
		}

		/// <summary>
		/// Client is connected. They move from connecting to Connections
		/// </summary>
		public virtual void OnConnected( ConnectionInfo info )
		{
			Interface?.OnConnected( info );
		}

		/// <summary>
		/// The connection has been closed remotely or disconnected locally. Check data.State for details.
		/// </summary>
		public virtual void OnDisconnected( ConnectionInfo info )
		{
			Interface?.OnDisconnected( info );
		}

		public unsafe int Receive( int bufferSize = 32, bool receiveToEnd = true )
		{
			if (SteamNetworkingSockets.Internal is null) { return 0; }

			if ( bufferSize < 1 || bufferSize > 256 ) throw new ArgumentOutOfRangeException( nameof( bufferSize ) );

			int totalProcessed = 0;
			NetMsg** messageBuffer = stackalloc NetMsg*[bufferSize];
			
			while ( true )
			{
				int processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnConnection( Connection, new IntPtr( &messageBuffer[0] ), bufferSize );
				totalProcessed += processed;

				try
				{
					for ( int i = 0; i < processed; i++ )
					{
						ReceiveMessage( ref messageBuffer[i] );
					}
				}
				catch
				{
					for ( int i = 0; i < processed; i++ )
					{
						if ( messageBuffer[i] != null )
						{
							NetMsg.InternalRelease( messageBuffer[i] );
						}
					}

					throw;
				}
				

				//
				// Keep going if receiveToEnd and we filled the buffer
				//
				if ( !receiveToEnd || processed < bufferSize )
					break;
			}

			return totalProcessed;
		}

		/// <summary>
		/// Sends a message to multiple connections.
		/// </summary>
		/// <param name="connections">The connections to send the message to.</param>
		/// <param name="connectionCount">The number of connections to send the message to, to allow reusing the connections array.</param>
		/// <param name="ptr">Pointer to the message data.</param>
		/// <param name="size">Size of the message data.</param>
		/// <param name="sendType">Flags to control delivery of the message.</param>
		/// <param name="results">An optional array to hold the results of sending the messages for each connection.</param>
		public unsafe void SendMessages( Connection[] connections, int connectionCount, IntPtr ptr, int size, SendType sendType = SendType.Reliable, Result[]? results = null )
		{
			if ( connections == null )
				throw new ArgumentNullException( nameof( connections ) );
			if ( connectionCount < 0 || connectionCount > connections.Length )
				throw new ArgumentException( "`connectionCount` must be between 0 and `connections.Length`", nameof( connectionCount ) );
			if ( results != null && connectionCount > results.Length )
				throw new ArgumentException( "`results` must have at least `connectionCount` entries", nameof( results ) );
			if ( connectionCount > 1024 ) // restricting this because we stack allocate based on this value
				throw new ArgumentOutOfRangeException( nameof( connectionCount ) );
			if ( ptr == IntPtr.Zero )
				throw new ArgumentNullException( nameof( ptr ) );
			if ( size == 0 )
				throw new ArgumentException( "`size` cannot be zero", nameof( size ) );

			if ( SteamNetworkingSockets.Internal is null )
				return;
			if ( connectionCount == 0 )
				return;

			// SendMessages does not make a copy of the data. We will need to copy because we don't want to force the caller to keep the pointer valid.
			//   1. We don't want a copy per message. They all refer to the same data. This is the benefit of using Broadcast vs. many sends.
			//   2. We need to use unmanaged memory. Managed memory may move around and invalidate pointers so it's not an option.
			//   3. We'll use a reference counter and custom free() function to release this unmanaged memory.
			var copyPtr = BufferManager.Get( size, connectionCount );
			Buffer.MemoryCopy( (void*)ptr, (void*)copyPtr, size, size );

			var messages = stackalloc NetMsg*[connectionCount];
			var messageNumberOrResults = stackalloc long[results != null ? connectionCount : 0];

			for ( var i = 0; i < connectionCount; i++ )
			{
				messages[i] = SteamNetworkingUtils.AllocateMessage();
				messages[i]->Connection = connections[i];
				messages[i]->Flags = sendType;
				messages[i]->DataPtr = copyPtr;
				messages[i]->DataSize = size;
				messages[i]->FreeDataPtr = BufferManager.FreeFunctionPointer;
			}

			SteamNetworkingSockets.Internal.SendMessages( connectionCount, messages, messageNumberOrResults );

			if (results == null)
				return;

			for ( var i = 0; i < connectionCount; i++ )
			{
				if ( messageNumberOrResults[i] < 0 )
				{
					results[i] = (Result)( -messageNumberOrResults[i] );
				}
				else
				{
					results[i] = Result.OK;
				}
			}
		}

		/// <summary>
		/// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and 
		/// you're not creating a new one every frame (like using .ToArray())
		/// </summary>
		public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, SendType sendType = SendType.Reliable, Result[]? results = null )
		{
			fixed ( byte* ptr = data )
			{
				SendMessages( connections, connectionCount, (IntPtr)ptr, data.Length, sendType, results );
			}
		}

		/// <summary>
		/// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and 
		/// you're not creating a new one every frame (like using .ToArray())
		/// </summary>
		public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, int offset, int length, SendType sendType = SendType.Reliable, Result[]? results = null )
		{
			fixed ( byte* ptr = data )
			{
				SendMessages( connections, connectionCount, (IntPtr)ptr + offset, length, sendType, results );
			}
		}

		/// <summary>
		/// This creates a ton of garbage - so don't do anything with this beyond testing!
		/// </summary>
		public void SendMessages( Connection[] connections, int connectionCount, string str, SendType sendType = SendType.Reliable, Result[]? results = null )
		{
			var bytes = Utility.Utf8NoBom.GetBytes( str );
			SendMessages( connections, connectionCount, bytes, sendType, results );
		}

		internal unsafe void ReceiveMessage( ref NetMsg* msg )
		{
			try
			{
				OnMessage( msg->DataPtr, msg->DataSize, msg->RecvTime, msg->MessageNumber, msg->Channel );
			}
			finally
			{
				//
				// Releases the message
				//
				NetMsg.InternalRelease( msg );
				msg = null;
			}
		}

		public virtual void OnMessage( IntPtr data, int size, long messageNum, long recvTime, int channel )
		{
			Interface?.OnMessage( data, size, messageNum, recvTime, channel );
		}
	}
}
