// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Visual;
using osuTK;

namespace osu.Game.Rulesets.Catch.Tests
{
    public class TestSceneDrawableHitObjects : OsuTestScene
    {
        private DrawableCatchRuleset drawableRuleset;
        private double playfieldTime => drawableRuleset.Playfield.Time.Current;

        [SetUp]
        public void Setup() => Schedule(() =>
        {
            var controlPointInfo = new ControlPointInfo();
            controlPointInfo.Add(0, new TimingControlPoint());

            IWorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap
            {
                HitObjects = new List<HitObject> { new Fruit() },
                BeatmapInfo = new BeatmapInfo
                {
                    Difficulty = new BeatmapDifficulty(),
                    Metadata = new BeatmapMetadata
                    {
                        Artist = @"Unknown",
                        Title = @"You're breathtaking",
                        Author = { Username = @"Everyone" },
                    },
                    Ruleset = new CatchRuleset().RulesetInfo
                },
                ControlPointInfo = controlPointInfo
            });

            Child = new Container
            {
                Anchor = Anchor.Centre,
                Origin = Anchor.Centre,
                RelativeSizeAxes = Axes.Both,
                Children = new[]
                {
                    drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap.GetPlayableBeatmap(new CatchRuleset().RulesetInfo))
                }
            };
        });

        [Test]
        public void TestFruits()
        {
            AddStep("hit fruits", () => spawnFruits(true));
            AddUntilStep("wait for completion", () => playfieldIsEmpty);
            AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);

            AddStep("miss fruits", () => spawnFruits());
            AddUntilStep("wait for completion", () => playfieldIsEmpty);
            AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail);
        }

        [Test]
        public void TestJuiceStream()
        {
            AddStep("hit juicestream", () => spawnJuiceStream(true));
            AddUntilStep("wait for completion", () => playfieldIsEmpty);
            AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);

            AddStep("miss juicestream", () => spawnJuiceStream());
            AddUntilStep("wait for completion", () => playfieldIsEmpty);
            AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail);
        }

        [Test]
        public void TestBananas()
        {
            AddStep("hit bananas", () => spawnBananas(true));
            AddUntilStep("wait for completion", () => playfieldIsEmpty);
            AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);

            AddStep("miss bananas", () => spawnBananas());
            AddUntilStep("wait for completion", () => playfieldIsEmpty);
            AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle);
        }

        private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive);

        private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).Catcher.CurrentState;

        private void spawnFruits(bool hit = false)
        {
            for (int i = 1; i <= 4; i++)
            {
                var fruit = new Fruit
                {
                    X = getXCoords(hit),
                    LastInCombo = i % 4 == 0,
                    StartTime = playfieldTime + 800 + (200 * i)
                };

                fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());

                addToPlayfield(new DrawableFruit(fruit));
            }
        }

        private void spawnJuiceStream(bool hit = false)
        {
            float xCoords = getXCoords(hit);

            var juice = new JuiceStream
            {
                X = xCoords,
                StartTime = playfieldTime + 1000,
                Path = new SliderPath(PathType.Linear, new[]
                {
                    Vector2.Zero,
                    new Vector2(0, 200)
                })
            };

            juice.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());

            if (juice.NestedHitObjects.Last() is CatchHitObject tail)
                tail.LastInCombo = true; // usually the (Catch)BeatmapProcessor would do this for us when necessary

            addToPlayfield(new DrawableJuiceStream(juice));
        }

        private void spawnBananas(bool hit = false)
        {
            for (int i = 1; i <= 4; i++)
            {
                var banana = new Banana
                {
                    X = getXCoords(hit),
                    LastInCombo = i % 4 == 0,
                    StartTime = playfieldTime + 800 + (200 * i)
                };

                banana.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());

                addToPlayfield(new DrawableBanana(banana));
            }
        }

        private float getXCoords(bool hit)
        {
            const float x_offset = 0.2f * CatchPlayfield.WIDTH;
            float xCoords = CatchPlayfield.CENTER_X;

            if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
                catchPlayfield.Catcher.X = xCoords - x_offset;

            if (hit)
                xCoords -= x_offset;
            else
                xCoords += x_offset;

            return xCoords;
        }

        private void addToPlayfield(DrawableCatchHitObject drawable)
        {
            foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
                mod.ApplyToDrawableHitObject(drawable);

            drawableRuleset.Playfield.Add(drawable);
        }
    }
}