// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Serialization;

namespace Microsoft.MixedReality.Toolkit.UI.HandCoach
{
    /// <summary>
    /// This class provides wrapper functionality for triggering animations and fades for the hand rig.
    /// </summary>
    public class HandInteractionHint : MonoBehaviour
    {
        public GameObject VisualsRoot { get; set; }

        [Tooltip("React to hand tracking state to hide visuals when hands are being tracked. If false, only the customShouldHideHands function will be evaluated.")]
        [SerializeField]
        private bool hideIfHandTracked = false;

        /// <summary>
        /// React to hand tracking state to hide visuals when hands are being tracked.
        /// If false, only the customShouldHideHands function will be evaluated.
        /// </summary>
        public bool HideIfHandTracked
        {
            get
            {
                return hideIfHandTracked;
            }
            set
            {
                hideIfHandTracked = value;
            }
        }

        [Tooltip("When the user's hands are not in view, the visuals will appear after HintDisplayDelay seconds.")]
        [SerializeField]
        [FormerlySerializedAs("minDelay")]
        private float hintDisplayDelay = 5f;

        /// <summary>
        /// When the user's hands are not in view, the visuals will appear after HintDisplayDelay seconds.
        /// </summary>
        public float HintDisplayDelay
        {
            get
            {
                return hintDisplayDelay;
            }
            set
            {
                hintDisplayDelay = value;
            }
        }

        [Tooltip("When the user's hands are in view, the visuals will appear after TrackedHandHintDisplayDelay seconds.")]
        [SerializeField]
        [FormerlySerializedAs("maxDelay")]
        private float trackedHandHintDisplayDelay = 10f;

        /// <summary>
        /// When the user's hands are in view, the visuals will appear after TrackedHandHintDisplayDelay seconds."
        /// </summary>
        public float TrackedHandHintDisplayDelay
        {
            get
            {
                return trackedHandHintDisplayDelay;
            }
            set
            {
                trackedHandHintDisplayDelay = value;
            }
        }

        [Tooltip("Number of times to repeat the hint before fading out and waiting for timer again.")]
        [SerializeField]
        private int repeats = 2;

        /// <summary>
        /// Number of times to repeat the hint before fading out and waiting for timer again.
        /// </summary>
        public int Repeats
        {
            get
            {
                return repeats;
            }
            set
            {
                repeats = value;
            }
        }

        [Tooltip("If true, logic runs whenever this component is active.  If false, you must manually start the logic with StartShowTimer.")]
        [SerializeField]
        private bool autoActivate = true;

        /// <summary>
        /// If true, logic runs whenever this component is active.  If false, you must manually start the logic with StartShowTimer.
        /// </summary>
        public bool AutoActivate
        {
            get
            {
                return autoActivate;
            }
            set
            {
                autoActivate = value;
            }
        }

        [Tooltip("Name of animation to play during loop.")]
        [SerializeField]
        private string animationState = "";

        /// <summary>
        /// Name of animation to play during loop.
        /// </summary>
        public string AnimationState
        {
            get
            {
                return animationState;
            }
            set
            {
                animationState = value;
            }
        }

        [Tooltip("Time to wait between repeats in seconds.")]
        [SerializeField]
        private float repeatDelay = 1f;

        /// <summary>
        /// Time to wait between repeats in seconds.
        /// </summary>
        public float RepeatDelay
        {
            get
            {
                return repeatDelay;
            }
            set
            {
                repeatDelay = value;
            }
        }

        private string fadeInAnimationState = "Fade_In";

        private string fadeOutAnimationState = "Fade_Out";

        private float animationHideTime = 3f;

        private float animationHideDuration = 0.5f;

        /// <summary>
        /// Custom function to determine visibility of visuals.
        /// Return true to hide visuals and reset min timer (max timer will still be in effect), return false when user is doing nothing and needs a hint.
        /// </summary>
        public Func<bool> CustomShouldHideVisuals = delegate { return false; };

        private Animator animator;

        private bool animatingOut = false;

        private bool loopRunning = false;

        private void Awake()
        {
            if (VisualsRoot == null)
            {
                if (transform.childCount > 0)
                {
                    VisualsRoot = transform.GetChild(0).gameObject;
                }
                else
                {
                    Debug.LogError("Incorrect hand rig setup. Disabling gameObject");
                    gameObject.SetActive(false);
                }
            }

            // store the root's animator
            animator = VisualsRoot.GetComponent<Animator>();

            if (animator == null)
            {
                Debug.LogError("Hand rig does not have an animator. Disabling gameObject");
                gameObject.SetActive(false);
            }

            // hide visuals by default
            if (VisualsRoot != null)
            {
                VisualsRoot.SetActive(false);
            }
        }

        private void OnEnable()
        {
            // When component is enabled, start up the timer logic if auto activate is specified
            if (AutoActivate)
            {
                StartHintLoop();
            }
        }

        private void OnDisable()
        {
            // Stop all logic when the component is disabled, even if not using auto activate
            if (loopRunning)
            {
                StopAllCoroutines();
                loopRunning = false;
            }

            // Also turn off the visuals
            SetActive(VisualsRoot, false);
        }

        /// <summary>
        /// Starts the hint loop logic.
        /// </summary>
        public void StartHintLoop()
        {
            if (!loopRunning && VisualsRoot != null)
            {
                loopRunning = true;
                animationHideDuration = GetAnimationDuration(fadeOutAnimationState);
                animationHideTime = GetAnimationDuration(AnimationState) - animationHideDuration;
                if (animationHideTime < 0)
                {
                    animationHideTime = 0;
                }
                StartCoroutine(HintLoopSequence(AnimationState));
            }
        }

        /// <summary>
        /// Fades out the hint and stops the hint loop logic
        /// </summary>
        public void StopHintLoop()
        {
            if (loopRunning && !animatingOut)
            {
                StopAllCoroutines();
                StartCoroutine(FadeOutHint());
            }
            loopRunning = false;
        }

        /// <summary>
        /// Stops the hint with appropriate fade.
        /// </summary>
        private IEnumerator FadeOutHint()
        {
            animatingOut = true;
            if (animationHideDuration > 0)
            {
                // Tell the animator to play the animation
                if (animator != null)
                {
                    animator.Play(fadeOutAnimationState);
                }

                yield return new WaitForSeconds(animationHideDuration);
            }
            SetActive(VisualsRoot, false);
            animatingOut = false;
        }

        /// <summary>
        /// The main timer logic coroutine. Pass the min/max delay to use and the function to evaluate the desired state.
        /// </summary>
        private IEnumerator HintLoopSequence(string stateToPlay)
        {
            // loop until the gameObject has been turned off
            while (VisualsRoot != null && loopRunning)
            {
                // First wait for the min timer, resetting it whenever ShouldHide is true.  Also
                // wait for the max timer, never resetting it.
                float timer = 0;
                float displayDelay = IsHandTracked() ? TrackedHandHintDisplayDelay : HintDisplayDelay;
                while (timer < displayDelay)
                {
                    if (ShouldHideVisuals())
                    {
                        timer = 0;
                    }
                    else
                    {
                        timer += Time.deltaTime;
                    }
                    displayDelay = IsHandTracked() ? TrackedHandHintDisplayDelay : HintDisplayDelay;

                    yield return null;
                }

                // show the root
                SetActive(VisualsRoot, true);
                if (animator != null)
                {
                    animator.Play(stateToPlay);
                }

                float visibleTime = Time.time;
                int playCount = 0;

                // loop as long as visuals are active and we haven't repeated the number of times desired
                while (VisualsRoot.activeSelf && playCount < Repeats)
                {
                    // hide if hand is present and HideIfHandTracked is true
                    bool shouldHide = ShouldHideVisuals();
                    bool fadeOut = Time.time - visibleTime >= animationHideTime;
                    if (shouldHide || fadeOut)
                    {
                        // Yield while deactivate anim is playing (or instant deactivate if not animating)
                        yield return FadeOutHint();

                        // if fade out was caused by user interacting, we've reached the repeat limit, or we've stopped the loop, break out
                        if (shouldHide || playCount == Repeats - 1 || !loopRunning)
                        {
                            break;
                        }
                        // If we auto-hid, then reappear if hands are not tracked
                        else
                        {
                            yield return new WaitForSeconds(RepeatDelay);
                            SetActive(VisualsRoot, true);
                            if (animator != null)
                            {
                                animator.Play(stateToPlay);
                            }
                            visibleTime = Time.time;
                            playCount++;
                        }
                    }
                    yield return null;
                }
            }
        }

        private void SetActive(GameObject root, bool show)
        {
            if (root != null)
            {
                root.SetActive(show);

                if (show && animator != null)
                {
                    animator.Play(fadeInAnimationState);
                }
            }
        }

        /// <summary>
        /// Gets the duration of the animation name passed in, or 0 if the state name is not found.
        /// </summary>
        public float GetAnimationDuration(string animationStateName)
        {
            if (animator != null)
            {
                RuntimeAnimatorController ac = animator.runtimeAnimatorController;
                for (int i = 0; i < ac.animationClips.Length; i++)
                {
                    if (ac.animationClips[i].name.StartsWith(animationStateName))
                    {
                        return ac.animationClips[i].length;
                    }
                }
            }

            // the specified state is not found
            return 0;
        }

        /// <summary>
        /// Check if the user is making an attempt to proceed, according to the hint.
        /// Return true if the user is attempting to interact properly.  Visuals will hide until the max timer expires.
        /// Return false if the user is doing nothing.  Visuals will show according to the min timer.
        /// </summary>
        private bool ShouldHideVisuals()
        {
            // Check hand tracking, if configured to do so
            bool shouldHide = HideIfHandTracked && IsHandTracked();

            // Check the custom show visuals function
            if (!shouldHide)
            {
                shouldHide |= CustomShouldHideVisuals();
            }
            return shouldHide;
        }

        /// <summary>
        /// Return true if either of the user's hands are being tracked.
        /// Return false if neither of the user's hands are being tracked.
        /// </summary>
        private bool IsHandTracked()
        {
            return HandJointUtils.FindHand(Handedness.Right) != null || HandJointUtils.FindHand(Handedness.Left) != null;
        }
    }
}
