using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;

namespace Streamingle.Gaze.Tests
{
    public sealed class GazeDirectionSolverTests
    {
        [Test]
        public void Calculate_ClampsAndNormalizesAsymmetricRanges()
        {
            var result = GazeDirectionSolver.Calculate(
                Quaternion.identity,
                GazeDirectionSolver.DirectionFromAngles(60f, 30f),
                -20f,
                40f,
                -10f,
                20f);

            Assert.That(result.RawYaw, Is.EqualTo(60f).Within(0.01f));
            Assert.That(result.RawPitch, Is.EqualTo(30f).Within(0.01f));
            Assert.That(result.ClampedYaw, Is.EqualTo(40f).Within(0.01f));
            Assert.That(result.ClampedPitch, Is.EqualTo(20f).Within(0.01f));
            Assert.That(result.NormalizedYaw, Is.EqualTo(1f).Within(0.001f));
            Assert.That(result.NormalizedPitch, Is.EqualTo(1f).Within(0.001f));
            Assert.That(result.WasClamped, Is.True);
        }

        [Test]
        public void Calculate_UsesCharacterLocalReference()
        {
            var reference = Quaternion.Euler(5f, 73f, 0f);
            var localDirection = GazeDirectionSolver.DirectionFromAngles(-10f, -5f);
            var result = GazeDirectionSolver.Calculate(
                reference,
                reference * localDirection,
                -20f,
                40f,
                -10f,
                20f);

            Assert.That(result.RawYaw, Is.EqualTo(-10f).Within(0.01f));
            Assert.That(result.RawPitch, Is.EqualTo(-5f).Within(0.01f));
            Assert.That(result.NormalizedYaw, Is.EqualTo(-0.5f).Within(0.001f));
            Assert.That(result.NormalizedPitch, Is.EqualTo(-0.5f).Within(0.001f));
            Assert.That(result.WasClamped, Is.False);
        }

        [Test]
        public void Profile_BilinearlyInterpolatesTheThreeByThreeGrid()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            try
            {
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft")
                });

                for (var index = 0; index < 9; index++)
                    profile.SetSample((GazeCalibrationPoint)index, new[] { (float)index });

                var output = new float[1];
                var direction = DirectionWithNormalizedCoordinates(-0.5f, -0.5f);

                Assert.That(profile.TryEvaluate(direction, false, output), Is.True);
                // Bottom-left/center cell: lerp of sample values 0,1,3,4.
                Assert.That(output[0], Is.EqualTo(2f).Within(0.001f));

                direction = DirectionWithNormalizedCoordinates(0.5f, 0.5f);
                Assert.That(profile.TryEvaluate(direction, false, output), Is.True);
                // Center/top-right cell: lerp of sample values 4,5,7,8.
                Assert.That(output[0], Is.EqualTo(6f).Within(0.001f));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void Profile_ExcludesCorrectiveChannelsUnlessRequested()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            try
            {
                profile.ReplaceChannels(new List<GazeBlendShapeChannel>
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft"),
                    new GazeBlendShapeChannel("eyeBlinkLeft", GazeChannelUsage.Corrective)
                });

                for (var index = 0; index < 9; index++)
                    profile.SetSample((GazeCalibrationPoint)index, new[] { 10f, 20f });

                var output = new float[2];
                Assert.That(profile.TryEvaluate(DirectionWithNormalizedCoordinates(0f, 0f), false, output), Is.True);
                Assert.That(output[0], Is.EqualTo(10f));
                Assert.That(float.IsNaN(output[1]), Is.True);

                Assert.That(profile.TryEvaluate(DirectionWithNormalizedCoordinates(0f, 0f), true, output), Is.True);
                Assert.That(output[1], Is.EqualTo(20f));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void Profile_DoesNotEvaluateAnIncompleteCalibration()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            try
            {
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft")
                });
                profile.SetSample(GazeCalibrationPoint.Center, new[] { 50f });

                Assert.That(
                    profile.TryEvaluate(
                        DirectionWithNormalizedCoordinates(0f, 0f),
                        false,
                        new float[1]),
                    Is.False);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(profile);
            }
        }

        private static GazeDirectionResult DirectionWithNormalizedCoordinates(float yaw, float pitch)
        {
            return new GazeDirectionResult(
                Vector3.forward,
                Vector3.forward,
                0f,
                0f,
                0f,
                0f,
                yaw,
                pitch);
        }
    }
}
