// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;

namespace osu.Game.Rulesets.Osu.Tests.Editor
{
    public partial class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene
    {
        private Slider slider;
        private PathControlPointVisualiser<Slider> visualiser;

        [SetUp]
        public void Setup() => Schedule(() =>
        {
            slider = new Slider();
            slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
        });

        [Test]
        public void TestPerfectCurveTooManyPoints()
        {
            createVisualiser(true);

            addControlPointStep(new Vector2(200), PathType.BEZIER);
            addControlPointStep(new Vector2(300));
            addControlPointStep(new Vector2(500, 300));
            addControlPointStep(new Vector2(700, 200));
            addControlPointStep(new Vector2(500, 100));

            // Must be both hovering and selecting the control point for the context menu to work.
            moveMouseToControlPoint(1);
            AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
            addContextMenuItemStep("Perfect curve");

            assertControlPointPathType(0, PathType.BEZIER);
            assertControlPointPathType(1, PathType.PERFECT_CURVE);
            assertControlPointPathType(3, PathType.BEZIER);
        }

        [Test]
        public void TestPerfectCurveLastThreePoints()
        {
            createVisualiser(true);

            addControlPointStep(new Vector2(200), PathType.BEZIER);
            addControlPointStep(new Vector2(300));
            addControlPointStep(new Vector2(500, 300));
            addControlPointStep(new Vector2(700, 200));
            addControlPointStep(new Vector2(500, 100));

            moveMouseToControlPoint(2);
            AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true);
            addContextMenuItemStep("Perfect curve");

            assertControlPointPathType(0, PathType.BEZIER);
            assertControlPointPathType(2, PathType.PERFECT_CURVE);
            assertControlPointPathType(4, null);
        }

        [Test]
        public void TestPerfectCurveLastTwoPoints()
        {
            createVisualiser(true);

            addControlPointStep(new Vector2(200), PathType.BEZIER);
            addControlPointStep(new Vector2(300));
            addControlPointStep(new Vector2(500, 300));
            addControlPointStep(new Vector2(700, 200));
            addControlPointStep(new Vector2(500, 100));

            moveMouseToControlPoint(3);
            AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
            addContextMenuItemStep("Perfect curve");

            assertControlPointPathType(0, PathType.BEZIER);
            AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null);
        }

        [Test]
        public void TestPerfectCurveTooManyPointsLinear()
        {
            createVisualiser(true);

            addControlPointStep(new Vector2(200), PathType.LINEAR);
            addControlPointStep(new Vector2(300));
            addControlPointStep(new Vector2(500, 300));
            addControlPointStep(new Vector2(700, 200));
            addControlPointStep(new Vector2(500, 100));

            // Must be both hovering and selecting the control point for the context menu to work.
            moveMouseToControlPoint(1);
            AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
            addContextMenuItemStep("Perfect curve");

            assertControlPointPathType(0, PathType.LINEAR);
            assertControlPointPathType(1, PathType.PERFECT_CURVE);
            assertControlPointPathType(3, PathType.LINEAR);
        }

        [Test]
        public void TestPerfectCurveChangeToBezier()
        {
            createVisualiser(true);

            addControlPointStep(new Vector2(200), PathType.BEZIER);
            addControlPointStep(new Vector2(300), PathType.PERFECT_CURVE);
            addControlPointStep(new Vector2(500, 300));
            addControlPointStep(new Vector2(700, 200), PathType.BEZIER);
            addControlPointStep(new Vector2(500, 100));

            moveMouseToControlPoint(3);
            AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
            addContextMenuItemStep("Inherit");

            assertControlPointPathType(0, PathType.BEZIER);
            assertControlPointPathType(1, PathType.BEZIER);
            assertControlPointPathType(3, null);
        }

        [Test]
        public void TestCatmullAvailableIffSelectionContainsCatmull()
        {
            createVisualiser(true);

            addControlPointStep(new Vector2(200), PathType.CATMULL);
            addControlPointStep(new Vector2(300));
            addControlPointStep(new Vector2(500, 300));
            addControlPointStep(new Vector2(700, 200));
            addControlPointStep(new Vector2(500, 100));

            moveMouseToControlPoint(2);
            AddStep("select first and third control point", () =>
            {
                visualiser.Pieces[0].IsSelected.Value = true;
                visualiser.Pieces[2].IsSelected.Value = true;
            });
            addContextMenuItemStep("Catmull");

            assertControlPointPathType(0, PathType.CATMULL);
            assertControlPointPathType(2, PathType.CATMULL);
            assertControlPointPathType(4, null);
        }

        [Test]
        public void TestStackingUpdatesPointsPosition()
        {
            createVisualiser(true);

            Vector2[] points =
            [
                new Vector2(200),
                new Vector2(300),
                new Vector2(500, 300),
                new Vector2(700, 200),
                new Vector2(500, 100)
            ];

            foreach (var point in points) addControlPointStep(point);

            AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1);

            for (int i = 0; i < points.Length; i++)
                addAssertPointPositionChanged(points, i);
        }

        [Test]
        public void TestChangingControlPointTypeViaTab()
        {
            createVisualiser(true);

            addControlPointStep(new Vector2(200), PathType.LINEAR);
            addControlPointStep(new Vector2(300));
            addControlPointStep(new Vector2(500, 300));
            addControlPointStep(new Vector2(700, 200));
            addControlPointStep(new Vector2(500, 100));

            AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true);
            AddStep("press tab", () => InputManager.Key(Key.Tab));
            assertControlPointPathType(0, PathType.BEZIER);

            AddStep("press shift-tab", () =>
            {
                InputManager.PressKey(Key.LShift);
                InputManager.Key(Key.Tab);
                InputManager.ReleaseKey(Key.LShift);
            });
            assertControlPointPathType(0, PathType.LINEAR);

            AddStep("press shift-tab", () =>
            {
                InputManager.PressKey(Key.LShift);
                InputManager.Key(Key.Tab);
                InputManager.ReleaseKey(Key.LShift);
            });
            assertControlPointPathType(0, PathType.BSpline(4));

            AddStep("press shift-tab", () =>
            {
                InputManager.PressKey(Key.LShift);
                InputManager.Key(Key.Tab);
                InputManager.ReleaseKey(Key.LShift);
            });
            assertControlPointPathType(0, PathType.PERFECT_CURVE);
            assertControlPointPathType(2, PathType.BSpline(4));

            AddStep("select third last control point", () =>
            {
                visualiser.Pieces[0].IsSelected.Value = false;
                visualiser.Pieces[2].IsSelected.Value = true;
            });

            AddStep("press shift-tab", () =>
            {
                InputManager.PressKey(Key.LShift);
                InputManager.Key(Key.Tab);
                InputManager.ReleaseKey(Key.LShift);
            });
            assertControlPointPathType(2, PathType.PERFECT_CURVE);

            AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
            assertControlPointPathType(0, PathType.BEZIER);
            assertControlPointPathType(2, null);

            AddStep("select first and third control points", () =>
            {
                visualiser.Pieces[0].IsSelected.Value = true;
                visualiser.Pieces[2].IsSelected.Value = true;
            });
            AddStep("press alt-1", () =>
            {
                InputManager.PressKey(Key.AltLeft);
                InputManager.Key(Key.Number1);
                InputManager.ReleaseKey(Key.AltLeft);
            });
            assertControlPointPathType(0, PathType.LINEAR);
            assertControlPointPathType(2, PathType.LINEAR);
        }

        private void addAssertPointPositionChanged(Vector2[] points, int index)
        {
            AddAssert($"Point at {points.ElementAt(index)} changed",
                () => visualiser.Pieces[index].Position,
                () => !Is.EqualTo(points.ElementAt(index))
            );
        }

        private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
        {
            Anchor = Anchor.Centre,
            Origin = Anchor.Centre
        });

        private void addControlPointStep(Vector2 position) => addControlPointStep(position, null);

        private void addControlPointStep(Vector2 position, PathType? type)
        {
            AddStep($"add {type?.Type} control point at {position}", () =>
            {
                slider.Path.ControlPoints.Add(new PathControlPoint(position, type));
            });
        }

        private void moveMouseToControlPoint(int index)
        {
            AddStep($"move mouse to control point {index}", () =>
            {
                Vector2 position = slider.Path.ControlPoints[index].Position;
                InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
            });
        }

        private void assertControlPointPathType(int controlPointIndex, PathType? type)
        {
            AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type == type);
        }

        private void addContextMenuItemStep(string contextMenuText)
        {
            AddStep($"click context menu item \"{contextMenuText}\"", () =>
            {
                MenuItem item = visualiser.ContextMenuItems!.FirstOrDefault(menuItem => menuItem.Text.Value == "Curve type")?.Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);

                item?.Action.Value?.Invoke();
            });
        }
    }
}
