// Panel Menu|Prefabs|0130
namespace VRTK
{
    using System.Collections;
    using UnityEngine;

    /// <summary>
    /// Adds a top-level controller to handle the display of up to four child PanelMenuItemController items which are displayed as a canvas UI panel.
    /// </summary>
    /// <remarks>
    /// **Prefab Usage:**
    ///  * Place the `VRTK/Prefabs/PanelMenu/PanelMenu` prefab as a child of the `VRTK_InteractableObject` the panel menu is for.
    ///  * Optionally remove the panel control menu item child GameObjects if they are not required, e.g. `PanelTopControls`.
    ///  * Set the panel menu item controllers on the `VRTK_PanelMenuController` script to determine which panel control menu items are available.
    ///  * The available panel control menu items can be activated by pressing the corresponding direction on the touchpad.
    /// </remarks>
    /// <example>
    /// `040_Controls_Panel_Menu` contains three basic interactive object examples of the PanelMenu in use.
    /// </example>
    public class VRTK_PanelMenuController : MonoBehaviour
    {
        public enum TouchpadPressPosition
        {
            None,
            Top,
            Bottom,
            Left,
            Right
        }

        [Tooltip("The GameObject the panel should rotate towards, which is the Camera (eye) by default.")]
        public GameObject rotateTowards;
        [Tooltip("The scale multiplier, which relates to the scale of parent interactable object.")]
        public float zoomScaleMultiplier = 1f;
        [Tooltip("The top PanelMenuItemController, which is triggered by pressing up on the controller touchpad.")]
        public VRTK_PanelMenuItemController topPanelMenuItemController;
        [Tooltip("The bottom PanelMenuItemController, which is triggered by pressing down on the controller touchpad.")]
        public VRTK_PanelMenuItemController bottomPanelMenuItemController;
        [Tooltip("The left PanelMenuItemController, which is triggered by pressing left on the controller touchpad.")]
        public VRTK_PanelMenuItemController leftPanelMenuItemController;
        [Tooltip("The right PanelMenuItemController, which is triggered by pressing right on the controller touchpad.")]
        public VRTK_PanelMenuItemController rightPanelMenuItemController;

        // Relates to scale of canvas on panel items.
        protected const float CanvasScaleSize = 0.001f;

        // Swipe sensitivity / detection.
        protected const float AngleTolerance = 30f;
        protected const float SwipeMinDist = 0.2f;
        protected const float SwipeMinVelocity = 4.0f;

        protected VRTK_ControllerEvents controllerEvents;
        protected VRTK_PanelMenuItemController currentPanelMenuItemController;
        protected GameObject interactableObject;
        protected GameObject canvasObject;
        protected readonly Vector2 xAxis = new Vector2(1, 0);
        protected readonly Vector2 yAxis = new Vector2(0, 1);
        protected Vector2 touchStartPosition;
        protected Vector2 touchEndPosition;
        protected float touchStartTime;
        protected float currentAngle;
        protected bool isTrackingSwipe = false;
        protected bool isPendingSwipeCheck = false;
        protected bool isGrabbed = false;
        protected bool isShown = false;
        protected Coroutine tweenMenuScaleRoutine;

        /// <summary>
        /// The ToggleMenu method is used to show or hide the menu.
        /// </summary>
        public virtual void ToggleMenu()
        {
            if (isShown)
            {
                HideMenu(true);
            }
            else
            {
                ShowMenu();
            }
        }

        /// <summary>
        /// The ShowMenu method is used to show the menu.
        /// </summary>
        public virtual void ShowMenu()
        {
            if (!isShown)
            {
                isShown = true;
                InitTweenMenuScale(isShown);
            }
        }

        /// <summary>
        /// The HideMenu method is used to hide the menu.
        /// </summary>
        /// <param name="force">If true then the menu is always hidden.</param>
        public virtual void HideMenu(bool force)
        {
            if (isShown && force)
            {
                isShown = false;
                InitTweenMenuScale(isShown);
            }
        }

        /// <summary>
        /// The HideMenuImmediate method is used to immediately hide the menu.
        /// </summary>
        public virtual void HideMenuImmediate()
        {
            if (currentPanelMenuItemController != null && isShown)
            {
                HandlePanelMenuItemControllerVisibility(currentPanelMenuItemController);
            }
            transform.localScale = Vector3.zero;
            canvasObject.transform.localScale = Vector3.zero;
            isShown = false;
        }

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

        protected virtual void Start()
        {
            interactableObject = gameObject.transform.parent.gameObject;
            if (interactableObject == null || interactableObject.GetComponent<VRTK_InteractableObject>() == null)
            {
                VRTK_Logger.Warn(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.REQUIRED_COMPONENT_MISSING_FROM_GAMEOBJECT, "PanelMenuController", "VRTK_InteractableObject", "a parent"));
                return;
            }

            interactableObject.GetComponent<VRTK_InteractableObject>().InteractableObjectGrabbed += new InteractableObjectEventHandler(DoInteractableObjectIsGrabbed);
            interactableObject.GetComponent<VRTK_InteractableObject>().InteractableObjectUngrabbed += new InteractableObjectEventHandler(DoInteractableObjectIsUngrabbed);

            canvasObject = gameObject.transform.GetChild(0).gameObject;
            if (canvasObject == null || canvasObject.GetComponent<Canvas>() == null)
            {
                VRTK_Logger.Warn(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.REQUIRED_COMPONENT_MISSING_FROM_GAMEOBJECT, "PanelMenuController", "Canvas", "a child"));
            }
        }

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

        protected virtual void Update()
        {
            if (interactableObject != null)
            {
                if (rotateTowards == null)
                {
                    rotateTowards = VRTK_DeviceFinder.HeadsetTransform().gameObject;
                    if (rotateTowards == null)
                    {
                        VRTK_Logger.Warn(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.COULD_NOT_FIND_OBJECT_FOR_ACTION, "PanelMenuController", "an object", "rotate towards"));
                    }
                }

                if (isShown)
                {
                    if (rotateTowards != null)
                    {
                        transform.rotation = Quaternion.LookRotation((rotateTowards.transform.position - transform.position) * -1, Vector3.up);
                    }
                }

                if (isPendingSwipeCheck)
                {
                    CalculateSwipeAction();
                }
            }
        }

        protected virtual void Initialize()
        {
            if (Application.isPlaying)
            {
                if (!isShown)
                {
                    transform.localScale = Vector3.zero;
                }
            }

            if (controllerEvents == null)
            {
                transform.localPosition = new Vector3(transform.localPosition.x, transform.localPosition.y, transform.localPosition.z);
                controllerEvents = GetComponentInParent<VRTK_ControllerEvents>();
            }
        }

        protected virtual void BindControllerEvents()
        {
            controllerEvents.TouchpadPressed += new ControllerInteractionEventHandler(DoTouchpadPress);
            controllerEvents.TouchpadTouchStart += new ControllerInteractionEventHandler(DoTouchpadTouched);
            controllerEvents.TouchpadTouchEnd += new ControllerInteractionEventHandler(DoTouchpadUntouched);
            controllerEvents.TouchpadAxisChanged += new ControllerInteractionEventHandler(DoTouchpadAxisChanged);
            controllerEvents.TriggerPressed += new ControllerInteractionEventHandler(DoTriggerPressed);
        }

        protected virtual void UnbindControllerEvents()
        {
            controllerEvents.TouchpadPressed -= new ControllerInteractionEventHandler(DoTouchpadPress);
            controllerEvents.TouchpadTouchStart -= new ControllerInteractionEventHandler(DoTouchpadTouched);
            controllerEvents.TouchpadTouchEnd -= new ControllerInteractionEventHandler(DoTouchpadUntouched);
            controllerEvents.TouchpadAxisChanged -= new ControllerInteractionEventHandler(DoTouchpadAxisChanged);
            controllerEvents.TriggerPressed -= new ControllerInteractionEventHandler(DoTriggerPressed);
        }

        protected virtual void HandlePanelMenuItemControllerVisibility(VRTK_PanelMenuItemController targetPanelItemController)
        {
            if (isShown)
            {
                if (currentPanelMenuItemController == targetPanelItemController)
                {
                    targetPanelItemController.Hide(interactableObject);
                    currentPanelMenuItemController = null;
                    HideMenu(true);
                }
                else
                {
                    currentPanelMenuItemController.Hide(interactableObject);
                    currentPanelMenuItemController = targetPanelItemController;
                }
            }
            else
            {
                currentPanelMenuItemController = targetPanelItemController;
            }

            if (currentPanelMenuItemController != null)
            {
                currentPanelMenuItemController.Show(interactableObject);
                ShowMenu();
            }
        }

        protected virtual void InitTweenMenuScale(bool show)
        {
            if (tweenMenuScaleRoutine != null)
            {
                StopCoroutine(tweenMenuScaleRoutine);
            }
            if (enabled)
            {
                tweenMenuScaleRoutine = StartCoroutine(TweenMenuScale(show));
            }
        }

        protected virtual IEnumerator TweenMenuScale(bool show)
        {
            float targetScale = 0;
            Vector3 direction = -1 * Vector3.one;
            if (show)
            {
                canvasObject.transform.localScale = new Vector3(CanvasScaleSize, CanvasScaleSize, CanvasScaleSize);
                targetScale = zoomScaleMultiplier;
                direction = Vector3.one;
            }
            int i = 0;
            while (i < 250 && ((show && transform.localScale.x < targetScale) || (!show && transform.localScale.x > targetScale)))
            {
                transform.localScale += direction * Time.deltaTime * 4f * zoomScaleMultiplier;
                yield return true;
                i++;
            }
            transform.localScale = direction * targetScale;

            if (!show)
            {
                canvasObject.transform.localScale = Vector3.zero;
            }
        }

        protected virtual void DoInteractableObjectIsGrabbed(object sender, InteractableObjectEventArgs e)
        {
            controllerEvents = e.interactingObject.GetComponentInParent<VRTK_ControllerEvents>();
            if (controllerEvents != null)
            {
                BindControllerEvents();
            }
            isGrabbed = true;
        }

        protected virtual void DoInteractableObjectIsUngrabbed(object sender, InteractableObjectEventArgs e)
        {
            isGrabbed = false;
            if (isShown)
            {
                HideMenuImmediate();
            }

            if (controllerEvents != null)
            {
                UnbindControllerEvents();
                controllerEvents = null;
            }
        }

        protected virtual void DoTouchpadPress(object sender, ControllerInteractionEventArgs e)
        {
            if (isGrabbed)
            {
                TouchpadPressPosition pressPosition = CalculateTouchpadPressPosition();
                switch (pressPosition)
                {
                    case TouchpadPressPosition.Top:
                        if (topPanelMenuItemController != null)
                        {
                            HandlePanelMenuItemControllerVisibility(topPanelMenuItemController);
                        }
                        break;

                    case TouchpadPressPosition.Bottom:
                        if (bottomPanelMenuItemController != null)
                        {
                            HandlePanelMenuItemControllerVisibility(bottomPanelMenuItemController);
                        }
                        break;

                    case TouchpadPressPosition.Left:
                        if (leftPanelMenuItemController != null)
                        {
                            HandlePanelMenuItemControllerVisibility(leftPanelMenuItemController);
                        }
                        break;

                    case TouchpadPressPosition.Right:
                        if (rightPanelMenuItemController != null)
                        {
                            HandlePanelMenuItemControllerVisibility(rightPanelMenuItemController);
                        }
                        break;
                }
            }
        }

        protected virtual void DoTouchpadTouched(object sender, ControllerInteractionEventArgs e)
        {
            touchStartPosition = new Vector2(e.touchpadAxis.x, e.touchpadAxis.y);
            touchStartTime = Time.time;
            isTrackingSwipe = true;
        }

        protected virtual void DoTouchpadUntouched(object sender, ControllerInteractionEventArgs e)
        {
            isTrackingSwipe = false;
            isPendingSwipeCheck = true;
        }

        protected virtual void DoTouchpadAxisChanged(object sender, ControllerInteractionEventArgs e)
        {
            ChangeAngle(CalculateAngle(e));

            if (isTrackingSwipe)
            {
                touchEndPosition = new Vector2(e.touchpadAxis.x, e.touchpadAxis.y);
            }
        }

        protected virtual void DoTriggerPressed(object sender, ControllerInteractionEventArgs e)
        {
            if (isGrabbed)
            {
                OnTriggerPressed();
            }
        }

        protected virtual void ChangeAngle(float angle, object sender = null)
        {
            currentAngle = angle;
        }

        protected virtual void CalculateSwipeAction()
        {
            isPendingSwipeCheck = false;

            float deltaTime = Time.time - touchStartTime;
            Vector2 swipeVector = touchEndPosition - touchStartPosition;
            float velocity = swipeVector.magnitude / deltaTime;

            if ((velocity > SwipeMinVelocity) && (swipeVector.magnitude > SwipeMinDist))
            {
                swipeVector.Normalize();
                float angleOfSwipe = Vector2.Dot(swipeVector, xAxis);
                angleOfSwipe = Mathf.Acos(angleOfSwipe) * Mathf.Rad2Deg;

                // Left / right
                if (angleOfSwipe < AngleTolerance)
                {
                    OnSwipeRight();
                }
                else if ((180.0f - angleOfSwipe) < AngleTolerance)
                {
                    OnSwipeLeft();
                }
                else
                {
                    // Top / bottom
                    angleOfSwipe = Vector2.Dot(swipeVector, yAxis);
                    angleOfSwipe = Mathf.Acos(angleOfSwipe) * Mathf.Rad2Deg;
                    if (angleOfSwipe < AngleTolerance)
                    {
                        OnSwipeTop();
                    }
                    else if ((180.0f - angleOfSwipe) < AngleTolerance)
                    {
                        OnSwipeBottom();
                    }
                }
            }
        }

        protected virtual TouchpadPressPosition CalculateTouchpadPressPosition()
        {
            if (CheckAnglePosition(currentAngle, AngleTolerance, 0))
            {
                return TouchpadPressPosition.Top;
            }
            else if (CheckAnglePosition(currentAngle, AngleTolerance, 180))
            {
                return TouchpadPressPosition.Bottom;
            }
            else if (CheckAnglePosition(currentAngle, AngleTolerance, 270))
            {
                return TouchpadPressPosition.Left;
            }
            else if (CheckAnglePosition(currentAngle, AngleTolerance, 90))
            {
                return TouchpadPressPosition.Right;
            }

            return TouchpadPressPosition.None;
        }

        protected virtual void OnSwipeLeft()
        {
            if (currentPanelMenuItemController != null)
            {
                currentPanelMenuItemController.SwipeLeft(interactableObject);
            }
        }

        protected virtual void OnSwipeRight()
        {
            if (currentPanelMenuItemController != null)
            {
                currentPanelMenuItemController.SwipeRight(interactableObject);
            }
        }

        protected virtual void OnSwipeTop()
        {
            if (currentPanelMenuItemController != null)
            {
                currentPanelMenuItemController.SwipeTop(interactableObject);
            }
        }

        protected virtual void OnSwipeBottom()
        {
            if (currentPanelMenuItemController != null)
            {
                currentPanelMenuItemController.SwipeBottom(interactableObject);
            }
        }

        protected virtual void OnTriggerPressed()
        {
            if (currentPanelMenuItemController != null)
            {
                currentPanelMenuItemController.TriggerPressed(interactableObject);
            }
        }

        protected virtual float CalculateAngle(ControllerInteractionEventArgs e)
        {
            return e.touchpadAngle;
        }

        protected virtual float NormAngle(float currentDegree, float maxAngle = 360)
        {
            if (currentDegree < 0) currentDegree = currentDegree + maxAngle;
            return currentDegree % maxAngle;
        }

        protected virtual bool CheckAnglePosition(float currentDegree, float tolerance, float targetDegree)
        {
            float lowerBound = NormAngle(currentDegree - tolerance);
            float upperBound = NormAngle(currentDegree + tolerance);

            if (lowerBound > upperBound)
            {
                return targetDegree >= lowerBound || targetDegree <= upperBound;
            }
            return targetDegree >= lowerBound && targetDegree <= upperBound;
        }
    }
}
