// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Users;

namespace osu.Game.Online.Metadata
{
    public abstract partial class MetadataClient : Component, IMetadataClient, IMetadataServer
    {
        public abstract IBindable<bool> IsConnected { get; }

        /// <summary>
        /// A list of all watched multiplayer rooms (see <see cref="BeginWatchingMultiplayerRoom"/>).
        /// </summary>
        protected readonly HashSet<long> WatchedRooms = new HashSet<long>();

        [Resolved]
        private IAPIProvider api { get; set; } = null!;

        private readonly IBindableList<APIRelation> localFriends = new BindableList<APIRelation>();

        protected override void LoadComplete()
        {
            base.LoadComplete();

            localFriends.BindTo(api.LocalUserState.Friends);
            localFriends.BindCollectionChanged((_, _) => RefreshFriends().FireAndForget());
        }

        #region Beatmap metadata updates

        public abstract Task<BeatmapUpdates> GetChangesSince(int queueId);

        public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);

        public event Action<int[]>? ChangedBeatmapSetsArrived;

        protected Task ProcessChanges(int[] beatmapSetIDs)
        {
            ChangedBeatmapSetsArrived?.Invoke(beatmapSetIDs.Distinct().ToArray());
            return Task.CompletedTask;
        }

        #endregion

        #region User presence updates

        /// <summary>
        /// The <see cref="UserPresence"/> information about the current user.
        /// </summary>
        public abstract UserPresence LocalUserPresence { get; }

        /// <summary>
        /// Dictionary keyed by user ID containing all of the <see cref="UserPresence"/> information about currently online users received from the server.
        /// </summary>
        public abstract IBindableDictionary<int, UserPresence> UserPresences { get; }

        /// <summary>
        /// Dictionary keyed by user ID containing all of the <see cref="UserPresence"/> information about currently online friends received from the server.
        /// </summary>
        public abstract IBindableDictionary<int, UserPresence> FriendPresences { get; }

        /// <summary>
        /// Attempts to retrieve the presence of a user.
        /// </summary>
        /// <remarks>
        /// This will return data if the client is currently receiving presence data. See <see cref="BeginWatchingUserPresence"/>.
        /// </remarks>
        /// <param name="userId">The user ID.</param>
        /// <returns>The user presence, or null if not available or the user's offline.</returns>
        public UserPresence? GetPresence(int userId)
        {
            if (userId == api.LocalUser.Value.OnlineID)
                return LocalUserPresence;

            if (FriendPresences.TryGetValue(userId, out UserPresence presence))
                return presence;

            if (UserPresences.TryGetValue(userId, out presence))
                return presence;

            return null;
        }

        public abstract Task UpdateActivity(UserActivity? activity);

        public abstract Task UpdateStatus(UserStatus? status);

        private int userPresenceWatchCount;

        protected bool IsWatchingUserPresence
            => Interlocked.CompareExchange(ref userPresenceWatchCount, userPresenceWatchCount, userPresenceWatchCount) > 0;

        /// <summary>
        /// Signals to the server that we want to begin receiving status updates for all users.
        /// </summary>
        /// <returns>An <see cref="IDisposable"/> which will end the session when disposed.</returns>
        public IDisposable BeginWatchingUserPresence() => new UserPresenceWatchToken(this);

        Task IMetadataServer.BeginWatchingUserPresence()
        {
            if (Interlocked.Increment(ref userPresenceWatchCount) == 1)
                return BeginWatchingUserPresenceInternal();

            return Task.CompletedTask;
        }

        Task IMetadataServer.EndWatchingUserPresence()
        {
            if (Interlocked.Decrement(ref userPresenceWatchCount) == 0)
                return EndWatchingUserPresenceInternal();

            return Task.CompletedTask;
        }

        protected abstract Task BeginWatchingUserPresenceInternal();

        protected abstract Task EndWatchingUserPresenceInternal();

        public abstract Task UserPresenceUpdated(int userId, UserPresence? presence);

        public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence);

        private class UserPresenceWatchToken : IDisposable
        {
            private readonly IMetadataServer server;
            private bool isDisposed;

            public UserPresenceWatchToken(IMetadataServer server)
            {
                this.server = server;
                server.BeginWatchingUserPresence().FireAndForget();
            }

            public void Dispose()
            {
                if (isDisposed)
                    return;

                server.EndWatchingUserPresence().FireAndForget();
                isDisposed = true;
            }
        }

        #endregion

        #region Daily Challenge

        public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }

        public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);

        #endregion

        #region Multiplayer room watching

        public abstract Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id);

        public abstract Task EndWatchingMultiplayerRoom(long id);

        public abstract Task RefreshFriends();

        public event Action<MultiplayerRoomScoreSetEvent>? MultiplayerRoomScoreSet;

        Task IMetadataClient.MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent)
        {
            if (MultiplayerRoomScoreSet != null)
                Schedule(MultiplayerRoomScoreSet, roomScoreSetEvent);

            return Task.CompletedTask;
        }

        #endregion

        #region Disconnection handling

        /// <summary>
        /// Invoked just prior to disconnection.
        /// </summary>
        public event Action? Disconnecting;

        public abstract Task Reconnect();

        protected abstract Task DisconnectInternal();

        Task IStatefulUserHubClient.DisconnectRequested()
        {
            Schedule(() =>
            {
                Disconnecting?.Invoke();
                DisconnectInternal().FireAndForget();
            });
            return Task.CompletedTask;
        }

        Task IStatefulUserHubClient.ServerShuttingDown()
        {
            this.ReconnectWhenReady(IsConnected, () => WatchedRooms.Count == 0, Reconnect);
            return Task.CompletedTask;
        }

        #endregion
    }
}
