// Step Multiplier|Locomotion|20130
namespace VRTK
{
    using UnityEngine;

    /// <summary>
    /// Multiplies each real world step within the play area to enable further distances to be travelled in the virtual world.
    /// </summary>
    /// <remarks>
    /// **Optional Components:**
    ///  * `VRTK_ControllerEvents` - The events component to listen for the button presses on. This must be applied on the same GameObject as this script if one is not provided via the `Controller Events` parameter.
    ///
    /// **Script Usage:**
    ///  * Place the `VRTK_StepMultiplier` script on either:
    ///    * Any GameObject in the scene if no activation button is required.
    ///    * The GameObject with the Controller Events scripts if an activation button is required.
    ///    * Any other scene GameObject and provide a valid `VRTK_ControllerEvents` component to the `Controller Events` parameter of this script if an activation button is required.
    /// </remarks>
    /// <example>
    /// `VRTK/Examples/028_CameraRig_RoomExtender` shows how the Step Multiplier can be used to move around the scene with multiplied steps.
    /// </example>
    [AddComponentMenu("VRTK/Scripts/Locomotion/VRTK_StepMultiplier")]
    public class VRTK_StepMultiplier : MonoBehaviour
    {
        /// <summary>
        /// Movement methods.
        /// </summary>
        public enum MovementFunction
        {
            /// <summary>
            /// Moves the head with a non-linear drift movement.
            /// </summary>
            Nonlinear,
            /// <summary>
            /// Moves the headset in a direct linear movement.
            /// </summary>
            LinearDirect
        }

        [Header("Step Multiplier Settings")]

        [Tooltip("The controller button to activate the step multiplier effect. If it is `Undefined` then the step multiplier will always be active.")]
        public VRTK_ControllerEvents.ButtonAlias activationButton = VRTK_ControllerEvents.ButtonAlias.Undefined;
        [Tooltip("This determines the type of movement used by the extender.")]
        public MovementFunction movementFunction = MovementFunction.LinearDirect;
        [Tooltip("This is the factor by which movement at the edge of the circle is amplified. `0` is no movement of the play area. Higher values simulate a bigger play area but may be too uncomfortable.")]
        [Range(0, 10)]
        public float additionalMovementMultiplier = 1.0f;
        [Tooltip("This is the size of the circle in which the play area is not moved and everything is normal. If it is to low it becomes uncomfortable when crouching.")]
        [Range(0, 5)]
        public float headZoneRadius = 0.25f;

        [Header("Custom Settings")]

        [Tooltip("The Controller Events to listen for the events on. If the script is being applied onto a controller then this parameter can be left blank as it will be auto populated by the controller the script is on at runtime.")]
        public VRTK_ControllerEvents controllerEvents;

        protected Vector3 relativeMovementOfCameraRig = new Vector3();
        protected Transform movementTransform;
        protected Transform playArea;
        protected Vector3 headCirclePosition;
        protected Vector3 lastPosition;
        protected Vector3 lastMovement;
        protected bool activationEnabled;
        protected VRTK_ControllerEvents.ButtonAlias subscribedActivationButton = VRTK_ControllerEvents.ButtonAlias.Undefined;
        protected bool buttonSubscribed;

        protected virtual void Awake()
        {
            VRTK_SDKManager.AttemptAddBehaviourToToggleOnLoadedSetupChange(this);
        }

        protected virtual void OnEnable()
        {
            activationEnabled = false;
            buttonSubscribed = false;
            movementTransform = VRTK_DeviceFinder.HeadsetTransform();
            playArea = VRTK_DeviceFinder.PlayAreaTransform();
            MoveHeadCircleNonLinearDrift();
            if (movementTransform != null)
            {
                lastPosition = movementTransform.localPosition;
            }
        }

        protected virtual void OnDestroy()
        {
            VRTK_SDKManager.AttemptRemoveBehaviourToToggleOnLoadedSetupChange(this);
        }

        protected virtual void Update()
        {
            ManageButtonSubscription();
            switch (movementFunction)
            {
                case MovementFunction.Nonlinear:
                    MoveHeadCircleNonLinearDrift();
                    break;
                case MovementFunction.LinearDirect:
                    MoveHeadCircle();
                    break;
                default:
                    break;
            }
        }

        protected virtual void ManageButtonSubscription()
        {
            controllerEvents = (controllerEvents != null ? controllerEvents : GetComponentInParent<VRTK_ControllerEvents>());

            if (controllerEvents != null && buttonSubscribed && subscribedActivationButton != VRTK_ControllerEvents.ButtonAlias.Undefined && activationButton != subscribedActivationButton)
            {
                buttonSubscribed = false;
                controllerEvents.UnsubscribeToButtonAliasEvent(subscribedActivationButton, true, ActivationButtonPressed);
                controllerEvents.UnsubscribeToButtonAliasEvent(subscribedActivationButton, false, ActivationButtonReleased);
                subscribedActivationButton = VRTK_ControllerEvents.ButtonAlias.Undefined;
            }

            if (controllerEvents != null && !buttonSubscribed && activationButton != VRTK_ControllerEvents.ButtonAlias.Undefined)
            {
                controllerEvents.SubscribeToButtonAliasEvent(activationButton, true, ActivationButtonPressed);
                controllerEvents.SubscribeToButtonAliasEvent(activationButton, false, ActivationButtonReleased);
                buttonSubscribed = true;
                subscribedActivationButton = activationButton;
            }
        }

        protected virtual void ActivationButtonPressed(object sender, ControllerInteractionEventArgs e)
        {
            activationEnabled = true;
        }

        protected virtual void ActivationButtonReleased(object sender, ControllerInteractionEventArgs e)
        {
            activationEnabled = false;
        }

        protected virtual void Move(Vector3 movement)
        {
            headCirclePosition += movement;
            if (playArea != null && (activationEnabled || activationButton == VRTK_ControllerEvents.ButtonAlias.Undefined))
            {
                playArea.localPosition += movement * additionalMovementMultiplier;
                relativeMovementOfCameraRig += movement * additionalMovementMultiplier;
            }
        }

        protected virtual void MoveHeadCircle()
        {
            if (movementTransform != null)
            {
                //Get the movement of the head relative to the headCircle.
                Vector3 circleCenterToHead = new Vector3(movementTransform.localPosition.x - headCirclePosition.x, 0, movementTransform.localPosition.z - headCirclePosition.z);

                //Get the direction of the head movement.
                UpdateLastMovement();

                //Checks if the head is outside of the head cirlce and moves the head circle and play area in the movementDirection.
                if (circleCenterToHead.sqrMagnitude > headZoneRadius * headZoneRadius && lastMovement != Vector3.zero)
                {
                    //Just move like the headset moved
                    Move(lastMovement);
                }
            }
        }

        protected virtual void MoveHeadCircleNonLinearDrift()
        {
            if (movementTransform != null)
            {
                Vector3 movement = new Vector3(movementTransform.localPosition.x - headCirclePosition.x, 0, movementTransform.localPosition.z - headCirclePosition.z);
                if (movement.sqrMagnitude > headZoneRadius * headZoneRadius)
                {
                    Vector3 deltaMovement = movement.normalized * (movement.magnitude - headZoneRadius);
                    Move(deltaMovement);
                }
            }
        }

        protected virtual void UpdateLastMovement()
        {
            if (movementTransform != null)
            {
                //Save the last movement
                lastMovement = movementTransform.localPosition - lastPosition;
                lastMovement.y = 0;
                lastPosition = movementTransform.localPosition;
            }
        }
    }
}