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

namespace Steamworks
{
	/// <summary>
	/// Functions for accessing and manipulating Steam user information.
	/// This is also where the APIs for Steam Voice are exposed.
	/// </summary>
	public class SteamUser : SteamClientClass<SteamUser>
	{
		internal static ISteamUser? Internal => Interface as ISteamUser;

		internal override bool InitializeInterface( bool server )
		{
			SetInterface( server, new ISteamUser( server ) );
			if ( Interface is null || Interface.Self == IntPtr.Zero ) return false;

			InstallEvents();

			richPresence = new Dictionary<string, string>();
			SampleRate = OptimalSampleRate;

			return true;
		}

		static Dictionary<string, string>? richPresence;

		internal static void InstallEvents()
		{
			Dispatch.Install<SteamServersConnected_t>( x => OnSteamServersConnected?.Invoke() );
			Dispatch.Install<SteamServerConnectFailure_t>( x => OnSteamServerConnectFailure?.Invoke() );
			Dispatch.Install<SteamServersDisconnected_t>( x => OnSteamServersDisconnected?.Invoke() );
			Dispatch.Install<ClientGameServerDeny_t>( x => OnClientGameServerDeny?.Invoke() );
			Dispatch.Install<LicensesUpdated_t>( x => OnLicensesUpdated?.Invoke() );
			Dispatch.Install<ValidateAuthTicketResponse_t>( x => OnValidateAuthTicketResponse?.Invoke( x.SteamID, x.OwnerSteamID, x.AuthSessionResponse ) );
			Dispatch.Install<MicroTxnAuthorizationResponse_t>( x => OnMicroTxnAuthorizationResponse?.Invoke( x.AppID, x.OrderID, x.Authorized != 0 ) );
			Dispatch.Install<GameWebCallback_t>( x => OnGameWebCallback?.Invoke( x.URLUTF8() ) );
			Dispatch.Install<GetAuthSessionTicketResponse_t>( x => OnGetAuthSessionTicketResponse?.Invoke( x ) );
			Dispatch.Install<GetTicketForWebApiResponse_t>( x => OnGetTicketForWebApiResponse?.Invoke( x ) );
			Dispatch.Install<DurationControl_t>( x => OnDurationControl?.Invoke( new DurationControl { _inner = x } ) );
		}

		/// <summary>
		/// Invoked when a connections to the Steam back-end has been established.
		/// This means the Steam client now has a working connection to the Steam servers. 
		/// Usually this will have occurred before the game has launched, and should only be seen if the 
		/// user has dropped connection due to a networking issue or a Steam server update.
		/// </summary>
		public static event Action? OnSteamServersConnected;

		/// <summary>
		/// Invoked when a connection attempt has failed.
		///	This will occur periodically if the Steam client is not connected, 
		///	and has failed when retrying to establish a connection.
		/// </summary>
		public static event Action? OnSteamServerConnectFailure;

		/// <summary>
		/// Invoked when the client has lost connection to the Steam servers.
		/// Real-time services will be disabled until a matching OnSteamServersConnected has been posted.
		/// </summary>
		public static event Action? OnSteamServersDisconnected;

		/// <summary>
		/// Sent by the Steam server to the client telling it to disconnect from the specified game server, 
		/// which it may be in the process of or already connected to.
		/// The game client should immediately disconnect upon receiving this message.
		/// This can usually occur if the user doesn't have rights to play on the game server.
		/// </summary>
		public static event Action? OnClientGameServerDeny;

		/// <summary>
		/// Invoked whenever the users licenses (owned packages) changes.
		/// </summary>
		public static event Action? OnLicensesUpdated;

		/// <summary>
		/// Invoked when an auth ticket has been validated. 
		/// The first parameter is the <see cref="SteamId"/> of this user
		/// The second is the <see cref="SteamId"/> that owns the game, which will be different from the first 
		/// if the game is being borrowed via Steam Family Sharing.
		/// </summary>
		public static event Action<SteamId, SteamId, AuthResponse>? OnValidateAuthTicketResponse;

		/// <summary>
		/// Used internally for <see cref="GetAuthSessionTicketAsync(NetIdentity, double)"/>.
		/// </summary>
		internal static event Action<GetAuthSessionTicketResponse_t>? OnGetAuthSessionTicketResponse;
		
		/// <summary>
		/// Used internally for <see cref="GetAuthTicketForWebApi(string)"/>.
		/// </summary>
		internal static event Action<GetTicketForWebApiResponse_t>? OnGetAuthTicketForWebApiResponse;

		/// <summary>
		/// Used internally for <see cref="GetAuthTicketForWebApiAsync(string, double)"/>.
		/// </summary>
		internal static event Action<GetTicketForWebApiResponse_t>? OnGetTicketForWebApiResponse;

		/// <summary>
		/// Invoked when a user has responded to a microtransaction authorization request.
		/// ( appid, orderid, user authorized )
		/// </summary>
		public static event Action<AppId, ulong, bool>? OnMicroTxnAuthorizationResponse;

		/// <summary>
		/// Sent to your game in response to a steam://gamewebcallback/(appid)/command/stuff command from a user clicking a 
		/// link in the Steam overlay browser.
		/// You can use this to add support for external site signups where you want to pop back into the browser after some web page 
		/// signup sequence, and optionally get back some detail about that.
		/// </summary>
		public static event Action<string>? OnGameWebCallback;

		/// <summary>
		/// Sent for games with enabled anti indulgence / duration control, for enabled users.
		/// Lets the game know whether persistent rewards or XP should be granted at normal rate, 
		/// half rate, or zero rate.
		/// </summary>
		public static event Action<DurationControl>? OnDurationControl;

		static bool _recordingVoice;

		/// <summary>
		/// Starts/Stops voice recording.
		/// Once started, use GetAvailableVoice and GetVoice to get the data, and then call StopVoiceRecording 
		/// when the user has released their push-to-talk hotkey or the game session has completed.
		/// </summary>
		public static bool VoiceRecord
		{
			get => _recordingVoice;
			set
			{
				_recordingVoice = value;
				if ( value ) Internal?.StartVoiceRecording();
				else Internal?.StopVoiceRecording();
			}
		}


		/// <summary>
		/// Returns true if we have voice data waiting to be read.
		/// </summary>
		public static bool HasVoiceData
		{
			get
			{
				uint szCompressed = 0, deprecated = 0;

				if ( Internal is null || Internal.GetAvailableVoice( ref szCompressed, ref deprecated, 0 ) != VoiceResult.OK )
					return false;

				return szCompressed > 0;
			}
		}

		static byte[] readBuffer = new byte[1024*128];

		/// <summary>
		/// Reads the voice data and returns the number of bytes written.
		/// The compressed data can be transmitted by your application and decoded back into raw audio data using 
		/// DecompressVoice on the other side. The compressed data provided is in an arbitrary format and is not meant to be played directly.
		/// This should be called once per frame, and at worst no more than four times a second to keep the microphone input delay as low as 
		/// possible. Calling this any less may result in gaps in the returned stream.
		/// </summary>
		public static unsafe int ReadVoiceData( System.IO.Stream stream )
		{
			if ( !HasVoiceData )
				return 0;

			uint szWritten = 0;
			uint deprecated = 0;

			fixed ( byte* b = readBuffer )
			{
				if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK )
					return 0;
			}

			if ( szWritten == 0 )
				return 0;

			stream.Write( readBuffer, 0, (int) szWritten );

			return (int) szWritten;
		}

		/// <summary>
		/// Reads the voice data and returns the bytes. You should obviously ideally be using
		/// ReadVoiceData because it won't be creating a new byte array every call. But this 
		/// makes it easier to get it working, so let the babies have their bottle.
		/// </summary>
		public static unsafe byte[]? ReadVoiceDataBytes()
		{
			if ( !HasVoiceData )
				return null;

			uint szWritten = 0;
			uint deprecated = 0;

			fixed ( byte* b = readBuffer )
			{
				if ( Internal is null || Internal.GetVoice( true, (IntPtr)b, (uint)readBuffer.Length, ref szWritten, false, IntPtr.Zero, 0, ref deprecated, 0 ) != VoiceResult.OK )
					return null;
			}

			if ( szWritten == 0 )
				return null;

			var arry = new byte[szWritten];
			Array.Copy( readBuffer, 0, arry, 0, szWritten );
			return arry;
		}

		static uint sampleRate = 48000;

		public static uint SampleRate
		{
			get => sampleRate;
			
			set
			{
				if ( SampleRate < 11025 ) throw new System.Exception( "Sample Rate must be between 11025 and 48000" );
				if ( SampleRate > 48000 ) throw new System.Exception( "Sample Rate must be between 11025 and 48000" );

				sampleRate = value;
			}
		}

		public static uint OptimalSampleRate => Internal?.GetVoiceOptimalSampleRate() ?? 0;


		/// <summary>
		/// Decodes the compressed voice data returned by GetVoice.
		/// The output data is raw single-channel 16-bit PCM audio.The decoder supports any sample rate from 11025 to 48000.
		/// </summary>
		public static unsafe int DecompressVoice( System.IO.Stream input, int length, System.IO.Stream output )
		{
			var from = Helpers.TakeBuffer( length );
			var to = Helpers.TakeBuffer( 1024 * 64 );

			//
			// Copy from input stream to a pinnable buffer
			//
			using ( var s = new System.IO.MemoryStream( from ) )
			{
				input.CopyTo( s );
			}

			uint szWritten = 0;

			fixed ( byte* frm = from )
			fixed ( byte* dst = to )
			{
				if ( Internal is null || Internal.DecompressVoice( (IntPtr) frm, (uint) length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK )
					return 0;
			}

			if ( szWritten == 0 )
				return 0;

			//
			// Copy to output buffer
			//
			output.Write( to, 0, (int)szWritten );
			return (int)szWritten;
		}

		/// <summary>
		/// Lazy version
		/// </summary>
		public static unsafe int DecompressVoice( byte[] from, System.IO.Stream output )
		{
			var to = Helpers.TakeBuffer( 1024 * 64 );

			uint szWritten = 0;

			fixed ( byte* frm = from )
			fixed ( byte* dst = to )
			{
				if ( Internal is null || Internal.DecompressVoice( (IntPtr)frm, (uint)from.Length, (IntPtr)dst, (uint)to.Length, ref szWritten, SampleRate ) != VoiceResult.OK )
					return 0;
			}

			if ( szWritten == 0 )
				return 0;

			//
			// Copy to output buffer
			//
			output.Write( to, 0, (int)szWritten );
			return (int)szWritten;
		}

		/// <summary>
		/// Advanced and potentially fastest version - incase you know what you're doing
		/// </summary>
		public static unsafe int DecompressVoice( IntPtr from, int length, IntPtr to, int bufferSize )
		{
			if ( length <= 0 ) throw new ArgumentException( $"length should be > 0 " );
			if ( bufferSize <= 0 ) throw new ArgumentException( $"bufferSize should be > 0 " );

			uint szWritten = 0;

			if ( Internal is null || Internal.DecompressVoice( from, (uint) length, to, (uint)bufferSize, ref szWritten, SampleRate ) != VoiceResult.OK )
				return 0;
			
			return (int)szWritten;
		}

		/// <summary>
		/// Retrieve an authentication ticket to be sent to the entity who wishes to authenticate you.
		/// </summary>
		public static unsafe AuthTicket? GetAuthSessionTicket( NetIdentity identity )
		{
			var data = Helpers.TakeBuffer( 1024 );

			fixed ( byte* b = data )
			{
				uint ticketLength = 0;
				uint ticket = Internal?.GetAuthSessionTicket( (IntPtr)b, data.Length, ref ticketLength, ref identity ) ?? 0;

				if ( ticket == 0 )
					return null;

				return new AuthTicket()
				{
					Data = data.Take( (int)ticketLength ).ToArray(),
					Handle = ticket
				};
			}
		}

		/// <summary>
		/// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you.
		/// This waits for a positive response from the backend before returning the ticket. This means
		/// the ticket is definitely ready to go as soon as it returns. Will return <see langword="null"/> if the callback
		/// times out or returns negatively.
		/// </summary>
		public static async Task<AuthTicket?> GetAuthSessionTicketAsync( NetIdentity identity, double timeoutSeconds = 10.0f )
		{
			var result = Result.Pending;
			AuthTicket? ticket = null;
			var stopwatch = Stopwatch.StartNew();

			void f( GetAuthSessionTicketResponse_t t )
			{
				if ( t.AuthTicket != ticket?.Handle ) return;
				result = t.Result;
			}

			OnGetAuthSessionTicketResponse += f;

			try
			{
				ticket = GetAuthSessionTicket( identity );
				if ( ticket == null )
					return null;

				while ( result == Result.Pending )
				{
					await Task.Delay( 10 );

					if ( stopwatch.Elapsed.TotalSeconds > timeoutSeconds )
					{
						ticket.Cancel();
						return null;
					}
				}

				if ( result == Result.OK )
					return ticket;

				ticket.Cancel();
				return null;
			}
			finally
			{
				OnGetAuthSessionTicketResponse -= f;
			}
		}

		/// <summary>
		/// Retrieve an authentication ticket to be sent to the entity who wishes to authenticate you.
		/// </summary>
		private static unsafe AuthTicket? GetAuthTicketForWebApi( string identity )
		{
			if ( Internal is null ) { return null; }
			uint ticket = Internal.GetAuthTicketForWebApi( identity );

			if ( ticket == 0 )
				return null;

			return new AuthTicket()
			{
				Handle = ticket
			};
		}

		/// <summary>
		/// Retrieve a authentication ticket to be sent to the entity who wishes to authenticate you.
		/// This waits for a positive response from the backend before returning the ticket. This means
		/// the ticket is definitely ready to go as soon as it returns. Will return <see langword="null"/> if the callback
		/// times out or returns negatively.
		/// </summary>
		public static async Task<AuthTicket?> GetAuthTicketForWebApiAsync( string identity, double timeoutSeconds = 10.0f )
		{
			if ( Internal is null ) { return null; }
			var result = Result.Pending;
			AuthTicket? ticket = null;
			var stopwatch = Stopwatch.StartNew();

			void f( GetTicketForWebApiResponse_t t )
			{
				if ( ticket is null || t.AuthTicket != ticket.Handle ) return;
				result = t.Result;
				ticket.Data = t.GubTicket;
			}

			OnGetTicketForWebApiResponse += f;

			try
			{
				ticket = GetAuthTicketForWebApi( identity );
				if ( ticket == null )
					return null;

				while ( result == Result.Pending )
				{
					await Task.Delay( 10 );

					if ( stopwatch.Elapsed.TotalSeconds > timeoutSeconds )
					{
						ticket.Cancel();
						return null;
					}
				}

				if ( result == Result.OK )
					return ticket;

				ticket.Cancel();
				return null;
			}
			finally
			{
				OnGetTicketForWebApiResponse -= f;
			}
		}

		public static unsafe BeginAuthResult BeginAuthSession( byte[] ticketData, SteamId steamid )
		{
			fixed ( byte* ptr = ticketData )
			{
				return Internal?.BeginAuthSession( (IntPtr) ptr, ticketData.Length, steamid ) ?? BeginAuthResult.ServerNotConnectedToSteam;
			}
		}

		public static void EndAuthSession( SteamId steamid ) => Internal?.EndAuthSession( steamid );


		// UserHasLicenseForApp - SERVER VERSION ( DLC CHECKING )

		/// <summary>
		/// Checks if the current users looks like they are behind a NAT device.
		/// This is only valid if the user is connected to the Steam servers and may not catch all forms of NAT.
		/// </summary>
		public static bool IsBehindNAT => Internal != null && Internal.BIsBehindNAT();

		/// <summary>
		/// Gets the Steam level of the user, as shown on their Steam community profile.
		/// </summary>
		public static int SteamLevel => Internal?.GetPlayerSteamLevel() ?? 0;

		/// <summary>
		/// Requests a URL which authenticates an in-game browser for store check-out, and then redirects to the specified URL.
		/// As long as the in-game browser accepts and handles session cookies, Steam microtransaction checkout pages will automatically recognize the user instead of presenting a login page.
		/// NOTE: The URL has a very short lifetime to prevent history-snooping attacks, so you should only call this API when you are about to launch the browser, or else immediately navigate to the result URL using a hidden browser window.
		/// NOTE: The resulting authorization cookie has an expiration time of one day, so it would be a good idea to request and visit a new auth URL every 12 hours.
		/// </summary>
		public static async Task<string?> GetStoreAuthUrlAsync( string url )
		{
			if (Internal is null) { return null; }
			
			var response = await Internal.RequestStoreAuthURL( url );
			if ( !response.HasValue )
				return null;

			return response.Value.URLUTF8();
		}

		/// <summary>
		/// Checks whether the current user has verified their phone number.
		/// </summary>
		public static bool IsPhoneVerified => Internal != null && Internal.BIsPhoneVerified();

		/// <summary>
		/// Checks whether the current user has Steam Guard two factor authentication enabled on their account.
		/// </summary>
		public static bool IsTwoFactorEnabled => Internal != null && Internal.BIsTwoFactorEnabled();

		/// <summary>
		/// Checks whether the user's phone number is used to uniquely identify them.
		/// </summary>
		public static bool IsPhoneIdentifying => Internal != null && Internal.BIsPhoneIdentifying();

		/// <summary>
		/// Checks whether the current user's phone number is awaiting (re)verification.
		/// </summary>
		public static bool IsPhoneRequiringVerification => Internal != null && Internal.BIsPhoneRequiringVerification();

		/// <summary>
		/// Requests an application ticket encrypted with the secret "encrypted app ticket key".
		/// The encryption key can be obtained from the Encrypted App Ticket Key page on the App Admin for your app.
		/// There can only be one call pending, and this call is subject to a 60 second rate limit.
		/// If you get a null result from this it's probably because you're calling it too often.
		/// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/
		/// </summary>
		public static async Task<byte[]?> RequestEncryptedAppTicketAsync( byte[] dataToInclude )
		{
			if (Internal is null) { return null; }
			
			var dataPtr = Marshal.AllocHGlobal( dataToInclude.Length );
			Marshal.Copy( dataToInclude, 0, dataPtr, dataToInclude.Length );

			try
			{
				var result = await Internal.RequestEncryptedAppTicket( dataPtr, dataToInclude.Length );
				if ( !result.HasValue || result.Value.Result != Result.OK ) return null;

				var ticketData = Marshal.AllocHGlobal( 1024 );
				uint outSize = 0;
				byte[]? data = null;

				if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) )
				{
					data = new byte[outSize];
					Marshal.Copy( ticketData, data, 0, (int) outSize );
				}

				Marshal.FreeHGlobal( ticketData );

				return data;
			}
			finally
			{
				Marshal.FreeHGlobal( dataPtr );
			}
		}

		/// <summary>
		/// Requests an application ticket encrypted with the secret "encrypted app ticket key".
		/// The encryption key can be obtained from the Encrypted App Ticket Key page on the App Admin for your app.
		/// There can only be one call pending, and this call is subject to a 60 second rate limit.
		/// This can fail if you don't have an encrypted ticket set for your app here https://partner.steamgames.com/apps/sdkauth/
		/// </summary>
		public static async Task<byte[]?> RequestEncryptedAppTicketAsync()
		{
			if (Internal is null) { return null; }
			
			var result = await Internal.RequestEncryptedAppTicket( IntPtr.Zero, 0 );
			if ( !result.HasValue || result.Value.Result != Result.OK ) return null;

			var ticketData = Marshal.AllocHGlobal( 1024 );
			uint outSize = 0;
			byte[]? data = null;

			if ( Internal.GetEncryptedAppTicket( ticketData, 1024, ref outSize ) )
			{
				data = new byte[outSize];
				Marshal.Copy( ticketData, data, 0, (int)outSize );
			}

			Marshal.FreeHGlobal( ticketData );

			return data;

		}


		/// <summary>
		/// Get anti indulgence / duration control
		/// </summary>
		public static async Task<DurationControl> GetDurationControl()
		{
			if (Internal is null) { return default; }

			var response = await Internal.GetDurationControl();
			if ( !response.HasValue ) return default;

			return new DurationControl { _inner = response.Value };
		}
	}
}
