using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Steamworks.Data;
using System.Diagnostics.CodeAnalysis;

namespace Steamworks
{
	/// <summary>
	/// Provides the core of the Steam Game Servers API
	/// </summary>
	public partial class SteamServer : SteamServerClass<SteamServer>
	{
		internal static ISteamGameServer? Internal => Interface as ISteamGameServer;

		internal override void InitializeInterface( bool server )
		{
			SetInterface( server, new ISteamGameServer( server ) );
			InstallEvents();
		}

		public static bool IsValid => Internal != null && Internal.IsValid;

		internal static void InstallEvents()
		{
            Dispatch.Install<ValidateAuthTicketResponse_t>( x => OnValidateAuthTicketResponse?.Invoke( x.SteamID, x.OwnerSteamID, x.AuthSessionResponse ), true );
			Dispatch.Install<SteamServersConnected_t>( x => OnSteamServersConnected?.Invoke(), true );
			Dispatch.Install<SteamServerConnectFailure_t>( x => OnSteamServerConnectFailure?.Invoke( x.Result, x.StillRetrying ), true );
			Dispatch.Install<SteamServersDisconnected_t>( x => OnSteamServersDisconnected?.Invoke( x.Result ), true );
			Dispatch.Install<SteamNetAuthenticationStatus_t>(x => OnSteamNetAuthenticationStatus?.Invoke(x.Avail), true);
		}

		/// <summary>
		/// User has been authed or rejected
		/// </summary>
		public static event Action<SteamId, SteamId, AuthResponse>? OnValidateAuthTicketResponse;

		/// <summary>
		/// Called when a connections to the Steam back-end has been established.
		/// This means the server now is logged on and has a working connection to the Steam master server.
		/// </summary>
		public static event Action? OnSteamServersConnected;

		/// <summary>
		/// This will occur periodically if the Steam client is not connected, and has failed when retrying to establish a connection (result, stilltrying)
		/// </summary>
		public static event Action<Result, bool>? OnSteamServerConnectFailure;

		/// <summary>
		/// Disconnected from Steam
		/// </summary>
		public static event Action<Result>? OnSteamServersDisconnected;

		/// <summary>
		/// Called when authentication status changes, useful for grabbing SteamId once aavailability is current
		/// </summary>
		public static event Action<SteamNetworkingAvailability>? OnSteamNetAuthenticationStatus;


		/// <summary>
		/// Initialize the steam server.
		/// If asyncCallbacks is false you need to call RunCallbacks manually every frame.
		/// </summary>
		public static void Init( AppId appid, SteamServerInit init, bool asyncCallbacks = true )
		{
			if ( IsValid )
				throw new System.Exception( "Calling SteamServer.Init but is already initialized" );

			uint ipaddress = 0; // Any Port

			if ( init.SteamPort == 0 )
				init = init.WithRandomSteamPort();

			if ( init.IpAddress != null )
				ipaddress = Utility.IpToInt32( init.IpAddress );

			System.Environment.SetEnvironmentVariable( "SteamAppId", appid.ToString() );
			System.Environment.SetEnvironmentVariable( "SteamGameId", appid.ToString() );

			//
			// Get other interfaces
			//
			if ( !SteamInternal.GameServer_Init( ipaddress, init.SteamPort, init.GamePort, init.QueryPort, (int)init.Mode, init.VersionString ) )
			{
				throw new System.Exception( $"InitGameServer returned false ({ipaddress},{init.SteamPort},{init.GamePort},{init.QueryPort},{init.Mode},\"{init.VersionString}\")" );
			}

			//
			// Dispatch is responsible for pumping the
			// event loop.
			//
			Dispatch.Init();
			Dispatch.ServerPipe = SteamGameServer.GetHSteamPipe();

			AddInterface<SteamServer>();
			AddInterface<SteamUtils>();
			AddInterface<SteamNetworking>();
			AddInterface<SteamServerStats>();
			//AddInterface<ISteamHTTP>();
			AddInterface<SteamInventory>();
			AddInterface<SteamUGC>();
			AddInterface<SteamApps>();

			AddInterface<SteamNetworkingUtils>();
			AddInterface<SteamNetworkingSockets>();

			//
			// Initial settings
			//
			AutomaticHeartbeats = true;
			MaxPlayers = 32;
			BotCount = 0;
			Product = $"{appid.Value}";
			ModDir = init.ModDir;
			GameDescription = init.GameDescription;
			Passworded = false;
			DedicatedServer = init.DedicatedServer;

			if ( asyncCallbacks )
			{
				//
				// This will keep looping in the background every 16 ms
				// until we shut down.
				//
				Dispatch.LoopServerAsync();
			}
		}

		internal static void AddInterface<T>() where T : SteamClass, new()
		{
			var t = new T();
			t.InitializeInterface( true );
			openInterfaces.Add( t );
		}

		static readonly List<SteamClass> openInterfaces = new List<SteamClass>();

		internal static void ShutdownInterfaces()
		{
			foreach ( var e in openInterfaces )
			{
				e.DestroyInterface( true );
			}

			openInterfaces.Clear();
		}

		public static void Shutdown()
		{
			Dispatch.ShutdownServer();

			ShutdownInterfaces();
			SteamGameServer.Shutdown();
		}

		/// <summary>
		/// Run the callbacks. This is also called in Async callbacks.
		/// </summary>
		public static void RunCallbacks()
		{
			if ( Dispatch.ServerPipe != 0 )
			{
				Dispatch.Frame( Dispatch.ServerPipe );
			}
		}

		/// <summary>
		/// Sets whether this should be marked as a dedicated server.
		/// If not, it is assumed to be a listen server.
		/// </summary>
		public static bool DedicatedServer
		{
			get => _dedicatedServer;
			set { if ( _dedicatedServer == value ) return; Internal?.SetDedicatedServer( value ); _dedicatedServer = value; }
		}
		private static bool _dedicatedServer;

		/// <summary>
		/// Gets or sets the current MaxPlayers. 
		/// This doesn't enforce any kind of limit, it just updates the master server.
		/// </summary>
		public static int MaxPlayers
		{
			get => _maxplayers;
			set { if ( _maxplayers == value ) return; Internal?.SetMaxPlayerCount( value ); _maxplayers = value; }
		}
		private static int _maxplayers = 0;

		/// <summary>
		/// Gets or sets the current BotCount. 
		/// This doesn't enforce any kind of limit, it just updates the master server.
		/// </summary>
		public static int BotCount
		{
			get => _botcount;
			set { if ( _botcount == value ) return; Internal?.SetBotPlayerCount( value ); _botcount = value; }
		}
		private static int _botcount = 0;

		/// <summary>
		/// Gets or sets the current Map Name. 
		/// </summary>
		public static string MapName
		{
			get => _mapname;
			set { if ( _mapname == value ) return; Internal?.SetMapName( value ); _mapname = value; }
		}
		private static string _mapname = "";

		/// <summary>
		/// Gets or sets the current ModDir
		/// </summary>
		public static string ModDir
		{
			get => _modDir; 
			internal set { if ( _modDir == value ) return; Internal?.SetModDir( value ); _modDir = value; }
		}
		private static string _modDir = "";

		/// <summary>
		/// Gets the current product
		/// </summary>
		public static string Product
		{
			get => _product;
			internal set { if ( _product == value ) return; Internal?.SetProduct( value ); _product = value; }
		}
		private static string _product = "";

		/// <summary>
		/// Gets or sets the current Product
		/// </summary>
		public static string GameDescription
		{
			get => _gameDescription;
			internal set { if ( _gameDescription == value ) return; Internal?.SetGameDescription( value ); _gameDescription = value; }
		}
		private static string _gameDescription = "";

		/// <summary>
		/// Gets or sets the current ServerName
		/// </summary>
		public static string ServerName
		{
			get => _serverName;
			set { if ( _serverName == value ) return; Internal?.SetServerName( value ); _serverName = value; }
		}
		private static string _serverName = "";

		/// <summary>
		/// Set whether the server should report itself as passworded
		/// </summary>
		public static bool Passworded
		{
			get => _passworded;
			set { if ( _passworded == value ) return; Internal?.SetPasswordProtected( value ); _passworded = value; }
		}
		private static bool _passworded;

		/// <summary>
		/// Gets or sets the current GameTags. This is a comma seperated list of tags for this server.
		/// When querying the server list you can filter by these tags.
		/// </summary>
		public static string GameTags
		{
			get => _gametags;
			set
			{
				if ( _gametags == value ) return;
				Internal?.SetGameTags( value );
				_gametags = value;
			}
		}
		private static string _gametags = "";

		public static SteamId SteamId => Internal?.GetSteamID() ?? default;

		/// <summary>
		/// Log onto Steam anonymously.
		/// </summary>
		public static void LogOnAnonymous()
		{
			Internal?.LogOnAnonymous();
			ForceHeartbeat();
		}

		/// <summary>
		/// Log onto Steam anonymously.
		/// </summary>
		public static void LogOff()
		{
			Internal?.LogOff();
		}

		/// <summary>
		/// Returns true if the server is connected and registered with the Steam master server
		/// You should have called LogOnAnonymous etc on startup.
		/// </summary>
		public static bool LoggedOn => Internal != null && Internal.BLoggedOn();

		/// <summary>
		/// To the best of its ability this tries to get the server's
		/// current public ip address. Be aware that this is likely to return
		/// null for the first few seconds after initialization.
		/// </summary>
		public static System.Net.IPAddress? PublicIp => Internal?.GetPublicIP();

		/// <summary>
		/// Enable or disable heartbeats, which are sent regularly to the master server.
		/// Enabled by default.
		/// </summary>
		public static bool AutomaticHeartbeats
		{
			set { Internal?.EnableHeartbeats( value ); }
		}

		/// <summary>
		/// Set heartbeat interval, if automatic heartbeats are enabled.
		/// You can leave this at the default.
		/// </summary>
		public static int AutomaticHeartbeatRate
		{
			set { Internal?.SetHeartbeatInterval( value ); }
		}

		/// <summary>
		/// Force send a heartbeat to the master server instead of waiting
		/// for the next automatic update (if you've left them enabled)
		/// </summary>
		public static void ForceHeartbeat()
		{
			Internal?.ForceHeartbeat();
		}

		/// <summary>
		/// Update this connected player's information. You should really call this
		/// any time a player's name or score changes. This keeps the information shown
		/// to server queries up to date.
		/// </summary>
		public static void UpdatePlayer( SteamId steamid, string name, int score )
		{
			Internal?.BUpdateUserData( steamid, name, (uint)score );
		}

		static Dictionary<string, string> KeyValue = new Dictionary<string, string>();

		/// <summary>
		/// Sets a Key Value. These can be anything you like, and are accessible
		/// when querying servers from the server list.
		/// 
		/// Information describing gamemodes are common here.
		/// </summary>
		public static void SetKey( string Key, string Value )
		{
			if (Internal is null) { return; }
			
			if ( KeyValue.ContainsKey( Key ) )
			{
				if ( KeyValue[Key] == Value )
					return;

				KeyValue[Key] = Value;
			}
			else
			{
				KeyValue.Add( Key, Value );
			}

			Internal.SetKeyValue( Key, Value );
		}

		/// <summary>
		/// Remove all key values
		/// </summary>
		public static void ClearKeys()
		{
			KeyValue.Clear();
			Internal?.ClearAllKeyValues();
		}

        /// <summary>
        /// Start authorizing a ticket. This user isn't authorized yet. Wait for a call to OnValidateAuthTicketResponse.
        /// </summary>
        public static unsafe BeginAuthResult BeginAuthSession( byte[] data, SteamId steamid )
		{
			if (Internal is null) { return BeginAuthResult.ServerNotConnectedToSteam; }
			fixed ( byte* p = data )
			{
				var result = Internal.BeginAuthSession( (IntPtr)p, data.Length, steamid );

                return result;
			}
		}

		/// <summary>
		/// Forget this guy. They're no longer in the game.
		/// </summary>
		public static void EndSession( SteamId steamid )
		{
			Internal?.EndAuthSession( steamid );
		}

		/// <summary>
		/// If true, Steam wants to send a packet. You should respond by sending
		/// this packet in an unconnected way to the returned Address and Port.
		/// </summary>
		/// <param name="packet">Packet to send. The Data passed is pooled - so use it immediately.</param>
		/// <returns>True if we want to send a packet</returns>
		public static unsafe bool GetOutgoingPacket( out OutgoingPacket packet )
		{
			if (Internal is null)
			{
				packet = default;
				return false;
			}
			
			var buffer = Helpers.TakeBuffer( 1024 * 32 );
			packet = new OutgoingPacket();

			fixed ( byte* ptr = buffer )
			{
				uint addr = 0;
				ushort port = 0;

				var size = Internal.GetNextOutgoingPacket( (IntPtr)ptr, buffer.Length, ref addr, ref port );
				if ( size == 0 )
					return false;

				packet.Size = size;
				packet.Data = buffer;
				packet.Address = addr;
				packet.Port = port;
				return true;
			}
		}

		/// <summary>
		/// We have received a server query on our game port. Pass it to Steam to handle.
		/// </summary>
		public static unsafe void HandleIncomingPacket( byte[] data, int size, uint address, ushort port )
		{
			fixed ( byte* ptr = data )
			{
				HandleIncomingPacket( (IntPtr)ptr, size, address, port );
			}
		}
		
		/// <summary>
		/// We have received a server query on our game port. Pass it to Steam to handle.
		/// </summary>
		public static unsafe void HandleIncomingPacket( IntPtr ptr, int size, uint address, ushort port )
		{
			Internal?.HandleIncomingPacket( ptr, size, address, port );
		}		
		
		/// <summary>
		/// Does the user own this app (which could be DLC)
		/// </summary>
		public static UserHasLicenseForAppResult UserHasLicenseForApp( SteamId steamid, AppId appid )
		{
			return Internal?.UserHasLicenseForApp( steamid, appid ) ?? UserHasLicenseForAppResult.NoAuth;
		}
	}
}