// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Timing;

namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
    public class MultiplayerSyncManager : Component, IMultiplayerSyncManager
    {
        /// <summary>
        /// The offset from the master clock to which slaves should be synchronised to.
        /// </summary>
        public const double SYNC_TARGET = 16;

        /// <summary>
        /// The offset from the master clock at which slaves begin resynchronising.
        /// </summary>
        public const double MAX_SYNC_OFFSET = 50;

        /// <summary>
        /// The maximum delay to start gameplay, if any (but not all) slaves are ready.
        /// </summary>
        public const double MAXIMUM_START_DELAY = 15000;

        /// <summary>
        /// The catchup rate.
        /// </summary>
        public const double CATCHUP_RATE = 2;

        /// <summary>
        /// The master clock which is used to control the timing of all slave clocks.
        /// </summary>
        public IAdjustableClock Master { get; }

        /// <summary>
        /// The slave clocks.
        /// </summary>
        private readonly List<IMultiplayerSlaveClock> slaves = new List<IMultiplayerSlaveClock>();

        private bool hasStarted;
        private double? firstStartAttemptTime;

        public MultiplayerSyncManager(IAdjustableClock master)
        {
            Master = master;
        }

        public void AddSlave(IMultiplayerSlaveClock clock) => slaves.Add(clock);

        public void RemoveSlave(IMultiplayerSlaveClock clock) => slaves.Remove(clock);

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

            if (!attemptStart())
            {
                // Ensure all slaves are stopped until the start succeeds.
                foreach (var slave in slaves)
                    slave.Stop();
                return;
            }

            updateCatchup();
            updateMasterClock();
        }

        /// <summary>
        /// Attempts to start playback. Awaits for all slaves to have available frames for up to <see cref="MAXIMUM_START_DELAY"/> milliseconds.
        /// </summary>
        /// <returns>Whether playback was started and syncing should occur.</returns>
        private bool attemptStart()
        {
            if (hasStarted)
                return true;

            if (slaves.Count == 0)
                return false;

            firstStartAttemptTime ??= Time.Current;

            int readyCount = slaves.Count(s => !s.WaitingOnFrames.Value);

            if (readyCount == slaves.Count)
            {
                Logger.Log("Gameplay started (all ready).");
                return hasStarted = true;
            }

            if (readyCount > 0 && (Time.Current - firstStartAttemptTime) > MAXIMUM_START_DELAY)
            {
                Logger.Log($"Gameplay started (maximum delay exceeded, {readyCount}/{slaves.Count} ready).");
                return hasStarted = true;
            }

            return false;
        }

        /// <summary>
        /// Updates the catchup states of all slave clocks.
        /// </summary>
        private void updateCatchup()
        {
            for (int i = 0; i < slaves.Count; i++)
            {
                var slave = slaves[i];
                double timeDelta = Master.CurrentTime - slave.CurrentTime;

                // Check that the slave isn't too far ahead.
                // This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the slave.
                if (timeDelta < -SYNC_TARGET)
                {
                    slave.Stop();
                    continue;
                }

                // Make sure the slave is running if it can.
                if (!slave.WaitingOnFrames.Value)
                    slave.Start();

                if (slave.IsCatchingUp)
                {
                    // Stop the slave from catching up if it's within the sync target.
                    if (timeDelta <= SYNC_TARGET)
                    {
                        slave.IsCatchingUp = false;
                        Logger.Log($"Slave {i} catchup finished (delta = {timeDelta})");
                    }
                }
                else
                {
                    // Make the slave start catching up if it's exceeded the maximum allowable sync offset.
                    if (timeDelta > MAX_SYNC_OFFSET)
                    {
                        slave.IsCatchingUp = true;
                        Logger.Log($"Slave {i} catchup started (too far behind, delta = {timeDelta})");
                    }
                }
            }
        }

        /// <summary>
        /// Updates the master clock's running state.
        /// </summary>
        private void updateMasterClock()
        {
            bool anyInSync = slaves.Any(s => !s.IsCatchingUp);

            if (Master.IsRunning != anyInSync)
            {
                if (anyInSync)
                    Master.Start();
                else
                    Master.Stop();
            }
        }
    }
}