// 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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;

namespace osu.Game.Tests.NonVisual
{
    [TestFixture]
    public class FramedReplayInputHandlerTest
    {
        private Replay replay;
        private TestInputHandler handler;

        [SetUp]
        public void SetUp()
        {
            handler = new TestInputHandler(replay = new Replay
            {
                HasReceivedAllFrames = false
            });
        }

        [Test]
        public void TestNormalPlayback()
        {
            setReplayFrames();

            setTime(0, 0);
            confirmCurrentFrame(0);
            confirmNextFrame(1);

            // if we hit the first frame perfectly, time should progress to it.
            setTime(1000, 1000);
            confirmCurrentFrame(1);
            confirmNextFrame(2);

            // in between non-important frames should progress based on input.
            setTime(1200, 1200);
            confirmCurrentFrame(1);

            setTime(1400, 1400);
            confirmCurrentFrame(1);

            // progressing beyond the next frame should force time to that frame once.
            setTime(2200, 2000);
            confirmCurrentFrame(2);

            // second attempt should progress to input time
            setTime(2200, 2200);
            confirmCurrentFrame(2);

            // entering important section
            setTime(3000, 3000);
            confirmCurrentFrame(3);

            // cannot progress within
            setTime(3500, null);
            confirmCurrentFrame(3);

            setTime(4000, 4000);
            confirmCurrentFrame(4);

            // still cannot progress
            setTime(4500, null);
            confirmCurrentFrame(4);

            setTime(5200, 5000);
            confirmCurrentFrame(5);

            // important section AllowedImportantTimeSpan allowance
            setTime(5200, 5200);
            confirmCurrentFrame(5);

            setTime(7200, 7000);
            confirmCurrentFrame(6);

            setTime(7200, null);
            confirmCurrentFrame(6);

            // exited important section
            setTime(8200, 8000);
            confirmCurrentFrame(7);
            confirmNextFrame(null);

            setTime(8200, 8200);
            confirmCurrentFrame(7);
            confirmNextFrame(null);
        }

        [Test]
        public void TestIntroTime()
        {
            setReplayFrames();

            setTime(-1000, -1000);
            confirmCurrentFrame(null);
            confirmNextFrame(0);

            setTime(-500, -500);
            confirmCurrentFrame(null);
            confirmNextFrame(0);

            setTime(0, 0);
            confirmCurrentFrame(0);
            confirmNextFrame(1);
        }

        [Test]
        public void TestBasicRewind()
        {
            setReplayFrames();

            setTime(2800, 0);
            setTime(2800, 1000);
            setTime(2800, 2000);
            setTime(2800, 2800);
            confirmCurrentFrame(2);
            confirmNextFrame(3);

            // pivot without crossing a frame boundary
            setTime(2700, 2700);
            confirmCurrentFrame(2);
            confirmNextFrame(3);

            // cross current frame boundary
            setTime(1980, 2000);
            confirmCurrentFrame(2);
            confirmNextFrame(3);

            setTime(1200, 1200);
            confirmCurrentFrame(1);
            confirmNextFrame(2);

            // ensure each frame plays out until start
            setTime(-500, 1000);
            confirmCurrentFrame(1);
            confirmNextFrame(2);

            setTime(-500, 0);
            confirmCurrentFrame(0);
            confirmNextFrame(1);

            setTime(-500, -500);
            confirmCurrentFrame(null);
            confirmNextFrame(0);
        }

        [Test]
        public void TestRewindInsideImportantSection()
        {
            setReplayFrames();
            fastForwardToPoint(3000);

            setTime(4000, 4000);
            confirmCurrentFrame(4);
            confirmNextFrame(5);

            setTime(3500, null);
            confirmCurrentFrame(3);
            confirmNextFrame(4);

            setTime(3000, 3000);
            confirmCurrentFrame(3);
            confirmNextFrame(4);

            setTime(3500, null);
            confirmCurrentFrame(3);
            confirmNextFrame(4);

            setTime(4000, 4000);
            confirmCurrentFrame(4);
            confirmNextFrame(5);

            setTime(4500, null);
            confirmCurrentFrame(4);
            confirmNextFrame(5);

            setTime(4000, 4000);
            confirmCurrentFrame(4);
            confirmNextFrame(5);

            setTime(3500, null);
            confirmCurrentFrame(3);
            confirmNextFrame(4);

            setTime(3000, 3000);
            confirmCurrentFrame(3);
            confirmNextFrame(4);
        }

        [Test]
        public void TestRewindOutOfImportantSection()
        {
            setReplayFrames();
            fastForwardToPoint(3500);

            confirmCurrentFrame(3);
            confirmNextFrame(4);

            setTime(3200, null);
            confirmCurrentFrame(3);
            confirmNextFrame(4);

            setTime(3000, 3000);
            confirmCurrentFrame(3);
            confirmNextFrame(4);

            setTime(2800, 2800);
            confirmCurrentFrame(2);
            confirmNextFrame(3);
        }

        [Test]
        public void TestReplayStreaming()
        {
            // no frames are arrived yet
            setTime(0, null);
            setTime(1000, null);
            Assert.IsTrue(handler.WaitingForFrame, "Should be waiting for the first frame");

            replay.Frames.Add(new TestReplayFrame(0));
            replay.Frames.Add(new TestReplayFrame(1000));

            // should always play from beginning
            setTime(1000, 0);
            confirmCurrentFrame(0);
            Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet");
            setTime(1000, 1000);
            confirmCurrentFrame(1);
            confirmNextFrame(null);
            Assert.IsTrue(handler.WaitingForFrame, "Should be waiting");

            // cannot seek beyond the last frame
            setTime(1500, null);
            confirmCurrentFrame(1);

            setTime(-100, 0);
            confirmCurrentFrame(0);

            // can seek to the point before the first frame, however
            setTime(-100, -100);
            confirmCurrentFrame(null);
            confirmNextFrame(0);

            fastForwardToPoint(1000);
            setTime(3000, null);
            replay.Frames.Add(new TestReplayFrame(2000));
            confirmCurrentFrame(1);
            setTime(1000, 1000);
            setTime(3000, 2000);
        }

        [Test]
        public void TestMultipleFramesSameTime()
        {
            replay.Frames.Add(new TestReplayFrame(0));
            replay.Frames.Add(new TestReplayFrame(0));
            replay.Frames.Add(new TestReplayFrame(1000));
            replay.Frames.Add(new TestReplayFrame(1000));
            replay.Frames.Add(new TestReplayFrame(2000));

            // forward direction is prioritized when multiple frames have the same time.
            setTime(0, 0);
            setTime(0, 0);

            setTime(2000, 1000);
            setTime(2000, 1000);

            setTime(1000, 1000);
            setTime(1000, 1000);
            setTime(-100, 1000);
            setTime(-100, 0);
            setTime(-100, 0);
            setTime(-100, -100);
        }

        [Test]
        public void TestReplayFramesSortStability()
        {
            const double repeating_time = 5000;

            // add a collection of frames in shuffled order time-wise; each frame also stores its original index to check stability later.
            // data is hand-picked and breaks if the unstable List<T>.Sort() is used.
            // in theory this can still return a false-positive with another unstable algorithm if extremely unlucky,
            // but there is no conceivable fool-proof way to prevent that anyways.
            replay.Frames.AddRange(new[]
            {
                repeating_time,
                0,
                3000,
                repeating_time,
                repeating_time,
                6000,
                9000,
                repeating_time,
                repeating_time,
                1000,
                11000,
                21000,
                4000,
                repeating_time,
                repeating_time,
                8000,
                2000,
                7000,
                repeating_time,
                repeating_time,
                10000
            }.Select((time, index) => new TestReplayFrame(time, true, index)));

            replay.HasReceivedAllFrames = true;

            // create a new handler with the replay for the sort to be performed.
            handler = new TestInputHandler(replay);

            // ensure sort stability by checking that the frames with time == repeating_time are sorted in ascending frame index order themselves.
            var repeatingTimeFramesData = replay.Frames
                                                .Cast<TestReplayFrame>()
                                                .Where(f => f.Time == repeating_time)
                                                .Select(f => f.FrameIndex);

            Assert.That(repeatingTimeFramesData, Is.Ordered.Ascending);
        }

        private void setReplayFrames()
        {
            replay.Frames = new List<ReplayFrame>
            {
                new TestReplayFrame(0),
                new TestReplayFrame(1000),
                new TestReplayFrame(2000),
                new TestReplayFrame(3000, true),
                new TestReplayFrame(4000, true),
                new TestReplayFrame(5000, true),
                new TestReplayFrame(7000, true),
                new TestReplayFrame(8000),
            };
            replay.HasReceivedAllFrames = true;
        }

        private void fastForwardToPoint(double destination)
        {
            for (int i = 0; i < 1000; i++)
            {
                double? time = handler.SetFrameFromTime(destination);
                if (time == null || time == destination)
                    return;
            }

            throw new TimeoutException("Seek was never fulfilled");
        }

        private void setTime(double set, double? expect)
        {
            Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value");
        }

        private void confirmCurrentFrame(int? frame)
        {
            Assert.AreEqual(frame is int x ? replay.Frames[x].Time : null, handler.CurrentFrame?.Time, "Unexpected current frame");
        }

        private void confirmNextFrame(int? frame)
        {
            Assert.AreEqual(frame is int x ? replay.Frames[x].Time : null, handler.NextFrame?.Time, "Unexpected next frame");
        }

        private class TestReplayFrame : ReplayFrame
        {
            public readonly bool IsImportant;
            public readonly int FrameIndex;

            public TestReplayFrame(double time, bool isImportant = false, int frameIndex = 0)
                : base(time)
            {
                IsImportant = isImportant;
                FrameIndex = frameIndex;
            }

            public override bool IsEquivalentTo(ReplayFrame other)
                => other is TestReplayFrame testFrame && Time == testFrame.Time && IsImportant == testFrame.IsImportant && FrameIndex == testFrame.FrameIndex;
        }

        private class TestInputHandler : FramedReplayInputHandler<TestReplayFrame>
        {
            public TestInputHandler(Replay replay)
                : base(replay)
            {
                FrameAccuratePlayback = true;
            }

            protected override double AllowedImportantTimeSpan => 1000;

            protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant;
        }
    }
}
