// Base Object Control Action|ObjectControlActions|25000
namespace VRTK
{
    using UnityEngine;

    /// <summary>
    /// Provides a base that all object control actions can inherit from.
    /// </summary>
    /// <remarks>
    /// **Script Usage:**
    ///   > This is an abstract class that is to be inherited to a concrete class that provides object control action functionality, therefore this script should not be directly used.
    /// </remarks>
    public abstract class VRTK_BaseObjectControlAction : MonoBehaviour
    {
        /// <summary>
        /// The axis to listen to changes on.
        /// </summary>
        public enum AxisListeners
        {
            /// <summary>
            /// Listen for changes on the horizontal X axis.
            /// </summary>
            XAxisChanged,
            /// <summary>
            /// Listen for changes on the vertical y axis.
            /// </summary>
            YAxisChanged
        }

        [Tooltip("The Object Control script to receive axis change events from.")]
        public VRTK_ObjectControl objectControlScript;
        [Tooltip("Determines which Object Control Axis event to listen to.")]
        public AxisListeners listenOnAxisChange;

        protected Collider centerCollider;
        protected Vector3 colliderCenter = Vector3.zero;
        protected float colliderRadius = 0f;
        protected float colliderHeight = 0f;
        protected Transform controlledTransform;
        protected Transform playArea;
        protected VRTK_BodyPhysics internalBodyPhysics;
        
        protected Vector3 playerHeadPositionBeforeRotation;
        protected Transform headsetTransform;
        protected bool validPlayerObject;

        protected abstract void Process(GameObject controlledGameObject, Transform directionDevice, Vector3 axisDirection, float axis, float deadzone, bool currentlyFalling, bool modifierActive);

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

        protected virtual void OnEnable()
        {
            playArea = VRTK_DeviceFinder.PlayAreaTransform();
            if (objectControlScript)
            {
                switch (listenOnAxisChange)
                {
                    case AxisListeners.XAxisChanged:
                        objectControlScript.XAxisChanged += AxisChanged;
                        break;
                    case AxisListeners.YAxisChanged:
                        objectControlScript.YAxisChanged += AxisChanged;
                        break;
                }
            }
            internalBodyPhysics = (internalBodyPhysics == null ? VRTK_SharedMethods.FindEvenInactiveComponent<VRTK_BodyPhysics>(true) : internalBodyPhysics);
        }

        protected virtual void OnDisable()
        {
            if (objectControlScript)
            {
                switch (listenOnAxisChange)
                {
                    case AxisListeners.XAxisChanged:
                        objectControlScript.XAxisChanged -= AxisChanged;
                        break;
                    case AxisListeners.YAxisChanged:
                        objectControlScript.YAxisChanged -= AxisChanged;
                        break;
                }
            }
        }

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

        protected virtual void AxisChanged(object sender, ObjectControlEventArgs e)
        {
            Process(e.controlledGameObject, e.directionDevice, e.axisDirection, e.axis, e.deadzone, e.currentlyFalling, e.modifierActive);
        }

        protected virtual void RotateAroundPlayer(GameObject controlledGameObject, float angle)
        {
            Vector3 objectCenter = GetObjectCenter(controlledGameObject.transform);
            Vector3 objectPosition = controlledGameObject.transform.TransformPoint(objectCenter);
            controlledGameObject.transform.Rotate(Vector3.up, angle);
            objectPosition -= controlledGameObject.transform.TransformPoint(objectCenter);
            controlledGameObject.transform.position += objectPosition;
        }

        protected virtual void Blink(float blinkSpeed)
        {
            if (blinkSpeed > 0f)
            {
                VRTK_SDK_Bridge.HeadsetFade(Color.black, 0);
                ReleaseBlink(blinkSpeed);
            }
        }

        protected virtual void ReleaseBlink(float blinkSpeed)
        {
            VRTK_SDK_Bridge.HeadsetFade(Color.clear, blinkSpeed);
        }

        protected virtual Vector3 GetObjectCenter(Transform checkObject)
        {
            if (centerCollider == null || checkObject != controlledTransform)
            {
                controlledTransform = checkObject;

                if (checkObject == playArea)
                {
                    bool centerColliderSet = false;

                    if (internalBodyPhysics != null && internalBodyPhysics.GetBodyColliderContainer() != null)
                    {
                        CapsuleCollider playAreaCollider = internalBodyPhysics.GetBodyColliderContainer().GetComponent<CapsuleCollider>();
                        centerCollider = playAreaCollider;
                        if (playAreaCollider != null)
                        {
                            centerColliderSet = true;
                            colliderRadius = playAreaCollider.radius;
                            colliderHeight = playAreaCollider.height;
                            colliderCenter = playAreaCollider.center;
                        }
                    }

                    if (!centerColliderSet)
                    {
                        VRTK_Logger.Error(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.REQUIRED_COMPONENT_MISSING_FROM_GAMEOBJECT, "PlayArea", "CapsuleCollider", "the same or child"));
                    }
                }
                else
                {
                    centerCollider = checkObject.GetComponentInChildren<Collider>();
                    if (centerCollider == null)
                    {
                        VRTK_Logger.Error(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.REQUIRED_COMPONENT_MISSING_FROM_GAMEOBJECT, "CheckObject", "Collider", "the same or child"));
                    }
                    colliderRadius = 0.1f;
                    colliderHeight = 0.1f;
                }
            }

            return colliderCenter;
        }

        protected virtual int GetAxisDirection(float axis)
        {
            int axisDirection = 0;
            if (axis < 0)
            {
                axisDirection = -1;
            }
            else if (axis > 0)
            {
                axisDirection = 1;
            }

            return axisDirection;
        }

        protected virtual bool CanMove(VRTK_BodyPhysics givenBodyPhysics, Vector3 currentPosition, Vector3 proposedPosition)
        {
            if (givenBodyPhysics == null)
            {
                return true;
            }

            Vector3 proposedDirection = (proposedPosition - currentPosition).normalized;
            float distance = Vector3.Distance(currentPosition, proposedPosition);
            return !givenBodyPhysics.SweepCollision(proposedDirection, distance);
        }
        
        /// <summary>
        /// Since rotation scripts may rotate the game object '[CameraRig]' in order to rotate the player and the player's head does not always have the local position (0,0,0), the rotation will result in a position offset of player's head. The game object '[CameraRig]' will moved relativly to compensate that. Therefore it will save the player's head position in this method. 
        /// Call 'CheckForPlayerAfterRotation()' to correct the player's head position offset after rotation.
        /// </summary>
        /// <param name="controlledGameObject"></param>
        protected virtual void CheckForPlayerBeforeRotation(GameObject controlledGameObject)
        {
            VRTK_PlayerObject playerObject = controlledGameObject.GetComponent<VRTK_PlayerObject>();
            if (headsetTransform == null)
            {
                headsetTransform = VRTK_DeviceFinder.HeadsetTransform();
            }
            validPlayerObject = (playerObject != null && playerObject.objectType == VRTK_PlayerObject.ObjectTypes.CameraRig && headsetTransform != null);
            if (validPlayerObject)
            {
                //Save the player's head position for use in method 'CheckForPlayerAfterRotation'.
                playerHeadPositionBeforeRotation = headsetTransform.position;
            }
        }

        /// <summary>
        /// Corrects the player's head position offset after rotation. Call 'CheckForPlayerBeforeRotation' before execute rotation.
        /// </summary>
        /// <param name="controlledGameObject"></param>
        protected virtual void CheckForPlayerAfterRotation(GameObject controlledGameObject)
        {
            //If necessary the player's head position will be corrected by translate the Gameobject [CameraRig] relativly.
            if (validPlayerObject)
            {
                controlledGameObject.transform.position += playerHeadPositionBeforeRotation - headsetTransform.position;

                //Prevents multiple calls of this method without call of 'CheckForPlayerBeforeRotation'.
                validPlayerObject = false;
            }
        }
    }
}