// 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.

#nullable disable

using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osu.Game.Tests.Gameplay;
using osuTK;

namespace osu.Game.Rulesets.Osu.Tests
{
    [TestFixture]
    public partial class TestSceneGameplayCursor : OsuSkinnableTestScene
    {
        [Cached]
        private GameplayState gameplayState;

        private OsuCursorContainer lastContainer;

        [Resolved]
        private OsuConfigManager config { get; set; }

        private Drawable background;

        private readonly Bindable<bool> ripples = new Bindable<bool>();

        public TestSceneGameplayCursor()
        {
            var ruleset = new OsuRuleset();
            gameplayState = TestGameplayState.Create(ruleset);

            AddStep("change background colour", () =>
            {
                background?.Expire();

                Add(background = new Box
                {
                    RelativeSizeAxes = Axes.Both,
                    Depth = float.MaxValue,
                    Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
                });
            });

            AddToggleStep("ripples", v => ripples.Value = v);

            AddSliderStep("circle size", 0f, 10f, 0f, val =>
            {
                config.SetValue(OsuSetting.AutoCursorSize, true);
                gameplayState.Beatmap.Difficulty.CircleSize = val;
                Scheduler.AddOnce(loadContent);
            });

            AddStep("test cursor container", () => loadContent(false));
        }

        [BackgroundDependencyLoader]
        private void load()
        {
            var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
            rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
        }

        [TestCase(1, 1)]
        [TestCase(5, 1)]
        [TestCase(10, 1)]
        [TestCase(1, 1.5f)]
        [TestCase(5, 1.5f)]
        [TestCase(10, 1.5f)]
        public void TestSizing(int circleSize, float userScale)
        {
            AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
            AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
            AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));

            AddStep("load content", loadContent);

            AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);

            AddStep("set user scale to 1", () => config.SetValue(OsuSetting.GameplayCursorSize, 1f));
            AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize));

            AddStep("turn off autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, false));
            AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1);

            AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
            AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
        }

        [Test]
        public void TestTopLeftOrigin()
        {
            AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
        }

        private void loadContent() => loadContent(false);

        private void loadContent(bool automated, Func<SkinProvidingContainer> skinProvider = null)
        {
            SetContents(_ =>
            {
                var inputManager = automated ? (InputManager)new MovingCursorInputManager() : new OsuInputManager(new OsuRuleset().RulesetInfo);
                var skinContainer = skinProvider?.Invoke() ?? new SkinProvidingContainer(null);

                lastContainer = automated ? new ClickingCursorContainer() : new OsuCursorContainer();

                return inputManager.WithChild(skinContainer.WithChild(lastContainer));
            });
        }

        private class TopLeftCursorSkin : ISkin
        {
            public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => null;
            public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
            public ISample GetSample(ISampleInfo sampleInfo) => null;

            public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
            {
                switch (lookup)
                {
                    case OsuSkinConfiguration osuLookup:
                        if (osuLookup == OsuSkinConfiguration.CursorCentre)
                            return SkinUtils.As<TValue>(new BindableBool());

                        break;
                }

                return null;
            }
        }

        private partial class ClickingCursorContainer : OsuCursorContainer
        {
            private bool pressed;

            public bool Pressed
            {
                set
                {
                    if (value == pressed)
                        return;

                    pressed = value;
                    if (value)
                        OnPressed(new KeyBindingPressEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
                    else
                        OnReleased(new KeyBindingReleaseEvent<OsuAction>(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
                }
            }

            protected override void Update()
            {
                base.Update();
                Pressed = ((int)(Time.Current / 1000)) % 2 == 0;
            }
        }

        private partial class MovingCursorInputManager : ManualInputManager
        {
            public MovingCursorInputManager()
            {
                UseParentInput = false;
                ShowVisualCursorGuide = false;
            }

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

                const double spin_duration = 5000;
                double currentTime = Time.Current;

                double angle = (currentTime % spin_duration) / spin_duration * 2 * Math.PI;
                Vector2 rPos = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));

                MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
            }
        }
    }
}
