using NUnit.Framework;
using Streamingle.Gaze.Editor;
using UnityEngine;

namespace Streamingle.Gaze.Tests
{
    public sealed class GazeCalibrationPreviewSessionTests
    {
        [Test]
        public void Session_UsesRuntimeBlendSemantics_BlocksDriverAndRestoresWeights()
        {
            var root = new GameObject("CalibrationPreviewSessionTest");
            var mesh = CreateMesh();
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var session = new GazeCalibrationPreviewSession();

            try
            {
                var renderer = root.AddComponent<SkinnedMeshRenderer>();
                renderer.sharedMesh = mesh;
                renderer.SetBlendShapeWeight(0, 25f);
                renderer.SetBlendShapeWeight(1, 30f);
                renderer.SetBlendShapeWeight(2, 45f);
                renderer.SetBlendShapeWeight(3, 55f);

                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft"),
                    new GazeBlendShapeChannel(
                        "eyeCorrectiveAdd",
                        GazeChannelUsage.Corrective,
                        GazeCorrectiveBlendMode.AdditiveFromCenter),
                    new GazeBlendShapeChannel(
                        "eyeCorrectiveOverride",
                        GazeChannelUsage.Corrective,
                        GazeCorrectiveBlendMode.Override)
                });
                for (var index = 0; index < 9; index++)
                {
                    profile.SetSample(
                        (GazeCalibrationPoint)index,
                        new[]
                        {
                            index * 10f,
                            10f + index * 2f,
                            100f - index * 5f
                        });
                }

                var driver = root.AddComponent<BlendshapeGazeDriver>();
                driver.TargetRenderer = renderer;
                driver.Profile = profile;
                var gazeOrigin = new GameObject("GazeOrigin").transform;
                gazeOrigin.SetParent(root.transform, false);
                driver.GazeOrigin = gazeOrigin;

                Assert.That(session.Start(driver, out var error), Is.True, error);
                Assert.That(session.GetOriginalWeightsCopy(), Is.EqualTo(new[] { 25f, 30f, 45f }));

                session.PreviewSample(GazeCalibrationPoint.UpRight);
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(80f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(38f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(60f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(3), Is.EqualTo(55f).Within(0.001f));

                session.PreviewWeights(new[] { 67f, 23f, 72f }, GazeCalibrationPoint.CenterRight);
                Assert.That(session.IsFreePreview, Is.False);
                Assert.That(session.PreviewCoordinates, Is.EqualTo(Vector2.right));
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(67f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(35f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(72f).Within(0.001f));

                // The four surrounding samples are deliberately distinct. At
                // (0.5, -0.5), indices 1/2/4/5 contribute equally.
                session.PreviewNormalized(new Vector2(0.5f, -0.5f));
                Assert.That(session.IsFreePreview, Is.True);
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(30f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(28f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(85f).Within(0.001f));

                driver.SetTimelineState(1234, new GazeTimelineState
                {
                    TargetPosition = new Vector3(1f, 0f, 1f),
                    HasTargetPosition = true,
                    Influence = 1f,
                    EnableCorrectives = true
                });
                driver.EvaluateNow(true);
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(30f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(28f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(85f).Within(0.001f));

                session.Stop();
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(25f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(30f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(45f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(3), Is.EqualTo(55f).Within(0.001f));

                // Releasing the session also releases the driver override. A yaw
                // beyond the profile limit resolves to CenterRight (sample 5).
                driver.EvaluateNow(true);
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(50f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(32f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(75f).Within(0.001f));
            }
            finally
            {
                session.Dispose();
                Object.DestroyImmediate(root);
                Object.DestroyImmediate(mesh);
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void Session_DoesNotRestoreIntoAnIndexWhoseResolvedNameChanged()
        {
            var root = new GameObject("CalibrationPreviewResolvedNameTest");
            var mesh = CreateMesh();
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var session = new GazeCalibrationPreviewSession();

            try
            {
                var renderer = root.AddComponent<SkinnedMeshRenderer>();
                renderer.sharedMesh = mesh;
                renderer.SetBlendShapeWeight(0, 25f);
                profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookUpLeft") });
                for (var index = 0; index < 9; index++)
                    profile.SetSample((GazeCalibrationPoint)index, new[] { 80f });

                var driver = root.AddComponent<BlendshapeGazeDriver>();
                driver.TargetRenderer = renderer;
                driver.Profile = profile;

                Assert.That(session.Start(driver, out var error), Is.True, error);
                session.PreviewSample(GazeCalibrationPoint.Center);
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(80f).Within(0.001f));

                mesh.ClearBlendShapes();
                AddBlendShape(mesh, "replacementShape");
                renderer.SetBlendShapeWeight(0, 66f);

                session.Stop();
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(66f).Within(0.001f));
            }
            finally
            {
                session.Dispose();
                Object.DestroyImmediate(root);
                Object.DestroyImmediate(mesh);
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void Session_AdditiveDraftUsesOriginalWeightUntilCenterIsCaptured()
        {
            var root = new GameObject("CalibrationPreviewDraftCorrectiveTest");
            var mesh = CreateMesh();
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var session = new GazeCalibrationPreviewSession();

            try
            {
                var renderer = root.AddComponent<SkinnedMeshRenderer>();
                renderer.sharedMesh = mesh;
                renderer.SetBlendShapeWeight(1, 30f);
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel(
                        "eyeCorrectiveAdd",
                        GazeChannelUsage.Corrective,
                        GazeCorrectiveBlendMode.AdditiveFromCenter)
                });

                var driver = root.AddComponent<BlendshapeGazeDriver>();
                driver.TargetRenderer = renderer;
                driver.Profile = profile;

                Assert.That(session.Start(driver, out var error), Is.True, error);
                session.PreviewWeights(new[] { 42f }, GazeCalibrationPoint.UpRight);
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(42f).Within(0.001f));

                session.Stop();
                Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(30f).Within(0.001f));
            }
            finally
            {
                session.Dispose();
                Object.DestroyImmediate(root);
                Object.DestroyImmediate(mesh);
                Object.DestroyImmediate(profile);
            }
        }

        private static Mesh CreateMesh()
        {
            var mesh = new Mesh { name = "CalibrationPreviewSessionMesh" };
            mesh.vertices = new[]
            {
                new Vector3(-0.1f, 0f, 0f),
                new Vector3(0.1f, 0f, 0f),
                new Vector3(0f, 0.1f, 0f)
            };
            mesh.triangles = new[] { 0, 1, 2 };
            AddBlendShape(mesh, "Face.eyeLookUpLeft");
            AddBlendShape(mesh, "Face.eyeCorrectiveAdd");
            AddBlendShape(mesh, "Face.eyeCorrectiveOverride");
            AddBlendShape(mesh, "unrelatedFaceShape");
            return mesh;
        }

        private static void AddBlendShape(Mesh mesh, string name)
        {
            mesh.AddBlendShapeFrame(
                name,
                100f,
                new[] { Vector3.up * 0.01f, Vector3.up * 0.01f, Vector3.up * 0.01f },
                new Vector3[3],
                new Vector3[3]);
        }
    }
}
