// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using UnityEngine;

namespace HoloToolkit.Unity.InputModule
{
    /// <summary>
    /// GazeStabilizer iterates over samples of Raycast data and
    /// helps stabilize the user's gaze for precision targeting.
    /// </summary>
    public class GazeStabilizer : BaseRayStabilizer
    {
        [Tooltip("Number of samples that you want to iterate on.")]
        [Range(40, 120)]
        public int StoredStabilitySamples = 60;

        private Vector3 stablePosition;
        public override Vector3 StablePosition
        {
            get { return stablePosition; }
        }

        private Quaternion stableRotation;
        public override Quaternion StableRotation
        {
            get { return stableRotation; }
        }

        private Ray stableRay;
        public override Ray StableRay
        {
            get { return stableRay; }
        }

        /// <summary>
        /// Calculates standard deviation and averages for the gaze position.
        /// </summary>
        private readonly VectorRollingStatistics positionRollingStats = new VectorRollingStatistics();

        /// <summary>
        /// Calculates standard deviation and averages for the gaze direction.
        /// </summary>
        private readonly VectorRollingStatistics directionRollingStats = new VectorRollingStatistics();

        /// <summary>
        /// Tunable parameter.
        /// If the standard deviation for the position is above this value, we reset and stop stabilizing.
        /// </summary>
        private const float PositionStandardDeviationReset = 0.2f;

        /// <summary>
        /// Tunable parameter.
        /// If the standard deviation for the direction is above this value, we reset and stop stabilizing.
        /// </summary>
        private const float DirectionStandardDeviationReset = 0.1f;

        /// <summary>
        /// We must have at least this many samples with a standard deviation below the above constants to stabalize
        /// </summary>
        private const int MinimumSamplesRequiredToStabalize = 30;

        /// <summary>
        /// When not stabalizing this is the 'lerp' applied to the position and direction of the gaze to smooth it over time.
        /// </summary>
        private const float UnstabalizedLerpFactor = 0.3f;

        /// <summary>
        /// When stabalizing we will use the standard deviation of the position and direction to create the lerp value.
        /// By default this value will be low and the cursor will be too sluggish, so we 'boost' it by this value.
        /// </summary>
        private const float StabalizedLerpBoost = 10.0f;

        private void Awake()
        {
            directionRollingStats.Init(StoredStabilitySamples);
            positionRollingStats.Init(StoredStabilitySamples);
        }

        /// <summary>
        /// Updates the StablePosition and StableRotation based on GazeSample values.
        /// Call this method with Raycasthit parameters to get stable values.
        /// </summary>
        /// <param name="gazePosition">Position value from a RaycastHit point.</param>
        /// <param name="gazeDirection">Direction value from a RaycastHit rotation.</param>
        public override void UpdateStability(Vector3 gazePosition, Vector3 gazeDirection)
        {
            positionRollingStats.AddSample(gazePosition);
            directionRollingStats.AddSample(gazeDirection);

            float lerpPower = UnstabalizedLerpFactor;
            if (positionRollingStats.ActualSampleCount > MinimumSamplesRequiredToStabalize && // we have enough samples and...
                (positionRollingStats.CurrentStandardDeviation > PositionStandardDeviationReset || // the standard deviation of positions is high or...
                 directionRollingStats.CurrentStandardDeviation > DirectionStandardDeviationReset)) // the standard deviation of directions is high
            {
                // We've detected that the user's gaze is no longer fixed, so stop stabalizing so that gaze is responsive.
                //Debug.LogFormat("Reset {0} {1} {2} {3}", positionRollingStats.standardDeviation, positionRollingStats.standardDeviationsAway, directionRollignStats.standardDeviation, directionRollignStats.standardDeviationsAway);
                positionRollingStats.Reset();
                directionRollingStats.Reset();
            }
            else if (positionRollingStats.ActualSampleCount > MinimumSamplesRequiredToStabalize)
            {
                // We've detected that the user's gaze is fairly fixed, so start stabalizing.  The more fixed the gaze the less the cursor will move.
                lerpPower = StabalizedLerpBoost * (positionRollingStats.CurrentStandardDeviation + directionRollingStats.CurrentStandardDeviation);
            }

            stablePosition = Vector3.Lerp(stablePosition, gazePosition, lerpPower);
            stableRotation = Quaternion.LookRotation(Vector3.Lerp(stableRotation * Vector3.forward, gazeDirection, lerpPower));
            stableRay = new Ray(stablePosition, stableRotation * Vector3.forward);
        }
    }
}