// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;

namespace Microsoft.MixedReality.Toolkit.Input
{
    /// <summary>
    /// A used-defined marker on the input animation timeline.
    /// </summary>
    [Serializable]
    public class InputAnimationMarker
    {
        /// <summary>
        /// Placement of the marker relative to the input animation start time.
        /// </summary>
        public float time = 0.0f;

        /// <summary>
        /// Custom name of the marker.
        /// </summary>
        public string name = "";
    }

    /// <summary>
    /// Contains a set of animation curves that describe motion of camera and hands.
    /// </summary>
    [Serializable]
    public class InputAnimation
    {
        /// <summary>
        /// Arbitrarily large weight for representing a boolean value in float curves.
        /// </summary>
        private const float BoolOutWeight = 1.0e6f;

        /// <summary>
        /// Maximum duration of all animations curves.
        /// </summary>
        [SerializeField]
        private float duration = 0.0f;
        /// <summary>
        /// Maximum duration of all animations curves.
        /// </summary>
        public float Duration => duration;

        private class PoseCurves
        {
            public AnimationCurve PositionX = new AnimationCurve();
            public AnimationCurve PositionY = new AnimationCurve();
            public AnimationCurve PositionZ = new AnimationCurve();
            public AnimationCurve RotationX = new AnimationCurve();
            public AnimationCurve RotationY = new AnimationCurve();
            public AnimationCurve RotationZ = new AnimationCurve();
            public AnimationCurve RotationW = new AnimationCurve();

            public void AddKey(float time, MixedRealityPose pose)
            {
                AddFloatKey(PositionX, time, pose.Position.x);
                AddFloatKey(PositionY, time, pose.Position.y);
                AddFloatKey(PositionZ, time, pose.Position.z);

                AddFloatKey(RotationX, time, pose.Rotation.x);
                AddFloatKey(RotationY, time, pose.Rotation.y);
                AddFloatKey(RotationZ, time, pose.Rotation.z);
                AddFloatKey(RotationW, time, pose.Rotation.w);
            }

            /// <summary>
            /// Optimizes the set of curves.
            /// </summary>
            /// <param name="positionThreshold">The maximum permitted error between the positions of the old and new curves, in units.</param>
            /// <param name="rotationThreshold">The maximum permitted error between the rotations of the old and new curves, in degrees.</param>
            /// <param name="partitionSize">The size of the partitions of the curves that will be optimized independently. Larger values will optimize the curves better, but may take longer.</param>
            public void Optimize(float positionThreshold, float rotationThreshold, int partitionSize)
            {
                OptimizePositionCurve(ref PositionX, ref PositionY, ref PositionZ, positionThreshold, partitionSize);
                OptimizeRotationCurve(ref RotationX, ref RotationY, ref RotationZ, ref RotationW, rotationThreshold, partitionSize);
            }

            public MixedRealityPose Evaluate(float time)
            {
                float px = PositionX.Evaluate(time);
                float py = PositionY.Evaluate(time);
                float pz = PositionZ.Evaluate(time);
                float rx = RotationX.Evaluate(time);
                float ry = RotationY.Evaluate(time);
                float rz = RotationZ.Evaluate(time);
                float rw = RotationW.Evaluate(time);

                var pose = new MixedRealityPose();

                pose.Position = new Vector3(px, py, pz);
                pose.Rotation = new Quaternion(rx, ry, rz, rw);
                pose.Rotation.Normalize();

                return pose;
            }
        }

        private class RayCurves
        {
            public AnimationCurve OriginX = new AnimationCurve();
            public AnimationCurve OriginY = new AnimationCurve();
            public AnimationCurve OriginZ = new AnimationCurve();
            public AnimationCurve DirectionX = new AnimationCurve();
            public AnimationCurve DirectionY = new AnimationCurve();
            public AnimationCurve DirectionZ = new AnimationCurve();

            public void AddKey(float time, Ray ray)
            {
                AddVectorKey(OriginX, OriginY, OriginZ, time, ray.origin);
                AddVectorKey(DirectionX, DirectionY, DirectionZ, time, ray.direction);
            }

            /// <summary>
            /// Optimizes the set of curves.
            /// </summary>
            /// <param name="originThreshold">The maximum permitted error between the origins of the old and new curves, in units.</param>
            /// <param name="directionThreshold">The maximum permitted error between the directions of the old and new curves, in degrees.</param>
            /// <param name="partitionSize">The size of the partitions of the curves that will be optimized independently. Larger values will optimize the curves better, but may take longer.</param>
            public void Optimize(float originThreshold, float directionThreshold, int partitionSize)
            {
                OptimizePositionCurve(ref OriginX, ref OriginY, ref OriginZ, originThreshold, partitionSize);
                OptimizeDirectionCurve(ref DirectionX, ref DirectionY, ref DirectionZ, directionThreshold, partitionSize);
            }

            public Ray Evaluate(float time)
            {
                float ox = OriginX.Evaluate(time);
                float oy = OriginY.Evaluate(time);
                float oz = OriginZ.Evaluate(time);
                float dx = DirectionX.Evaluate(time);
                float dy = DirectionY.Evaluate(time);
                float dz = DirectionZ.Evaluate(time);

                var ray = new Ray();

                ray.origin = new Vector3(ox, oy, oz);
                ray.direction = new Vector3(dx, dy, dz);
                ray.direction.Normalize();

                return ray;
            }
        }

        internal class CompareMarkers : IComparer<InputAnimationMarker>
        {
            public int Compare(InputAnimationMarker a, InputAnimationMarker b)
            {
                return a.time.CompareTo(b.time);
            }
        }

        [SerializeField]
        private AnimationCurve handTrackedCurveLeft;
        [SerializeField]
        private AnimationCurve handTrackedCurveRight;
        [SerializeField]
        private AnimationCurve handPinchCurveLeft;
        [SerializeField]
        private AnimationCurve handPinchCurveRight;
        [SerializeField]
        private Dictionary<TrackedHandJoint, PoseCurves> handJointCurvesLeft;
        [SerializeField]
        private Dictionary<TrackedHandJoint, PoseCurves> handJointCurvesRight;
        [SerializeField]
        private PoseCurves cameraCurves;
        [SerializeField]
        private RayCurves gazeCurves;

        /// <summary>
        /// Whether the animation has hand state and joint curves
        /// </summary>
        public bool HasHandData { get; private set; } = false;
        /// <summary>
        /// Whether the animation has camera pose curves
        /// </summary>
        public bool HasCameraPose { get; private set; } = false;
        /// <summary>
        /// Whether the animation has eye gaze curves
        /// </summary>
        public bool HasEyeGaze { get; private set; } = false;

        /// <summary>
        /// Number of markers in the animation.
        /// </summary>
        [SerializeField]
        private List<InputAnimationMarker> markers;
        /// <summary>
        /// Number of markers in the animation.
        /// </summary>
        public int markerCount => markers.Count;

        /// <summary>
        /// Default constructor
        /// </summary>
        public InputAnimation()
        {
            handTrackedCurveLeft = new AnimationCurve();
            handTrackedCurveRight = new AnimationCurve();
            handPinchCurveLeft = new AnimationCurve();
            handPinchCurveRight = new AnimationCurve();
            handJointCurvesLeft = new Dictionary<TrackedHandJoint, PoseCurves>();
            handJointCurvesRight = new Dictionary<TrackedHandJoint, PoseCurves>();
            cameraCurves = new PoseCurves();
            gazeCurves = new RayCurves();
            markers = new List<InputAnimationMarker>();
        }

        /// <summary>
        /// Add a keyframe for the tracking state of a hand.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        public void AddHandStateKey(float time, Handedness handedness, bool isTracked, bool isPinching)
        {
            if (handedness == Handedness.Left)
            {
                AddHandStateKey(time, isTracked, isPinching, handTrackedCurveLeft, handPinchCurveLeft);
            }
            else if (handedness == Handedness.Right)
            {
                AddHandStateKey(time, isTracked, isPinching, handTrackedCurveRight, handPinchCurveRight);
            }
        }

        /// <summary>
        /// Add a keyframe for one hand joint.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        public void AddHandJointKey(float time, Handedness handedness, TrackedHandJoint joint, MixedRealityPose jointPose, float positionThreshold, float rotationThreshold)
        {
            if (handedness == Handedness.Left)
            {
                AddHandJointKey(time, joint, jointPose, handJointCurvesLeft, positionThreshold, rotationThreshold);
            }
            else if (handedness == Handedness.Right)
            {
                AddHandJointKey(time, joint, jointPose, handJointCurvesRight, positionThreshold, rotationThreshold);
            }
        }

        /// <summary>
        /// Add a keyframe for the camera transform.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        public void AddCameraPoseKey(float time, MixedRealityPose cameraPose, float positionThreshold, float rotationThreshold)
        {
            AddPoseKeyFiltered(cameraCurves, time, cameraPose, positionThreshold, rotationThreshold);

            duration = Mathf.Max(duration, time);
        }

        /// <summary>
        /// Add a user-defined marker.
        /// </summary>
        public void AddMarker(InputAnimationMarker marker)
        {
            int index = FindMarkerInterval(marker.time) + 1;
            markers.Insert(index, marker);
        }

        /// <summary>
        /// Remove the user-defined marker at the given index.
        /// </summary>
        public void RemoveMarker(int index)
        {
            markers.RemoveAt(index);
        }

        /// <summary>
        /// Change the time of the marker at the given index.
        /// </summary>
        public void SetMarkerTime(int index, float time)
        {
            var marker = markers[index];
            markers.RemoveAt(index);

            int newIndex = FindMarkerInterval(time) + 1;
            marker.time = time;
            markers.Insert(newIndex, marker);
        }

        /// <summary>
        /// Remove all keyframes from all animation curves.
        /// </summary>
        public void Clear()
        {
            foreach (var curve in GetAllAnimationCurves())
            {
                curve.keys = new Keyframe[0];
            }
        }

        /// <summary>
        /// Remove all keyframes from all animation curves with time values before the given cutoff time.
        /// </summary>
        /// <remarks>
        /// <para>If keyframes exists before the cutoff time then one preceding keyframe will be retained,
        /// so that interpolation at the cutoff time yields the same result.</para>
        /// </remarks>
        [Obsolete("Cutoff is achieved in InputRecordingBuffer")]
        public void CutoffBeforeTime(float time)
        {
            foreach (var curve in GetAllAnimationCurves())
            {
                CutoffBeforeTime(curve, time);
            }
        }

        /// <summary>
        /// Serialize animation data into a stream.
        /// </summary>
        public void ToStream(Stream stream, float startTime)
        {
            var writer = new BinaryWriter(stream);

            InputAnimationSerializationUtils.WriteHeader(writer);
            writer.Write(HasCameraPose);
            writer.Write(HasHandData);
            writer.Write(HasEyeGaze);

            var defaultCurves = new PoseCurves();

            if (HasCameraPose)
            {
                PoseCurvesToStream(writer, cameraCurves, startTime);
            }

            if (HasHandData)
            {
                InputAnimationSerializationUtils.WriteBoolCurve(writer, handTrackedCurveLeft, startTime);
                InputAnimationSerializationUtils.WriteBoolCurve(writer, handTrackedCurveRight, startTime);
                InputAnimationSerializationUtils.WriteBoolCurve(writer, handPinchCurveLeft, startTime);
                InputAnimationSerializationUtils.WriteBoolCurve(writer, handPinchCurveRight, startTime);

                for (int i = 0; i < ArticulatedHandPose.JointCount; ++i)
                {
                    if (!handJointCurvesLeft.TryGetValue((TrackedHandJoint)i, out var curves))
                    {
                        curves = defaultCurves;
                    }
                    PoseCurvesToStream(writer, curves, startTime);
                }
                for (int i = 0; i < ArticulatedHandPose.JointCount; ++i)
                {
                    if (!handJointCurvesRight.TryGetValue((TrackedHandJoint)i, out var curves))
                    {
                        curves = defaultCurves;
                    }
                    PoseCurvesToStream(writer, curves, startTime);
                }
            }

            if (HasEyeGaze)
            {
                RayCurvesToStream(writer, gazeCurves, startTime);
            }

            InputAnimationSerializationUtils.WriteMarkerList(writer, markers, startTime);
        }

        /// <summary>
        /// Serialize animation data into a stream asynchronously.
        /// </summary>
        public async Task ToStreamAsync(Stream stream, float startTime, Action callback = null)
        {
            await Task.Run(() => ToStream(stream, startTime));

            callback?.Invoke();
        }

        /// <summary>
        /// Evaluate hand tracking state at the given time.
        /// </summary>
        public void EvaluateHandState(float time, Handedness handedness, out bool isTracked, out bool isPinching)
        {
            if (!HasHandData)
            {
                isTracked = false;
                isPinching = false;
            }

            if (handedness == Handedness.Left)
            {
                EvaluateHandState(time, handTrackedCurveLeft, handPinchCurveLeft, out isTracked, out isPinching);
            }
            else if (handedness == Handedness.Right)
            {
                EvaluateHandState(time, handTrackedCurveRight, handPinchCurveRight, out isTracked, out isPinching);
            }
            else
            {
                isTracked = false;
                isPinching = false;
            }
        }

        /// <summary>
        /// Find an index i in the sorted events list, such that events[i].time &lt;= time &lt; events[i+1].time.
        /// </summary>
        /// <returns>
        /// 0 &lt;= i &lt; eventCount if a full interval could be found.
        /// -1 if time is less than the first event time.
        /// eventCount-1 if time is greater than the last event time.
        /// </returns>
        /// <remarks>
        /// Uses binary search.
        /// </remarks>
        public int FindMarkerInterval(float time)
        {
            int lowIdx = -1;
            int highIdx = markers.Count;
            while (lowIdx < highIdx - 1)
            {
                int midIdx = (lowIdx + highIdx) >> 1;
                if (time >= markers[midIdx].time)
                {
                    lowIdx = midIdx;
                }
                else
                {
                    highIdx = midIdx;
                }
            }
            return lowIdx;
        }

        /// <summary>
        /// Evaluate the camera transform at the given time.
        /// </summary>
        public MixedRealityPose EvaluateCameraPose(float time)
        {
            if (!HasCameraPose)
            {
                return MixedRealityPose.ZeroIdentity;
            }

            return cameraCurves.Evaluate(time);
        }

        /// <summary>
        /// Evaluate joint pose at the given time.
        /// </summary>
        public MixedRealityPose EvaluateHandJoint(float time, Handedness handedness, TrackedHandJoint joint)
        {
            if (!HasHandData)
            {
                return MixedRealityPose.ZeroIdentity;
            }

            if (handedness == Handedness.Left)
            {
                return EvaluateHandJoint(time, joint, handJointCurvesLeft);
            }
            else if (handedness == Handedness.Right)
            {
                return EvaluateHandJoint(time, joint, handJointCurvesRight);
            }
            else
            {
                return MixedRealityPose.ZeroIdentity;
            }
        }

        /// <summary>
        /// Evaluate the eye gaze ray at the given time.
        /// </summary>
        public Ray EvaluateEyeGaze(float time)
        {
            if (!HasEyeGaze)
            {
                return new Ray(Vector3.zero, Vector3.forward);
            }

            return gazeCurves.Evaluate(time);
        }

        /// <summary>
        /// Get the marker at the given index.
        /// </summary>
        public InputAnimationMarker GetMarker(int index)
        {
            return markers[index];
        }

        /// <summary>
        /// Generates an input animation from the contents of a recording buffer.
        /// </summary>
        /// <param name="recordingBuffer">The buffer to convert to an animation</param>
        /// <param name="profile">The profile that specifies the parameters for optimization</param>
        public static InputAnimation FromRecordingBuffer(InputRecordingBuffer recordingBuffer, MixedRealityInputRecordingProfile profile)
        {
            var animation = new InputAnimation();
            float startTime = recordingBuffer.StartTime;

            animation.HasHandData = profile.RecordHandData;
            animation.HasCameraPose = profile.RecordCameraPose;
            animation.HasEyeGaze = profile.RecordEyeGaze;

            foreach (var keyframe in recordingBuffer)
            {
                float localTime = keyframe.Time - startTime;

                if (profile.RecordHandData)
                {
                    AddBoolKeyIfChanged(animation.handTrackedCurveLeft, localTime, keyframe.LeftTracked);
                    AddBoolKeyIfChanged(animation.handTrackedCurveRight, localTime, keyframe.RightTracked);
                    AddBoolKeyIfChanged(animation.handPinchCurveLeft, localTime, keyframe.LeftPinch);
                    AddBoolKeyIfChanged(animation.handPinchCurveRight, localTime, keyframe.RightPinch);

                    foreach (var joint in (TrackedHandJoint[])Enum.GetValues(typeof(TrackedHandJoint)))
                    {
                        AddJointPoseKeys(animation.handJointCurvesLeft, keyframe.LeftJoints, joint, localTime);
                        AddJointPoseKeys(animation.handJointCurvesRight, keyframe.RightJoints, joint, localTime);
                    }
                }

                if (profile.RecordCameraPose)
                {
                    animation.cameraCurves.AddKey(localTime, keyframe.CameraPose);
                }

                if (profile.RecordEyeGaze)
                {
                    animation.gazeCurves.AddKey(localTime, keyframe.GazeRay);
                }
            }

            animation.Optimize(profile);
            animation.ComputeDuration();

            return animation;

            void AddBoolKeyIfChanged(AnimationCurve curve, float time, bool value)
            {
                if (curve.length > 0 && (curve[curve.length - 1].value > 0.5f) == value)
                {
                    return;
                }

                AddBoolKey(curve, time, value);
            }

            void AddJointPoseKeys(Dictionary<TrackedHandJoint, PoseCurves> jointCurves, Dictionary<TrackedHandJoint, MixedRealityPose> poses, TrackedHandJoint joint, float time)
            {
                if (!poses.TryGetValue(joint, out var pose))
                {
                    return;
                }

                if (!jointCurves.TryGetValue(joint, out var curves))
                {
                    curves = new PoseCurves();
                    jointCurves.Add(joint, curves);
                }

                curves.AddKey(time, pose);
            }
        }

        /// <summary>
        /// Deserializes animation data from a stream.
        /// </summary>
        public static InputAnimation FromStream(Stream stream)
        {
            var animation = new InputAnimation();
            var reader = new BinaryReader(stream);

            InputAnimationSerializationUtils.ReadHeader(reader, out int versionMajor, out int versionMinor);

            int latestVersionMajor = InputAnimationSerializationUtils.VersionMajor;
            int latestVersionMinor = InputAnimationSerializationUtils.VersionMinor;

            if (versionMajor > latestVersionMajor || versionMajor == latestVersionMajor && versionMinor > latestVersionMinor)
            {
                Debug.LogError($"Only version {latestVersionMajor}.{latestVersionMinor} and earlier of input animation file format is supported.");

                return animation;
            }

            bool useNewFormat = versionMajor > 1 || versionMajor == 1 && versionMinor >= 1;

            if (useNewFormat)
            {
                animation.HasCameraPose = reader.ReadBoolean();
                animation.HasHandData = reader.ReadBoolean();
                animation.HasEyeGaze = reader.ReadBoolean();
            }
            else
            {
                animation.HasCameraPose = true;
                animation.HasHandData = true;
                animation.HasEyeGaze = false;
            }

            if (animation.HasCameraPose)
            {
                PoseCurvesFromStream(reader, animation.cameraCurves, useNewFormat);
            }

            if (animation.HasHandData)
            {
                InputAnimationSerializationUtils.ReadBoolCurve(reader, animation.handTrackedCurveLeft);
                InputAnimationSerializationUtils.ReadBoolCurve(reader, animation.handTrackedCurveRight);
                InputAnimationSerializationUtils.ReadBoolCurve(reader, animation.handPinchCurveLeft);
                InputAnimationSerializationUtils.ReadBoolCurve(reader, animation.handPinchCurveRight);

                for (int i = 0; i < ArticulatedHandPose.JointCount; ++i)
                {
                    if (!animation.handJointCurvesLeft.TryGetValue((TrackedHandJoint)i, out var curves))
                    {
                        curves = new PoseCurves();
                        animation.handJointCurvesLeft.Add((TrackedHandJoint)i, curves);
                    }

                    PoseCurvesFromStream(reader, curves, useNewFormat);
                }

                for (int i = 0; i < ArticulatedHandPose.JointCount; ++i)
                {
                    if (!animation.handJointCurvesRight.TryGetValue(key: (TrackedHandJoint)i, out var curves))
                    {
                        curves = new PoseCurves();
                        animation.handJointCurvesRight.Add((TrackedHandJoint)i, curves);
                    }

                    PoseCurvesFromStream(reader, curves, useNewFormat);
                }
            }

            if (animation.HasEyeGaze)
            {
                RayCurvesFromStream(reader, animation.gazeCurves, useNewFormat);
            }

            InputAnimationSerializationUtils.ReadMarkerList(reader, animation.markers);
            animation.ComputeDuration();

            return animation;
        }

        /// <summary>
        /// Deserialize animation data from a stream asynchronously.
        /// </summary>
        public static async Task<InputAnimation> FromStreamAsync(Stream stream, Action callback = null)
        {
            var result = await Task.Run(() => FromStream(stream));

            callback?.Invoke();

            return result;
        }

        /// <summary>
        /// Add a keyframe for the tracking state of a hand.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private void AddHandStateKey(float time, bool isTracked, bool isPinching, AnimationCurve trackedCurve, AnimationCurve pinchCurve)
        {
            AddBoolKeyFiltered(trackedCurve, time, isTracked);
            AddBoolKeyFiltered(pinchCurve, time, isPinching);

            duration = Mathf.Max(duration, time);
        }

        /// <summary>
        /// Add a keyframe for one hand joint.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private void AddHandJointKey(float time, TrackedHandJoint joint, MixedRealityPose jointPose, Dictionary<TrackedHandJoint, PoseCurves> jointCurves, float positionThreshold, float rotationThreshold)
        {
            if (!jointCurves.TryGetValue(joint, out var curves))
            {
                curves = new PoseCurves();
                jointCurves.Add(joint, curves);
            }

            AddPoseKeyFiltered(curves, time, jointPose, positionThreshold, rotationThreshold);

            duration = Mathf.Max(duration, time);
        }

        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private void CutoffBeforeTime(AnimationCurve curve, float time)
        {
            // Keep the keyframe before the cutoff time to ensure correct value at the beginning
            int idx0 = FindKeyframeInterval(curve, time);
            if (idx0 > 0)
            {
                var newKeys = new Keyframe[curve.keys.Length - idx0];
                for (int i = 0; i < newKeys.Length; ++i)
                {
                    newKeys[i] = curve.keys[idx0 + i];
                }
                curve.keys = newKeys;
            }
        }

        /// <summary>
        /// Make sure the pose animation curves for the given hand joint exist.
        /// </summary>
        [Obsolete("Unused")]
        private PoseCurves CreateHandJointCurves(Handedness handedness, TrackedHandJoint joint)
        {
            if (handedness == Handedness.Left)
            {
                if (!handJointCurvesLeft.TryGetValue(joint, out var curves))
                {
                    curves = new PoseCurves();
                    handJointCurvesLeft.Add(joint, curves);
                }
                return curves;
            }
            else if (handedness == Handedness.Right)
            {
                if (!handJointCurvesRight.TryGetValue(joint, out var curves))
                {
                    curves = new PoseCurves();
                    handJointCurvesRight.Add(joint, curves);
                }
                return curves;
            }
            return null;
        }

        /// <summary>
        /// Get animation curves for the pose of the given hand joint, if they exist.
        /// </summary>
        [Obsolete("Use EvaluateHandJoint to get joint pose data")]
        private bool TryGetHandJointCurves(Handedness handedness, TrackedHandJoint joint, out PoseCurves curves)
        {
            if (handedness == Handedness.Left)
            {
                return handJointCurvesLeft.TryGetValue(joint, out curves);
            }
            else if (handedness == Handedness.Right)
            {
                return handJointCurvesRight.TryGetValue(joint, out curves);
            }
            curves = null;
            return false;
        }

        private void ComputeDuration()
        {
            duration = 0.0f;
            foreach (var curve in GetAllAnimationCurves())
            {
                float curveDuration = (curve.length > 0 ? curve.keys[curve.length - 1].time : 0.0f);
                duration = Mathf.Max(duration, curveDuration);
            }
        }

        /// <summary>
        /// Optimizes the curves contained within the animation
        /// </summary>
        private void Optimize(MixedRealityInputRecordingProfile profile)
        {
            if (profile.RecordCameraPose)
            {
                cameraCurves.Optimize(profile.CameraPositionThreshold, profile.CameraRotationThreshold, profile.PartitionSize);
            }

            if (profile.RecordEyeGaze)
            {
                gazeCurves.Optimize(profile.EyeGazeOriginThreshold, profile.EyeGazeDirectionThreshold, profile.PartitionSize);
            }

            if (profile.RecordHandData)
            {
                foreach (var poseCurves in handJointCurvesLeft.Values)
                {
                    poseCurves.Optimize(profile.JointPositionThreshold, profile.JointRotationThreshold, profile.PartitionSize);
                }

                foreach (var poseCurves in handJointCurvesRight.Values)
                {
                    poseCurves.Optimize(profile.JointPositionThreshold, profile.JointRotationThreshold, profile.PartitionSize);
                }
            }
        }

        /// <summary>
        /// Evaluate hand tracking state at the given time.
        /// </summary>
        private void EvaluateHandState(float time, AnimationCurve trackedCurve, AnimationCurve pinchCurve, out bool isTracked, out bool isPinching)
        {
            isTracked = (trackedCurve.Evaluate(time) > 0.5f);
            isPinching = (pinchCurve.Evaluate(time) > 0.5f);
        }

        /// <summary>
        /// Evaluate joint pose at the given time.
        /// </summary>
        private MixedRealityPose EvaluateHandJoint(float time, TrackedHandJoint joint, Dictionary<TrackedHandJoint, PoseCurves> jointCurves)
        {
            if (jointCurves.TryGetValue(joint, out var curves))
            {
                return curves.Evaluate(time);
            }
            else
            {
                return MixedRealityPose.ZeroIdentity;
            }
        }

        private IEnumerable<AnimationCurve> GetAllAnimationCurves()
        {
            yield return handTrackedCurveLeft;
            yield return handTrackedCurveRight;
            yield return handPinchCurveLeft;
            yield return handPinchCurveRight;

            foreach (var curves in handJointCurvesLeft.Values)
            {
                yield return curves.PositionX;
                yield return curves.PositionY;
                yield return curves.PositionZ;
                yield return curves.RotationX;
                yield return curves.RotationY;
                yield return curves.RotationZ;
                yield return curves.RotationW;
            }

            foreach (var curves in handJointCurvesRight.Values)
            {
                yield return curves.PositionX;
                yield return curves.PositionY;
                yield return curves.PositionZ;
                yield return curves.RotationX;
                yield return curves.RotationY;
                yield return curves.RotationZ;
                yield return curves.RotationW;
            }

            yield return cameraCurves.PositionX;
            yield return cameraCurves.PositionY;
            yield return cameraCurves.PositionZ;
            yield return cameraCurves.RotationX;
            yield return cameraCurves.RotationY;
            yield return cameraCurves.RotationZ;
            yield return cameraCurves.RotationW;
            yield return gazeCurves.OriginX;
            yield return gazeCurves.OriginY;
            yield return gazeCurves.OriginZ;
            yield return gazeCurves.DirectionX;
            yield return gazeCurves.DirectionY;
            yield return gazeCurves.DirectionZ;
        }

        /// <summary>
        /// Utility function that creates a non-interpolated keyframe suitable for boolean values.
        /// </summary>
        private static void AddBoolKey(AnimationCurve curve, float time, bool value)
        {
            float fvalue = value ? 1.0f : 0.0f;
            // Set tangents and weights such than the input value is cut off and out tangent is constant.
            var keyframe = new Keyframe(time, fvalue, 0.0f, 0.0f, 0.0f, BoolOutWeight);

            keyframe.weightedMode = WeightedMode.Both;
            curve.AddKey(keyframe);
        }

        /// <summary>
        /// Add a float value to an animation curve.
        /// </summary>
        private static void AddFloatKey(AnimationCurve curve, float time, float value)
        {
            // Use linear interpolation by setting tangents and weights to zero.
            var keyframe = new Keyframe(time, value, 0.0f, 0.0f, 0.0f, 0.0f);

            keyframe.weightedMode = WeightedMode.Both;
            curve.AddKey(keyframe);
        }

        /// <summary>
        /// Add a vector value to an animation curve.
        /// </summary>
        private static void AddVectorKey(AnimationCurve curveX, AnimationCurve curveY, AnimationCurve curveZ, float time, Vector3 vector)
        {
            curveX.AddKey(time, vector.x);
            curveY.AddKey(time, vector.y);
            curveZ.AddKey(time, vector.z);
        }

        /// <summary>
        /// Add a pose keyframe to an animation curve.
        /// Keys are only added if the value changes sufficiently.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private static void AddPoseKeyFiltered(PoseCurves curves, float time, MixedRealityPose pose, float positionThreshold, float rotationThreshold)
        {
            AddPositionKeyFiltered(curves.PositionX, curves.PositionY, curves.PositionZ, time, pose.Position, positionThreshold);
            AddRotationKeyFiltered(curves.RotationX, curves.RotationY, curves.RotationZ, curves.RotationW, time, pose.Rotation, rotationThreshold);
        }

        /// <summary>
        /// Add a vector keyframe to animation curve if the threshold distance to the previous value is exceeded.
        /// Otherwise replace the last keyframe instead of adding a new one.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private static void AddPositionKeyFiltered(AnimationCurve curveX, AnimationCurve curveY, AnimationCurve curveZ, float time, Vector3 position, float threshold)
        {
            float sqrThreshold = threshold * threshold;

            int iX = FindKeyframeInterval(curveX, time);
            int iY = FindKeyframeInterval(curveY, time);
            int iZ = FindKeyframeInterval(curveZ, time);

            if (iX > 0 && iY > 0 && iZ > 0)
            {
                var v0 = new Vector3(curveX.keys[iX - 1].value, curveY.keys[iY - 1].value, curveZ.keys[iZ - 1].value);
                var v1 = new Vector3(curveX.keys[iX].value, curveY.keys[iY].value, curveZ.keys[iZ].value);

                // Merge the preceding two intervals if difference is small enough
                if ((v1 - v0).sqrMagnitude <= sqrThreshold && (position - v1).sqrMagnitude <= sqrThreshold)
                {
                    curveX.RemoveKey(iX);
                    curveY.RemoveKey(iY);
                    curveZ.RemoveKey(iZ);
                }
            }

            AddFloatKey(curveX, time, position.x);
            AddFloatKey(curveY, time, position.y);
            AddFloatKey(curveZ, time, position.z);
        }

        /// <summary>
        /// Add a quaternion keyframe to animation curve if the threshold angular difference (in degrees) to the previous value is exceeded.
        /// Otherwise replace the last keyframe instead of adding a new one.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private static void AddRotationKeyFiltered(AnimationCurve curveX, AnimationCurve curveY, AnimationCurve curveZ, AnimationCurve curveW, float time, Quaternion rotation, float threshold)
        {
            // Precompute the dot product threshold so that dot product can be used for comparison instead of angular difference
            float compThreshold = Mathf.Sqrt((Mathf.Cos(threshold * Mathf.PI / 180f) + 1f) / 2f);
            int iX = FindKeyframeInterval(curveX, time);
            int iY = FindKeyframeInterval(curveY, time);
            int iZ = FindKeyframeInterval(curveZ, time);
            int iW = FindKeyframeInterval(curveW, time);

            if (iX > 0 && iY > 0 && iZ > 0 && iW > 0)
            {
                var v0 = new Quaternion(curveX.keys[iX - 1].value, curveY.keys[iY - 1].value, curveZ.keys[iZ - 1].value, curveW.keys[iW - 1].value);
                var v1 = new Quaternion(curveX.keys[iX].value, curveY.keys[iY].value, curveZ.keys[iZ].value, curveW.keys[iW].value);

                // Merge the preceding two intervals if difference is small enough
                if (Quaternion.Dot(v0, v1) >= compThreshold && Quaternion.Dot(rotation, v1) >= compThreshold)
                {
                    curveX.RemoveKey(iX);
                    curveY.RemoveKey(iY);
                    curveZ.RemoveKey(iZ);
                    curveW.RemoveKey(iW);
                }
            }

            AddFloatKey(curveX, time, rotation.x);
            AddFloatKey(curveY, time, rotation.y);
            AddFloatKey(curveZ, time, rotation.z);
            AddFloatKey(curveW, time, rotation.w);
        }

        private static void PoseCurvesToStream(BinaryWriter writer, PoseCurves curves, float startTime)
        {
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.PositionX, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.PositionY, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.PositionZ, startTime);

            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.RotationX, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.RotationY, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.RotationZ, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.RotationW, startTime);
        }

        private static void PoseCurvesFromStream(BinaryReader reader, PoseCurves curves, bool readSimple)
        {
            if (readSimple)
            {
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.PositionX);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.PositionY);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.PositionZ);

                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.RotationX);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.RotationY);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.RotationZ);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.RotationW);
            }
            else
            {
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.PositionX);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.PositionY);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.PositionZ);

                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.RotationX);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.RotationY);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.RotationZ);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.RotationW);
            }
        }

        private static void RayCurvesToStream(BinaryWriter writer, RayCurves curves, float startTime)
        {
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.OriginX, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.OriginY, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.OriginZ, startTime);

            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.DirectionX, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.DirectionY, startTime);
            InputAnimationSerializationUtils.WriteFloatCurveSimple(writer, curves.DirectionZ, startTime);
        }

        private static void RayCurvesFromStream(BinaryReader reader, RayCurves curves, bool readSimple)
        {
            if (readSimple)
            {
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.OriginX);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.OriginY);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.OriginZ);

                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.DirectionX);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.DirectionY);
                InputAnimationSerializationUtils.ReadFloatCurveSimple(reader, curves.DirectionZ);
            }
            else
            {
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.OriginX);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.OriginY);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.OriginZ);

                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.DirectionX);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.DirectionY);
                InputAnimationSerializationUtils.ReadFloatCurve(reader, curves.DirectionZ);
            }
        }

        /// <summary>
        /// Removes points from a set of curves representing a 3D position, such that the error resulting from removing a point never exceeds 'threshold' units.
        /// </summary>
        /// <param name="threshold">The maximum permitted error between the old and new curves, in units.</param>
        /// <param name="partitionSize">The size of the partitions of the curves that will be optimized independently. Larger values will optimize the curves better, but may take longer.</param>
        /// <remarks>Uses the Ramer–Douglas–Peucker algorithm</remarks>
        private static void OptimizePositionCurve(ref AnimationCurve curveX, ref AnimationCurve curveY, ref AnimationCurve curveZ, float threshold, int partitionSize)
        {
            float sqrThreshold = threshold * threshold;
            var inCurveX = curveX;
            var inCurveY = curveY;
            var inCurveZ = curveZ;
            // Create new curves to avoid deleting points while iterating.
            var outCurveX = new AnimationCurve();
            var outCurveY = new AnimationCurve();
            var outCurveZ = new AnimationCurve();

            outCurveX.AddKey(curveX[0]);
            outCurveY.AddKey(curveY[0]);
            outCurveZ.AddKey(curveZ[0]);

            if (partitionSize == 0)
            {
                Recurse(0, curveX.length - 1);
                outCurveX.AddKey(curveX[curveX.length - 1]);
                outCurveY.AddKey(curveY[curveY.length - 1]);
                outCurveZ.AddKey(curveZ[curveZ.length - 1]);
            }
            else
            {
                for (int i = 0, j = partitionSize; i < curveX.length - partitionSize; i += partitionSize, j = Mathf.Min(j + partitionSize, curveX.length - 1))
                {
                    Recurse(i, j);
                    outCurveX.AddKey(curveX[j]);
                    outCurveY.AddKey(curveY[j]);
                    outCurveZ.AddKey(curveZ[j]);
                }
            }

            curveX = outCurveX;
            curveY = outCurveY;
            curveZ = outCurveZ;

            void Recurse(int start, int end)
            {
                if (start + 1 >= end - 1)
                {
                    return;
                }

                int bestIndex = -1;
                float bestDistance = 0f;
                float startTime = inCurveX[start].time;
                float endTime = inCurveX[end].time;
                var startPosition = new Vector3(inCurveX[start].value, inCurveY[start].value, inCurveZ[start].value);
                var endPosition = new Vector3(inCurveX[end].value, inCurveY[end].value, inCurveZ[end].value);

                for (int i = start + 1; i <= end - 1; i++)
                {
                    var position = new Vector3(inCurveX[i].value, inCurveY[i].value, inCurveZ[i].value);
                    var interp = Vector3.Lerp(startPosition, endPosition, Mathf.InverseLerp(startTime, endTime, inCurveX[i].time));

                    float distance = (position - interp).sqrMagnitude;

                    if (distance > bestDistance)
                    {
                        bestIndex = i;
                        bestDistance = distance;
                    }
                }

                if (bestDistance < sqrThreshold || bestIndex < 0)
                {
                    return;
                }

                outCurveX.AddKey(inCurveX[bestIndex]);
                outCurveY.AddKey(inCurveY[bestIndex]);
                outCurveZ.AddKey(inCurveZ[bestIndex]);
                Recurse(start, bestIndex);
                Recurse(bestIndex, end);
            }
        }

        /// <summary>
        /// Removes points from a set of curves representing a 3D direction vector, such that the error resulting from removing a point never exceeds 'threshold' degrees.
        /// </summary>
        /// <param name="threshold">The maximum permitted error between the old and new curves, in degrees.</param>
        /// <param name="partitionSize">The size of the partitions of the curves that will be optimized independently. Larger values will optimize the curves better, but may take longer.</param>
        /// <remarks>Uses the Ramer–Douglas–Peucker algorithm</remarks>
        private static void OptimizeDirectionCurve(ref AnimationCurve curveX, ref AnimationCurve curveY, ref AnimationCurve curveZ, float threshold, int partitionSize)
        {
            float cosThreshold = Mathf.Cos(threshold * Mathf.PI / 180f);
            var inCurveX = curveX;
            var inCurveY = curveY;
            var inCurveZ = curveZ;
            // Create new curves to avoid deleting points while iterating.
            var outCurveX = new AnimationCurve();
            var outCurveY = new AnimationCurve();
            var outCurveZ = new AnimationCurve();

            outCurveX.AddKey(curveX[0]);
            outCurveY.AddKey(curveY[0]);
            outCurveZ.AddKey(curveZ[0]);

            if (partitionSize == 0)
            {
                Recurse(0, curveX.length - 1);
                outCurveX.AddKey(curveX[curveX.length - 1]);
                outCurveY.AddKey(curveY[curveY.length - 1]);
                outCurveZ.AddKey(curveZ[curveZ.length - 1]);
            }
            else
            {
                for (int i = 0, j = partitionSize; i < curveX.length - partitionSize; i += partitionSize, j = Mathf.Min(j + partitionSize, curveX.length - 1))
                {
                    Recurse(i, j);
                    outCurveX.AddKey(curveX[j]);
                    outCurveY.AddKey(curveY[j]);
                    outCurveZ.AddKey(curveZ[j]);
                }
            }

            curveX = outCurveX;
            curveY = outCurveY;
            curveZ = outCurveZ;

            void Recurse(int start, int end)
            {
                if (start + 1 >= end - 1)
                {
                    return;
                }

                int bestIndex = -1;
                float bestDot = 1f;
                float startTime = inCurveX[start].time;
                float endTime = inCurveX[end].time;
                var startPosition = new Vector3(inCurveX[start].value, inCurveY[start].value, inCurveZ[start].value);
                var endPosition = new Vector3(inCurveX[end].value, inCurveY[end].value, inCurveZ[end].value);

                for (int i = start + 1; i <= end - 1; i++)
                {
                    var position = new Vector3(inCurveX[i].value, inCurveY[i].value, inCurveZ[i].value);
                    var interp = Vector3.Lerp(startPosition, endPosition, Mathf.InverseLerp(startTime, endTime, inCurveX[i].time)).normalized;

                    float dot = Vector3.Dot(position, interp);

                    if (dot < bestDot)
                    {
                        bestIndex = i;
                        bestDot = dot;
                    }
                }

                if (bestDot > cosThreshold || bestIndex < 0)
                {
                    return;
                }

                outCurveX.AddKey(inCurveX[bestIndex]);
                outCurveY.AddKey(inCurveY[bestIndex]);
                outCurveZ.AddKey(inCurveZ[bestIndex]);
                Recurse(start, bestIndex);
                Recurse(bestIndex, end);
            }
        }

        /// <summary>
        /// Removes points from a set of curves representing a quaternion, such that the error resulting from removing a point never exceeds 'threshold' degrees.
        /// </summary>
        /// <param name="threshold">The maximum permitted error between the old and new curves, in degrees</param>
        /// <param name="partitionSize">The size of the partitions of the curves that will be optimized independently. Larger values will optimize the curves better, but may take longer.</param>
        /// <remarks>Uses the Ramer–Douglas–Peucker algorithm</remarks>
        private static void OptimizeRotationCurve(ref AnimationCurve curveX, ref AnimationCurve curveY, ref AnimationCurve curveZ, ref AnimationCurve curveW, float threshold, int partitionSize)
        {
            float compThreshold = Mathf.Sqrt((Mathf.Cos(threshold * Mathf.PI / 180f) + 1f) / 2f);
            var inCurveX = curveX;
            var inCurveY = curveY;
            var inCurveZ = curveZ;
            var inCurveW = curveW;
            // Create new curves to avoid deleting points while iterating.
            var outCurveX = new AnimationCurve();
            var outCurveY = new AnimationCurve();
            var outCurveZ = new AnimationCurve();
            var outCurveW = new AnimationCurve();

            outCurveX.AddKey(curveX[0]);
            outCurveY.AddKey(curveY[0]);
            outCurveZ.AddKey(curveZ[0]);
            outCurveW.AddKey(curveW[0]);

            if (partitionSize == 0)
            {
                Recurse(0, curveX.length - 1);
                outCurveX.AddKey(curveX[curveX.length - 1]);
                outCurveY.AddKey(curveY[curveY.length - 1]);
                outCurveZ.AddKey(curveZ[curveZ.length - 1]);
                outCurveW.AddKey(curveW[curveW.length - 1]);
            }
            else
            {
                for (int i = 0, j = partitionSize; i < curveX.length - partitionSize; i += partitionSize, j = Mathf.Min(j + partitionSize, curveX.length - 1))
                {
                    Recurse(i, j);
                    outCurveX.AddKey(curveX[j]);
                    outCurveY.AddKey(curveY[j]);
                    outCurveZ.AddKey(curveZ[j]);
                    outCurveW.AddKey(curveW[j]);
                }
            }

            curveX = outCurveX;
            curveY = outCurveY;
            curveZ = outCurveZ;
            curveW = outCurveW;

            void Recurse(int start, int end)
            {
                if (start + 1 >= end - 1)
                {
                    return;
                }

                int bestIndex = -1;
                float bestDot = 1f;
                float startTime = inCurveX[start].time;
                float endTime = inCurveX[end].time;
                var startRotation = new Quaternion(inCurveX[start].value, inCurveY[start].value, inCurveZ[start].value, inCurveW[start].value).normalized;
                var endRotation = new Quaternion(inCurveX[end].value, inCurveY[end].value, inCurveZ[end].value, inCurveW[end].value).normalized;

                for (int i = start + 1; i <= end - 1; i++)
                {
                    var rotation = new Quaternion(inCurveX[i].value, inCurveY[i].value, inCurveZ[i].value, inCurveW[i].value).normalized;
                    var interp = Quaternion.Lerp(startRotation, endRotation, Mathf.InverseLerp(startTime, endTime, inCurveX[i].time));

                    float dot = Quaternion.Dot(rotation, interp);

                    if (dot < bestDot)
                    {
                        bestIndex = i;
                        bestDot = dot;
                    }
                }

                if (bestDot > compThreshold || bestIndex < 0)
                {
                    return;
                }

                outCurveX.AddKey(inCurveX[bestIndex]);
                outCurveY.AddKey(inCurveY[bestIndex]);
                outCurveZ.AddKey(inCurveZ[bestIndex]);
                outCurveW.AddKey(inCurveW[bestIndex]);
                Recurse(start, bestIndex);
                Recurse(bestIndex, end);
            }
        }

        /// <summary>
        /// Utility function that creates a non-interpolated keyframe suitable for boolean values.
        /// Keys are only added if the value changes.
        /// Returns the index of the newly added keyframe, or -1 if no keyframe has been added.
        /// </summary>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private static int AddBoolKeyFiltered(AnimationCurve curve, float time, bool value)
        {
            float fvalue = value ? 1.0f : 0.0f;
            // Set tangents and weights such than the input value is cut off and out tangent is constant.
            var keyframe = new Keyframe(time, fvalue, 0.0f, 0.0f, 0.0f, BoolOutWeight);
            keyframe.weightedMode = WeightedMode.Both;

            int insertAfter = FindKeyframeInterval(curve, time);
            if (insertAfter >= 0 && curve.keys[insertAfter].value == fvalue)
            {
                // Value unchanged from previous key, ignore
                return -1;
            }

            int insertBefore = insertAfter + 1;
            if (insertBefore < curve.keys.Length && curve.keys[insertBefore].value == fvalue)
            {
                // Same value as next key, replace next key
                return curve.MoveKey(insertBefore, keyframe);
            }

            return curve.AddKey(keyframe);
        }

        /// <summary>
        /// Find an index i in the sorted events list, such that events[i].time &lt;= time &lt; events[i+1].time.
        /// </summary>
        /// <returns>
        /// 0 &lt;= i &lt; eventCount if a full interval could be found.
        /// -1 if time is less than the first event time.
        /// eventCount-1 if time is greater than the last event time.
        /// </returns>
        /// <remarks>
        /// Uses binary search.
        /// </remarks>
        [Obsolete("Use FromRecordingBuffer to construct new InputAnimations")]
        private static int FindKeyframeInterval(AnimationCurve curve, float time)
        {
            var keys = curve.keys;
            int lowIdx = -1;
            int highIdx = keys.Length;
            while (lowIdx < highIdx - 1)
            {
                int midIdx = (lowIdx + highIdx) >> 1;
                if (time >= keys[midIdx].time)
                {
                    lowIdx = midIdx;
                }
                else
                {
                    highIdx = midIdx;
                }
            }
            return lowIdx;
        }
    }
}
