/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * Licensed under the Oculus SDK License Agreement (the "License");
 * you may not use the Oculus SDK except in compliance with the License,
 * which is provided at the time of installation or download, or which
 * otherwise accompanies this software in either electronic or hard copy form.
 *
 * You may obtain a copy of the License at
 *
 * https://developer.oculus.com/licenses/oculussdk/
 *
 * Unless required by applicable law or agreed to in writing, the Oculus SDK
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using UnityEngine;

namespace Oculus.Interaction.Input
{
    /// <summary>
    /// Alters hand data piped into this modifier to lock and unlock joints (wrist position and rotation,
    /// finger joint rotations)  When switching between locked and unlocked states, additionally smooths
    /// out transitions by easing between source hand data and target hand data.
    /// </summary>
    public class SyntheticHand : Hand
    {
        [System.Flags]
        public enum WristLockMode
        {
            Position = 1 << 0,
            Rotation = 1 << 1,
            Full = (1 << 2) - 1
        }

        [SerializeField]
        private ProgressCurve _wristPositionLockCurve;
        [SerializeField]
        private ProgressCurve _wristPositionUnlockCurve;
        [SerializeField]
        private ProgressCurve _wristRotationLockCurve;
        [SerializeField]
        private ProgressCurve _wristRotationUnlockCurve;
        [SerializeField]
        private ProgressCurve _jointLockCurve;
        [SerializeField]
        private ProgressCurve _jointUnlockCurve;

        /// <summary>
        /// Use this factor to control how much the fingers can spread when nearby a constrained pose.
        /// </summary>
        [SerializeField]
        [Tooltip("Use this factor to control how much the fingers can spread when nearby a constrained pose.")]
        private float _spreadAllowance = 5f;

        public System.Action UpdateRequired = delegate { };

        private readonly HandDataAsset _lastStates = new HandDataAsset();

        private float _wristPositionOverrideFactor;
        private float _wristRotationOverrideFactor;

        private float[] _jointsOverrideFactor = new float[FingersMetadata.HAND_JOINT_IDS.Length];

        private ProgressCurve[] _jointLockProgressCurves = new ProgressCurve[FingersMetadata.HAND_JOINT_IDS.Length];
        private ProgressCurve[] _jointUnlockProgressCurves = new ProgressCurve[FingersMetadata.HAND_JOINT_IDS.Length];

        private Pose _desiredWristPose;
        private bool _wristPositionLocked;
        private bool _wristRotationLocked;
        private Pose _constrainedWristPose;
        private Pose _lastWristPose;

        private Quaternion[] _desiredJointsRotation = new Quaternion[FingersMetadata.HAND_JOINT_IDS.Length];
        private Quaternion[] _constrainedJointRotations = new Quaternion[FingersMetadata.HAND_JOINT_IDS.Length];
        private Quaternion[] _lastSyntheticRotation = new Quaternion[FingersMetadata.HAND_JOINT_IDS.Length];
        private JointFreedom[] _jointsFreedomLevels = new JointFreedom[FingersMetadata.HAND_JOINT_IDS.Length];

        private bool _hasConnectedData;

        protected override void Start()
        {
            this.BeginStart(ref _started, () => base.Start());
            for (int i = 0; i < FingersMetadata.HAND_JOINT_IDS.Length; i++)
            {
                _jointLockProgressCurves[i] = new ProgressCurve(_jointLockCurve);
                _jointUnlockProgressCurves[i] = new ProgressCurve(_jointUnlockCurve);
            }
            this.EndStart(ref _started);
        }

        protected override void Apply(HandDataAsset data)
        {
            if (!Started || !data.IsDataValid || !data.IsTracked || !data.IsHighConfidence)
            {
                data.IsConnected = false;
                data.RootPoseOrigin = PoseOrigin.None;
                _hasConnectedData = false;
                return;
            }

            UpdateRequired.Invoke();
            _lastStates.CopyFrom(data);

            if (!_hasConnectedData)
            {
                _constrainedWristPose.CopyFrom(data.Root);
                _hasConnectedData = true;
            }

            UpdateJointsRotation(data);
            UpdateRootPose(ref data.Root);

            data.RootPoseOrigin = PoseOrigin.SyntheticPose;
        }

        /// <summary>
        /// Updates the pose of the root of the hand
        /// using the visual provided values. Sometimes this
        /// might require lerping between the tracked pose
        /// and the provided one to improve the movement of the hand
        /// without worrying about when the overwrite value was written.
        ///
        /// During this update, the modifier also ensures the unlocking
        /// animations are executed.
        /// </summary>
        /// <param name="root">The tracked root value to modify</param>
        private void UpdateRootPose(ref Pose root)
        {
            float smoothPositionFactor = _wristPositionLocked ? _wristPositionLockCurve.Progress() : _wristPositionUnlockCurve.Progress();
            Vector3 position = Vector3.Lerp(root.position, _desiredWristPose.position, _wristPositionOverrideFactor);
            root.position = Vector3.Lerp(_constrainedWristPose.position, position, smoothPositionFactor);

            float smoothRotationFactor = _wristRotationLocked ? _wristRotationLockCurve.Progress() : _wristRotationUnlockCurve.Progress();
            Quaternion rotation = Quaternion.Lerp(root.rotation, _desiredWristPose.rotation, _wristRotationOverrideFactor);
            root.rotation = Quaternion.Lerp(_constrainedWristPose.rotation, rotation, smoothRotationFactor);

            _lastWristPose.CopyFrom(root);
        }

        /// <summary>
        /// Updates the rotation of the joints in the hand
        /// using the visual provided values. Sometimes this
        /// might require lerping between the tracked pose
        /// and the provided ones to improve the movement of the fingers
        /// without worrying about when the overwrite values were written.
        ///
        /// During this update the modifier also ensures that fingers that disallow
        /// some movement (locked or constrained) have their values properly set, and
        /// when there is an unlock event the finger values are smoothly animated back to
        /// their tracked rotations.
        /// </summary>
        /// <param name="data">The entire hand data structure to read and write the joints rotations from</param>
        private void UpdateJointsRotation(HandDataAsset data)
        {
            float extraRotationAllowance = 0f;
            Quaternion[] jointRotations = data.Joints;

            for (int i = 0; i < FingersMetadata.HAND_JOINT_IDS.Length; ++i)
            {
                JointFreedom freedomLevel = _jointsFreedomLevels[i];
                Quaternion desiredRotation = _desiredJointsRotation[i];
                float overrideFactor = _jointsOverrideFactor[i];
                int rawJointIndex = (int)FingersMetadata.HAND_JOINT_IDS[i];

                if (freedomLevel == JointFreedom.Free)
                {
                    //nothing to do, we move the finger freely
                }
                else if (freedomLevel == JointFreedom.Locked)
                {
                    jointRotations[rawJointIndex] = Quaternion.Slerp(
                        jointRotations[rawJointIndex],
                        desiredRotation,
                        overrideFactor);
                }
                else if (freedomLevel == JointFreedom.Constrained)
                {
                    bool jointCanSpread = false;
                    if (FingersMetadata.HAND_JOINT_CAN_SPREAD[i])
                    {
                        jointCanSpread = true;
                        extraRotationAllowance = 0f;
                    }

                    Quaternion maxRotation = desiredRotation * Quaternion.Euler(0f, 0f, -90f * extraRotationAllowance);

                    float overRotation = OverFlex(jointRotations[rawJointIndex], maxRotation);
                    extraRotationAllowance = Mathf.Max(extraRotationAllowance, overRotation);

                    if (overRotation < 0f)
                    {
                        jointRotations[rawJointIndex] = Quaternion.Slerp(
                            jointRotations[rawJointIndex],
                            maxRotation,
                            overrideFactor);
                    }
                    else if (jointCanSpread)
                    {
                        Quaternion trackedRotation = jointRotations[rawJointIndex];
                        float spreadAngle = Vector3.SignedAngle(
                            trackedRotation * Vector3.forward,
                            maxRotation * Vector3.forward,
                            trackedRotation * Vector3.up);

                        float spreadFactor = 1f - Mathf.Clamp01(overRotation * _spreadAllowance);
                        trackedRotation = trackedRotation * Quaternion.Euler(0f, spreadAngle * spreadFactor, 0f);
                        jointRotations[rawJointIndex] = trackedRotation;
                    }
                }

                float smoothFactor = _jointsFreedomLevels[i] == JointFreedom.Free ?
                    _jointUnlockProgressCurves[i].Progress()
                    : _jointLockProgressCurves[i].Progress();

                jointRotations[rawJointIndex] = Quaternion.Slerp(
                    _constrainedJointRotations[i],
                    jointRotations[rawJointIndex],
                    smoothFactor);

                _lastSyntheticRotation[i] = jointRotations[rawJointIndex];
            }
        }

        /// <summary>
        /// Stores the rotation data for all joints in the hand, to be applied during the ApplyHand event.
        /// </summary>
        /// <param name="jointRotations">The joint rotations following the FingersMetadata.HAND_JOINT_IDS format.</param>
        /// <param name="overrideFactor">How much to lerp the fingers from the tracked (raw) state to the provided one.</param>
        public void OverrideAllJoints(in Quaternion[] jointRotations, float overrideFactor)
        {
            for (int i = 0; i < FingersMetadata.HAND_JOINT_IDS.Length; ++i)
            {
                _desiredJointsRotation[i] = jointRotations[i];
                _jointsOverrideFactor[i] = overrideFactor;
            }
        }

        /// <summary>
        /// Stores the rotation data for all joints for the given finger, to be applied during the ApplyHand event.
        /// </summary>
        /// <param name="finger">The finger for which to lock joints.</param>
        /// <param name="rotations">The joint rotations for each joint on the finger</param>
        /// <param name="overrideFactor">How much to lerp the fingers from the tracked (raw) state to the provided one.</param>
        public void OverrideFingerRotations(HandFinger finger, Quaternion[] rotations, float overrideFactor)
        {
            int[] jointIndices = FingersMetadata.FINGER_TO_JOINT_INDEX[(int)finger];
            for (int i = 0; i < jointIndices.Length; i++)
            {
                OverrideJointRotationAtIndex(jointIndices[i], rotations[i], overrideFactor);
            }
        }

        public void OverrideJointRotation(HandJointId jointId, Quaternion rotation, float overrideFactor)
        {
            int jointIndex = FingersMetadata.HandJointIdToIndex(jointId);
            OverrideJointRotationAtIndex(jointIndex, rotation, overrideFactor);
        }

        private void OverrideJointRotationAtIndex(int jointIndex, Quaternion rotation, float overrideFactor)
        {
            _desiredJointsRotation[jointIndex] = rotation;
            _jointsOverrideFactor[jointIndex] = overrideFactor;
        }

        /// <summary>
        /// Immediately locks an individual finger (all its internal joints) at the last known value.
        /// </summary>
        /// <param name="finger">The finger for which to lock joints.</param>
        public void LockFingerAtCurrent(in HandFinger finger)
        {
            SetFingerFreedom(finger, JointFreedom.Locked);

            int fingerIndex = (int)finger;
            int[] jointIndexes = FingersMetadata.FINGER_TO_JOINT_INDEX[fingerIndex];
            for (int i = 0; i < jointIndexes.Length; ++i)
            {
                int jointIndex = jointIndexes[i];
                int rawJointIndex = (int)FingersMetadata.HAND_JOINT_IDS[jointIndex];

                _desiredJointsRotation[jointIndex] = _lastStates.Joints[rawJointIndex];
                _jointsOverrideFactor[jointIndex] = 1f;
            }
        }

        public void LockJoint(in HandJointId jointId, Quaternion rotation, float overrideFactor = 1f)
        {
            int jointIndex = FingersMetadata.HandJointIdToIndex(jointId);
            _desiredJointsRotation[jointIndex] = rotation;
            _jointsOverrideFactor[jointIndex] = 1f;
            SetJointFreedomAtIndex(jointIndex, JointFreedom.Locked);
        }

        /// <summary>
        /// To use in conjunction with OverrideAllJoints, it sets the freedom state for a provided finger.
        /// Opposite to LockFingerAtCurrent, this method uses the data provided in OverrideAllJoints instead
        /// of the last known state.
        /// </summary>
        /// <param name="freedomLevel">The freedom level for the finger</param>
        public void SetFingerFreedom(in HandFinger finger, in JointFreedom freedomLevel, bool skipAnimation = false)
        {
            int[] jointIndexes = FingersMetadata.FINGER_TO_JOINT_INDEX[(int)finger];
            for (int i = 0; i < jointIndexes.Length; ++i)
            {
                SetJointFreedomAtIndex(jointIndexes[i], freedomLevel, skipAnimation);
            }
        }

        public void SetJointFreedom(in HandJointId jointId, in JointFreedom freedomLevel, bool skipAnimation = false)
        {
            int jointIndex = FingersMetadata.HandJointIdToIndex(jointId);
            SetJointFreedomAtIndex(jointIndex, freedomLevel, skipAnimation);
        }

        public JointFreedom GetJointFreedom(in HandJointId jointId)
        {
            int jointIndex = FingersMetadata.HandJointIdToIndex(jointId);
            return _jointsFreedomLevels[jointIndex];
        }

        /// <summary>
        /// Short-hand method for setting the freedom level of all fingers in a hand to Free.
        /// Similar to calling SetFingerFreedom for each single finger in the hand
        /// with a value of FingerFreedom.Free for the freedomLevel
        /// </summary>
        public void FreeAllJoints()
        {
            for (int i = 0; i < FingersMetadata.HAND_JOINT_IDS.Length; ++i)
            {
                SetJointFreedomAtIndex(i, JointFreedom.Free);
            }
        }

        private void SetJointFreedomAtIndex(int jointId, in JointFreedom freedomLevel, bool skipAnimation = false)
        {
            JointFreedom currentFreedom = _jointsFreedomLevels[jointId];
            if (currentFreedom != freedomLevel)
            {
                bool locked = freedomLevel == JointFreedom.Locked
                    || freedomLevel == JointFreedom.Constrained;
                UpdateProgressCurve(ref _jointLockProgressCurves[jointId],
                    ref _jointUnlockProgressCurves[jointId],
                    locked, skipAnimation);
                _constrainedJointRotations[jointId] = _lastSyntheticRotation[jointId];
            }

            _jointsFreedomLevels[jointId] = freedomLevel;
        }

        /// <summary>
        /// Stores the desired pose to set the wrist of the hand to.
        /// This is not necessarily the final pose of the hand, as it allows
        /// lerping between the tracked and provided one during the ApplyHand phase.
        ///
        /// To ensure the hand is locked at the desired pose, pass a value of 1 in the overrideFactor
        /// </summary>
        /// <param name="wristPose">The final pose desired for the wrist</param>
        /// <param name="lockMode">Either lock the position, rotation or both (default)</param>
        /// <param name="overrideFactor">How much to lerp between the tracked and the provided pose</param>
        /// <param name="skipAnimation">Whether to skip the animation curve for this override.</param>
        public void LockWristPose(Pose wristPose, float overrideFactor = 1f, WristLockMode lockMode = WristLockMode.Full, bool worldPose = false, bool skipAnimation = false)
        {
            Pose desiredWristPose = (worldPose && TrackingToWorldTransformer != null ) ?
                TrackingToWorldTransformer.ToTrackingPose(wristPose): wristPose;

            if ((lockMode & WristLockMode.Position) != 0)
            {
                LockWristPosition(desiredWristPose.position, overrideFactor, skipAnimation);
            }

            if ((lockMode & WristLockMode.Rotation) != 0)
            {
                LockWristRotation(desiredWristPose.rotation, overrideFactor, skipAnimation);
            }
        }

        public void LockWristPosition(Vector3 position, float overrideFactor = 1f, bool skipAnimation = false)
        {
            _wristPositionOverrideFactor = overrideFactor;
            _desiredWristPose.position = position;
            if (!_wristPositionLocked)
            {
                _wristPositionLocked = true;
                SyntheticWristLockChangedState(WristLockMode.Position, skipAnimation);
            }
        }

        public void LockWristRotation(Quaternion rotation, float overrideFactor = 1f, bool skipAnimation = false)
        {
            _wristRotationOverrideFactor = overrideFactor;
            _desiredWristPose.rotation = rotation;
            if (!_wristRotationLocked)
            {
                _wristRotationLocked = true;
                SyntheticWristLockChangedState(WristLockMode.Rotation, skipAnimation);
            }
        }

        /// <summary>
        /// Unlocks the hand (locked at the OverrideWristPose method) starting
        /// a timer for the smooth release animation.
        /// </summary>
        public void FreeWrist(WristLockMode lockMode = WristLockMode.Full)
        {
            if ((lockMode & WristLockMode.Position) != 0
                && _wristPositionLocked)
            {
                _wristPositionOverrideFactor = 0f;
                _wristPositionLocked = false;
                SyntheticWristLockChangedState(WristLockMode.Position);
            }
            if ((lockMode & WristLockMode.Rotation) != 0
                && _wristRotationLocked)
            {
                _wristRotationOverrideFactor = 0f;
                _wristRotationLocked = false;
                SyntheticWristLockChangedState(WristLockMode.Rotation);
            }
        }

        private void SyntheticWristLockChangedState(WristLockMode lockMode, bool skipAnimation = false)
        {
            if ((lockMode & WristLockMode.Position) != 0)
            {
                UpdateProgressCurve(ref _wristPositionLockCurve, ref _wristPositionUnlockCurve,
                    _wristPositionLocked, skipAnimation);
                _constrainedWristPose.position = _lastWristPose.position;
            }

            if ((lockMode & WristLockMode.Rotation) != 0)
            {
                UpdateProgressCurve(ref _wristRotationLockCurve, ref _wristRotationUnlockCurve,
                    _wristRotationLocked, skipAnimation);
                _constrainedWristPose.rotation = _lastWristPose.rotation;
            }
        }

        /// <summary>
        /// Indicates whether a joint's tracked rotation is past a given rotation.
        /// Works in local Unity Joint coordinates.
        /// This is useful for blocking fingers past the snapping point.
        /// </summary>
        /// <param name="desiredLocalRot">The known local rotation of the joint. </param>
        /// <param name="maxLocalRot">The desired max local rotation of the joint.</param>
        /// <returns>A negative scalar proportional to how much the rotation is over the max one, a proportional positive scalar if under.</returns>
        private static float OverFlex(in Quaternion desiredLocalRot, in Quaternion maxLocalRot)
        {
            Vector3 jointDir = desiredLocalRot * Vector3.right;
            Vector3 jointTan = desiredLocalRot * Vector3.back;
            Vector3 maxDir = maxLocalRot * Vector3.right;
            Vector3 difference = Vector3.Cross(jointDir, maxDir);
            return Vector3.Dot(jointTan, difference);
        }

        private static void UpdateProgressCurve(ref ProgressCurve lockProgress, ref ProgressCurve unlockProgress, bool locked, bool skipAnimation)
        {
            ProgressCurve progress = locked ? lockProgress : unlockProgress;
            if (skipAnimation)
            {
                progress.End();
            }
            else
            {
                progress.Start();
            }
        }

        #region Inject

        public void InjectAllSyntheticHandModifier(UpdateModeFlags updateMode, IDataSource updateAfter,
            DataModifier<HandDataAsset> modifyDataFromSource, bool applyModifier,
            ProgressCurve wristPositionLockCurve, ProgressCurve wristPositionUnlockCurve,
            ProgressCurve wristRotationLockCurve, ProgressCurve wristRotationUnlockCurve,
            ProgressCurve jointLockCurve, ProgressCurve jointUnlockCurve,
            float spreadAllowance)
        {
            base.InjectAllHand(updateMode, updateAfter, modifyDataFromSource, applyModifier);

            InjectWristPositionLockCurve(wristPositionLockCurve);
            InjectWristPositionUnlockCurve(wristPositionUnlockCurve);
            InjectWristRotationLockCurve(wristRotationLockCurve);
            InjectWristRotationUnlockCurve(wristRotationUnlockCurve);
            InjectJointLockCurve(jointLockCurve);
            InjectJointUnlockCurve(jointUnlockCurve);
            InjectSpreadAllowance(spreadAllowance);
        }

        public void InjectWristPositionLockCurve(ProgressCurve wristPositionLockCurve) {
            _wristPositionLockCurve = wristPositionLockCurve;
        }

        public void InjectWristPositionUnlockCurve(ProgressCurve wristPositionUnlockCurve) {
            _wristPositionUnlockCurve = wristPositionUnlockCurve;
        }

        public void InjectWristRotationLockCurve(ProgressCurve wristRotationLockCurve) {
            _wristRotationLockCurve = wristRotationLockCurve;
        }

        public void InjectWristRotationUnlockCurve(ProgressCurve wristRotationUnlockCurve) {
            _wristRotationUnlockCurve = wristRotationUnlockCurve;
        }

        public void InjectJointLockCurve(ProgressCurve jointLockCurve) {
            _jointLockCurve = jointLockCurve;
        }

        public void InjectJointUnlockCurve(ProgressCurve jointUnlockCurve) {
            _jointUnlockCurve = jointUnlockCurve;
        }

        public void InjectSpreadAllowance(float spreadAllowance) {
            _spreadAllowance = spreadAllowance;
        }

        #endregion
    }
}
