// 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.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;

namespace osu.Game.Rulesets.Osu.Tests
{
    public class SpinFramesGenerator
    {
        /// <summary>
        /// A small amount to spin beyond a given angle to mitigate floating-point precision errors.
        /// </summary>
        public const float SPIN_ERROR = MathF.PI / 8;

        /// <summary>
        /// The offset from the centre of the spinner at which to spin.
        /// </summary>
        private const float centre_spin_offset = 50;

        private readonly double startTime;
        private readonly float startAngle;
        private readonly List<(float deltaAngle, double duration)> sequences = new List<(float deltaAngle, double duration)>();

        /// <summary>
        /// Creates a new <see cref="SpinFramesGenerator"/> that can be used to generate spinner spin frames.
        /// </summary>
        /// <param name="startTime">The time at which to start spinning.</param>
        /// <param name="startAngle">The angle, in radians, at which to start spinning from. Defaults to the positive-y-axis.</param>
        public SpinFramesGenerator(double startTime, float startAngle = -MathF.PI / 2f)
        {
            this.startTime = startTime;
            this.startAngle = startAngle;
        }

        /// <summary>
        /// Performs a single spin.
        /// </summary>
        /// <param name="delta">The amount of degrees to spin.</param>
        /// <param name="duration">The time to spend to perform the spin.</param>
        /// <returns>This <see cref="SpinFramesGenerator"/>.</returns>
        public SpinFramesGenerator Spin(float delta, double duration)
        {
            sequences.Add((delta / 360 * 2 * MathF.PI, duration));
            return this;
        }

        /// <summary>
        /// Constructs the replay frames.
        /// </summary>
        /// <returns>The replay frames.</returns>
        public List<ReplayFrame> Build()
        {
            List<ReplayFrame> frames = new List<ReplayFrame>();

            double lastTime = startTime;
            float lastAngle = startAngle;
            int lastDirection = 0;

            for (int i = 0; i < sequences.Count; i++)
            {
                var seq = sequences[i];

                int seqDirection = Math.Sign(seq.deltaAngle);
                float seqError = SPIN_ERROR * seqDirection;

                if (seqDirection == lastDirection)
                {
                    // Spinning in the same direction, but the error was already added in the last rotation.
                    seqError = 0;
                }
                else if (lastDirection != 0)
                {
                    // Spinning in a different direction, we need to account for the error of the start angle, so double it.
                    seqError *= 2;
                }

                double seqStartTime = lastTime;
                double seqEndTime = lastTime + seq.duration;
                float seqStartAngle = lastAngle;
                float seqEndAngle = seqStartAngle + seq.deltaAngle + seqError;

                // Intermediate spin frames.
                for (; lastTime < seqEndTime; lastTime += 10)
                    frames.Add(new OsuReplayFrame(lastTime, calcOffsetAt((lastTime - seqStartTime) / (seqEndTime - seqStartTime), seqStartAngle, seqEndAngle), OsuAction.LeftButton));

                // Final frame at the end of the current spin.
                frames.Add(new OsuReplayFrame(seqEndTime, calcOffsetAt(1, seqStartAngle, seqEndAngle), OsuAction.LeftButton));

                lastTime = seqEndTime;
                lastAngle = seqEndAngle;
                lastDirection = seqDirection;
            }

            // Key release frame.
            if (frames.Count > 0)
                frames.Add(new OsuReplayFrame(frames[^1].Time, ((OsuReplayFrame)frames[^1]).Position));

            return frames;
        }

        private static Vector2 calcOffsetAt(double p, float startAngle, float endAngle)
        {
            float angle = startAngle + (endAngle - startAngle) * (float)p;
            return new Vector2(256, 192) + centre_spin_offset * new Vector2(MathF.Cos(angle), MathF.Sin(angle));
        }
    }
}
