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

namespace Steamworks
{
	/// <summary>
	/// Undocumented Parental Settings
	/// </summary>
	public class SteamFriends : SteamClientClass<SteamFriends>
	{
		internal static ISteamFriends? Internal => Interface as ISteamFriends;

		internal override void InitializeInterface( bool server )
		{
			SetInterface( server, new ISteamFriends( server ) );

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

			InstallEvents();
		}

		static Dictionary<string, string>? richPresence;

		internal void InstallEvents()
		{
			Dispatch.Install<PersonaStateChange_t>( x => OnPersonaStateChange?.Invoke( new Friend( x.SteamID ) ) );
			Dispatch.Install<GameRichPresenceJoinRequested_t>( x => OnGameRichPresenceJoinRequested?.Invoke( new Friend( x.SteamIDFriend), x.ConnectUTF8() ) );
			Dispatch.Install<GameConnectedFriendChatMsg_t>( OnFriendChatMessage );
			Dispatch.Install<GameOverlayActivated_t>( x => OnGameOverlayActivated?.Invoke( x.Active != 0 ) );
			Dispatch.Install<GameServerChangeRequested_t>( x => OnGameServerChangeRequested?.Invoke( x.ServerUTF8(), x.PasswordUTF8() ) );
			Dispatch.Install<GameLobbyJoinRequested_t>( x => OnGameLobbyJoinRequested?.Invoke( new Lobby( x.SteamIDLobby ), x.SteamIDFriend ) );
			Dispatch.Install<FriendRichPresenceUpdate_t>( x => OnFriendRichPresenceUpdate?.Invoke( new Friend( x.SteamIDFriend ) ) );
		}

		/// <summary>
		/// Called when chat message has been received from a friend. You'll need to turn on
		/// ListenForFriendsMessages to recieve this. (friend, msgtype, message)
		/// </summary>
		public static event Action<Friend, string, string>? OnChatMessage;

		/// <summary>
		/// called when a friends' status changes
		/// </summary>
		public static event Action<Friend>? OnPersonaStateChange;


		/// <summary>
		/// Called when the user tries to join a game from their friends list
		///	rich presence will have been set with the "connect" key which is set here
		/// </summary>
		public static event Action<Friend, string>? OnGameRichPresenceJoinRequested;

		/// <summary>
		/// Posted when game overlay activates or deactivates
		///	the game can use this to be pause or resume single player games
		/// </summary>
		public static event Action<bool>? OnGameOverlayActivated;

		/// <summary>
		/// Called when the user tries to join a different game server from their friends list
		///	game client should attempt to connect to specified server when this is received
		/// </summary>
		public static event Action<string, string>? OnGameServerChangeRequested;

		/// <summary>
		/// Called when the user tries to join a lobby from their friends list
		///	game client should attempt to connect to specified lobby when this is received
		/// </summary>
		public static event Action<Lobby, SteamId>? OnGameLobbyJoinRequested;

		/// <summary>
		/// Callback indicating updated data about friends rich presence information
		/// </summary>
		public static event Action<Friend>? OnFriendRichPresenceUpdate;

		static unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data )
		{
			if ( OnChatMessage == null ) return;

			var friend = new Friend( data.SteamIDUser );

			using var buffer = Helpers.TakeMemory();
			var type = ChatEntryType.ChatMsg;

			var len = Internal?.GetFriendMessage( data.SteamIDUser, data.MessageID, buffer, Helpers.MemoryBufferSize, ref type ) ?? 0;

			if ( len == 0 && type == ChatEntryType.Invalid )
				return;

			var typeName = type.ToString();
			var message = Helpers.MemoryToString( buffer );

			OnChatMessage( friend, typeName, message );
		}
		
		private static IEnumerable<Friend> GetFriendsWithFlag(FriendFlags flag)
		{
			if (Internal is null) { yield break; }
			int friendCount = Internal.GetFriendCount((int)flag);
			for ( int i=0; i<friendCount; i++ )
			{
				if (Internal is null) { yield break; }
				yield return new Friend( Internal.GetFriendByIndex( i, (int)flag ) );
			}
		}

        public static string? GetFriendPersonaName( SteamId steamId )
        {
            return Internal?.GetFriendPersonaName(steamId);
        }

		public static IEnumerable<Friend> GetFriends()
		{
			return GetFriendsWithFlag(FriendFlags.Immediate);
		}

		public static IEnumerable<Friend> GetBlocked()
		{
			return GetFriendsWithFlag(FriendFlags.Blocked);
		}

		public static IEnumerable<Friend> GetFriendsRequested()
		{
			return GetFriendsWithFlag( FriendFlags.FriendshipRequested );
		}

		public static IEnumerable<Friend> GetFriendsClanMembers()
		{
			return GetFriendsWithFlag( FriendFlags.ClanMember );
		}

		public static IEnumerable<Friend> GetFriendsOnGameServer()
		{
			return GetFriendsWithFlag( FriendFlags.OnGameServer );
		}

		public static IEnumerable<Friend> GetFriendsRequestingFriendship()
		{
			return GetFriendsWithFlag( FriendFlags.RequestingFriendship );
		}

		public static IEnumerable<Friend> GetPlayedWith()
		{
			if (Internal is null) { yield break; }
			int friendCount = Internal.GetCoplayFriendCount();
			for ( int i = 0; i < friendCount; i++ )
			{
				if (Internal is null) { yield break; }
				yield return new Friend( Internal.GetCoplayFriend( i ) );
			}
		}

		public static IEnumerable<Friend> GetFromSource( SteamId steamid )
		{
			if (Internal is null) { yield break; }
			int friendCount = Internal.GetFriendCountFromSource( steamid );
			for ( int i = 0; i < friendCount; i++ )
			{
				if (Internal is null) { yield break; }
		        yield return new Friend( Internal.GetFriendFromSourceByIndex( steamid, i ) );
		    }
		}

		public static IEnumerable<Clan> GetClans()
		{
			if (Internal is null) { yield break; }
			int friendCount = Internal.GetClanCount();
			for ( int i = 0; i < friendCount; i++ )
			{
				if (Internal is null) { yield break; }
				yield return new Clan( Internal.GetClanByIndex( i ) );
			}
		}

		/// <summary>
		/// The dialog to open. Valid options are: 
		/// "friends", 
		/// "community", 
		/// "players", 
		/// "settings", 
		/// "officialgamegroup", 
		/// "stats", 
		/// "achievements".
		/// </summary>
		public static void OpenOverlay( string type ) => Internal?.ActivateGameOverlay( type );

		/// <summary>
		/// "steamid" - Opens the overlay web browser to the specified user or groups profile.
		/// "chat" - Opens a chat window to the specified user, or joins the group chat.
		/// "jointrade" - Opens a window to a Steam Trading session that was started with the ISteamEconomy/StartTrade Web API.
		/// "stats" - Opens the overlay web browser to the specified user's stats.
		/// "achievements" - Opens the overlay web browser to the specified user's achievements.
		/// "friendadd" - Opens the overlay in minimal mode prompting the user to add the target user as a friend.
		/// "friendremove" - Opens the overlay in minimal mode prompting the user to remove the target friend.
		/// "friendrequestaccept" - Opens the overlay in minimal mode prompting the user to accept an incoming friend invite.
		/// "friendrequestignore" - Opens the overlay in minimal mode prompting the user to ignore an incoming friend invite.
		/// </summary>
		public static void OpenUserOverlay( SteamId id, string type ) => Internal?.ActivateGameOverlayToUser( type, id );

		/// <summary>
		/// Activates the Steam Overlay to the Steam store page for the provided app.
		/// </summary>
		public static void OpenStoreOverlay( AppId id ) => Internal?.ActivateGameOverlayToStore( id.Value, OverlayToStoreFlag.None );

		/// <summary>
		/// Activates Steam Overlay web browser directly to the specified URL.
		/// </summary>
		public static void OpenWebOverlay( string url, bool modal = false ) => Internal?.ActivateGameOverlayToWebPage( url, modal ? ActivateGameOverlayToWebPageMode.Modal : ActivateGameOverlayToWebPageMode.Default );

		/// <summary>
		/// Activates the Steam Overlay to open the invite dialog. Invitations sent from this dialog will be for the provided lobby.
		/// </summary>
		public static void OpenGameInviteOverlay( SteamId lobby ) => Internal?.ActivateGameOverlayInviteDialog( lobby );

		/// <summary>
		/// Mark a target user as 'played with'.
		/// NOTE: The current user must be in game with the other player for the association to work.
		/// </summary>
		public static void SetPlayedWith( SteamId steamid ) => Internal?.SetPlayedWith( steamid );

		/// <summary>
		/// Requests the persona name and optionally the avatar of a specified user.
		/// NOTE: It's a lot slower to download avatars and churns the local cache, so if you don't need avatars, don't request them.
		/// returns true if we're fetching the data, false if we already have it
		/// </summary>
		public static bool RequestUserInformation( SteamId steamid, bool nameonly = true ) => Internal != null && Internal.RequestUserInformation( steamid, nameonly );


		internal static async Task CacheUserInformationAsync( SteamId steamid, bool nameonly )
		{
			// Got it straight away, skip any waiting.
			if ( !RequestUserInformation( steamid, nameonly ) )
				return;

			await Task.Delay( 100 );

			while ( RequestUserInformation( steamid, nameonly ) )
			{
				await Task.Delay( 50 );
			}

			//
			// And extra wait here seems to solve avatars loading as [?]
			//
			await Task.Delay( 500 );
		}

		public static async Task<Data.Image?> GetSmallAvatarAsync( SteamId steamid )
		{
			if (Internal is null) { return null; }
			await CacheUserInformationAsync( steamid, false );
			return SteamUtils.GetImage( Internal.GetSmallFriendAvatar( steamid ) );
		}

		public static async Task<Data.Image?> GetMediumAvatarAsync( SteamId steamid )
		{
			if (Internal is null) { return null; }
			await CacheUserInformationAsync( steamid, false );
			return SteamUtils.GetImage( Internal.GetMediumFriendAvatar( steamid ) );
		}

		public static async Task<Data.Image?> GetLargeAvatarAsync( SteamId steamid )
		{
			if (Internal is null) { return null; }
			await CacheUserInformationAsync( steamid, false );

			var imageid = Internal.GetLargeFriendAvatar( steamid );

			// Wait for the image to download
			while ( imageid == -1 )
			{
				await Task.Delay( 50 );
				imageid = Internal.GetLargeFriendAvatar( steamid );
			}

			return SteamUtils.GetImage( imageid );
		}

		/// <summary>
		/// Find a rich presence value by key for current user. Will be null if not found.
		/// </summary>
		public static string? GetRichPresence( string key )
		{
			if (richPresence is null) { return null; }
			
			if ( richPresence.TryGetValue( key, out var val ) )
				return val;

			return null;
		}

		/// <summary>
		/// Sets a rich presence value by key for current user.
		/// </summary>
		public static bool SetRichPresence( string key, string value )
		{
			if (richPresence is null || Internal is null) { return false; }
			
			bool success = Internal.SetRichPresence( key, value );

			if ( success ) 
				richPresence[key] = value;

			return success;
		}

		/// <summary>
		/// Clears all of the current user's rich presence data.
		/// </summary>
		public static void ClearRichPresence()
		{
			richPresence?.Clear();
			Internal?.ClearRichPresence();
		}

		static bool _listenForFriendsMessages;

		/// <summary>
		/// Listens for Steam friends chat messages.
		/// You can then show these chats inline in the game. For example with a Blizzard style chat message system or the chat system in Dota 2.
		/// After enabling this you will receive callbacks when ever the user receives a chat message.
		/// </summary>
		public static bool ListenForFriendsMessages
		{
			get => _listenForFriendsMessages;
				
			set
			{
				_listenForFriendsMessages = value;
				Internal?.SetListenForFriendsMessages( value );
			}
		}

		public static async Task<bool> IsFollowing(SteamId steamID)
		{
			if (Internal is null) { return false; }
			var r = await Internal.IsFollowing(steamID);
			return r?.IsFollowing ?? false;
		}

		public static async Task<int> GetFollowerCount(SteamId steamID)
		{
			if (Internal is null) { return 0; }
			var r = await Internal.GetFollowerCount(steamID);
			return r?.Count ?? 0;
		}

        public static async Task<SteamId[]> GetFollowingList()
        {
            int resultCount = 0;
            var steamIds = new List<SteamId>();

            FriendsEnumerateFollowingList_t? result;

            do
            {
	            if (Internal is null) { break; }
                if ( (result = await Internal.EnumerateFollowingList((uint)resultCount)) != null)
                {
                    resultCount += result.Value.ResultsReturned;

                    Array.ForEach(result.Value.GSteamID, id => { if (id > 0) steamIds.Add(id); });
                }
            } while (result != null && resultCount < result.Value.TotalResultCount);

            return steamIds.ToArray();
        }
    }
}
