// 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 osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Overlays.OSD;

namespace osu.Game.Overlays
{
    /// <summary>
    /// An on-screen display which automatically tracks and displays toast notifications for <seealso cref="TrackedSettings"/>.
    /// Can also display custom content via <see cref="Display(Toast)"/>
    /// </summary>
    public class OnScreenDisplay : Container
    {
        private readonly Container box;

        private const float height = 110;
        private const float height_contracted = height * 0.9f;

        public OnScreenDisplay()
        {
            RelativeSizeAxes = Axes.Both;

            Children = new Drawable[]
            {
                box = new Container
                {
                    Origin = Anchor.Centre,
                    RelativePositionAxes = Axes.Both,
                    Position = new Vector2(0.5f, 0.75f),
                    Masking = true,
                    AutoSizeAxes = Axes.X,
                    Height = height_contracted,
                    Alpha = 0,
                    CornerRadius = 20,
                },
            };
        }

        [BackgroundDependencyLoader]
        private void load(FrameworkConfigManager frameworkConfig, OsuConfigManager osuConfig)
        {
            BeginTracking(this, frameworkConfig);
            BeginTracking(this, osuConfig);
        }

        private readonly Dictionary<(object, IConfigManager), TrackedSettings> trackedConfigManagers = new Dictionary<(object, IConfigManager), TrackedSettings>();

        /// <summary>
        /// Registers a <see cref="ConfigManager{T}"/> to have its settings tracked by this <see cref="OnScreenDisplay"/>.
        /// </summary>
        /// <param name="source">The object that is registering the <see cref="ConfigManager{T}"/> to be tracked.</param>
        /// <param name="configManager">The <see cref="ConfigManager{T}"/> to be tracked.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="configManager"/> is null.</exception>
        /// <exception cref="InvalidOperationException">If <paramref name="configManager"/> is already being tracked from the same <paramref name="source"/>.</exception>
        public void BeginTracking(object source, ITrackableConfigManager configManager)
        {
            if (configManager == null) throw new ArgumentNullException(nameof(configManager));

            if (trackedConfigManagers.ContainsKey((source, configManager)))
                throw new InvalidOperationException($"{nameof(configManager)} is already registered.");

            var trackedSettings = configManager.CreateTrackedSettings();
            if (trackedSettings == null)
                return;

            configManager.LoadInto(trackedSettings);
            trackedSettings.SettingChanged += displayTrackedSettingChange;

            trackedConfigManagers.Add((source, configManager), trackedSettings);
        }

        /// <summary>
        /// Unregisters a <see cref="ConfigManager{T}"/> from having its settings tracked by this <see cref="OnScreenDisplay"/>.
        /// </summary>
        /// <param name="source">The object that registered the <see cref="ConfigManager{T}"/> to be tracked.</param>
        /// <param name="configManager">The <see cref="ConfigManager{T}"/> that is being tracked.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="configManager"/> is null.</exception>
        /// <exception cref="InvalidOperationException">If <paramref name="configManager"/> is not being tracked from the same <paramref name="source"/>.</exception>
        public void StopTracking(object source, ITrackableConfigManager configManager)
        {
            if (configManager == null) throw new ArgumentNullException(nameof(configManager));

            if (!trackedConfigManagers.TryGetValue((source, configManager), out var existing))
                return;

            existing.Unload();
            existing.SettingChanged -= displayTrackedSettingChange;

            trackedConfigManagers.Remove((source, configManager));
        }

        /// <summary>
        /// Displays the provided <see cref="Toast"/> temporarily.
        /// </summary>
        /// <param name="toast"></param>
        public void Display(Toast toast)
        {
            box.Child = toast;
            DisplayTemporarily(box);
        }

        private void displayTrackedSettingChange(SettingDescription description) => Schedule(() => Display(new TrackedSettingToast(description)));

        private TransformSequence<Drawable> fadeIn;
        private ScheduledDelegate fadeOut;

        protected virtual void DisplayTemporarily(Drawable toDisplay)
        {
            // avoid starting a new fade-in if one is already active.
            if (fadeIn == null)
            {
                fadeIn = toDisplay.Animate(
                    b => b.FadeIn(500, Easing.OutQuint),
                    b => b.ResizeHeightTo(height, 500, Easing.OutQuint)
                );

                fadeIn.Finally(_ => fadeIn = null);
            }

            fadeOut?.Cancel();
            fadeOut = Scheduler.AddDelayed(() =>
            {
                toDisplay.Animate(
                    b => b.FadeOutFromOne(1500, Easing.InQuint),
                    b => b.ResizeHeightTo(height_contracted, 1500, Easing.InQuint));
            }, 500);
        }
    }
}