#if !UNITY_2019_3_OR_NEWER
#define CINEMACHINE_PHYSICS
#define CINEMACHINE_PHYSICS_2D
#endif

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Playables;

namespace Cinemachine
{
#if !(CINEMACHINE_PHYSICS || CINEMACHINE_PHYSICS_2D)
    // Workaround for Unity scripting bug
    [AddComponentMenu("")] // Hide in menu
    public class CinemachineTriggerAction : MonoBehaviour {}
#else
    /// <summary>
    /// A multi-purpose script which causes an action to occur when
    /// a trigger collider is entered and exited.
    /// </summary>
    [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
    [SaveDuringPlay]
    public class CinemachineTriggerAction : MonoBehaviour
    {
        /// <summary>Only triggers generated by objects on these layers will be considered.</summary>
        [Header("Trigger Object Filter")]
        [Tooltip("Only triggers generated by objects on these layers will be considered")]
        public LayerMask m_LayerMask = 1;

        /// <summary>If set, only triggers generated by objects with this tag will be considered</summary>
        [TagField]
        [Tooltip("If set, only triggers generated by objects with this tag will be considered")]
        public string m_WithTag = string.Empty;

        /// <summary>Triggers generated by objects with this tag will be ignored</summary>
        [TagField]
        [Tooltip("Triggers generated by objects with this tag will be ignored")]
        public string m_WithoutTag = string.Empty;

        /// <summary>Skip this many trigger entries before taking action</summary>
        [NoSaveDuringPlay]
        [Tooltip("Skip this many trigger entries before taking action")]
        public int m_SkipFirst = 0;

        /// <summary>Repeat the action for all subsequent trigger entries</summary>
        [Tooltip("Repeat the action for all subsequent trigger entries")]
        public bool m_Repeating = true;

        /// <summary>Defines what action to take on trigger enter/exit</summary>
        [Serializable]
        public struct ActionSettings
        {
            /// <summary>What action to take</summary>
            public enum Mode
            {
                /// <summary>Use the event only</summary>
                Custom,
                /// <summary>Boost priority of virtual camera target</summary>
                PriorityBoost,
                /// <summary>Activate the target GameObject</summary>
                Activate,
                /// <summary>Decativate target GameObject</summary>
                Deactivate,
                /// <summary>Enable a component</summary>
                Enable,
                /// <summary>Disable a component</summary>
                Disable,
                /// <summary>Start animation on target</summary>
                Play,
                /// <summary>Stop animation on target</summary>
                Stop
            }

            /// <summary>Serializable parameterless game event</summary>
            [Serializable] public class TriggerEvent : UnityEvent {}

            /// <summary>What action to take</summary>
            [Tooltip("What action to take")]
            public Mode m_Action;

            /// <summary>The target object on which to operate.  If null, then the current behaviour/GameObject will be used</summary>
            [Tooltip("The target object on which to operate.  If null, then the current behaviour/GameObject will be used")]
            public UnityEngine.Object m_Target;

            /// <summary>If PriorityBoost, this amount will be added to the virtual camera's priority</summary>
            [Tooltip("If PriorityBoost, this amount will be added to the virtual camera's priority")]
            public int m_BoostAmount;

            /// <summary>If playing a timeline, start at this time</summary>
            [Tooltip("If playing a timeline, start at this time")]
            public float m_StartTime;

            /// <summary>How to interpret the start time</summary>
            public enum TimeMode { FromStart, FromEnd, BeforeNow, AfterNow };

            /// <summary>How to interpret the start time</summary>
            [Tooltip("How to interpret the start time")]
            public TimeMode m_Mode;

            /// <summary>This event will be invoked</summary>
            [Tooltip("This event will be invoked")]
            public TriggerEvent m_Event;

            /// <summary>Constructor</summary>
            public ActionSettings(Mode action)
            {
                m_Action = action;
                m_Target = null;
                m_BoostAmount = 0;
                m_StartTime = 0;
                m_Mode = TimeMode.FromStart;
                m_Event = new TriggerEvent();
            }

            /// <summary>Invoke the action.  Depending on the mode, different action will
            /// be performed.  The embedded event will always be invoked, in addition to the
            /// action specified by the Mode.</summary>
            public void Invoke()
            {
                UnityEngine.Object currentTarget = m_Target;
                if (currentTarget != null)
                {
                    GameObject targetGameObject = currentTarget as GameObject;
                    Behaviour targetBehaviour = currentTarget as Behaviour;
                    if (targetBehaviour != null)
                        targetGameObject = targetBehaviour.gameObject;

                    switch (m_Action)
                    {
                        case Mode.Custom:
                            break;
                        case Mode.PriorityBoost:
                            {
                                CinemachineVirtualCameraBase vcam
                                    = targetGameObject.GetComponent<CinemachineVirtualCameraBase>();
                                if (vcam != null)
                                {
                                    vcam.Priority += m_BoostAmount;
                                    vcam.MoveToTopOfPrioritySubqueue();
                                }
                                break;
                            }
                        case Mode.Activate:
                            if (targetGameObject != null)
                            {
                                targetGameObject.SetActive(true);
                                CinemachineVirtualCameraBase vcam
                                    = targetGameObject.GetComponent<CinemachineVirtualCameraBase>();
                                if (vcam != null)
                                    vcam.MoveToTopOfPrioritySubqueue();
                            }
                            break;
                        case Mode.Deactivate:
                            if (targetGameObject != null)
                                targetGameObject.SetActive(false);
                            break;
                        case Mode.Enable:
                            {
                                if (targetBehaviour != null)
                                    targetBehaviour.enabled = true;
                                break;
                            }
                        case Mode.Disable:
                            {
                                if (targetBehaviour != null)
                                    targetBehaviour.enabled = false;
                                break;
                            }
                        case Mode.Play:
                            {
                                PlayableDirector playable
                                    = targetGameObject.GetComponent<PlayableDirector>();
                                if (playable != null)
                                {
                                    double startTime = 0;
                                    double duration = playable.duration;
                                    double current = playable.time;
                                    switch (m_Mode)
                                    {
                                        default:
                                        case TimeMode.FromStart:
                                            startTime += m_StartTime;
                                            break;
                                        case TimeMode.FromEnd:
                                            startTime = duration - m_StartTime;
                                            break;
                                        case TimeMode.BeforeNow:
                                            startTime = current - m_StartTime;
                                            break;
                                        case TimeMode.AfterNow:
                                            startTime = current + m_StartTime;
                                            break;
                                    }
                                    playable.time = startTime;
                                    playable.Play();
                                }
                                else
                                {
                                    Animation ani = targetGameObject.GetComponent<Animation>();
                                    if (ani != null)
                                        ani.Play();
                                }
                                break;
                            }
                        case Mode.Stop:
                            {
                                PlayableDirector playable
                                    = targetGameObject.GetComponent<PlayableDirector>();
                                if (playable != null)
                                    playable.Stop();
                                else
                                {
                                    Animation ani = targetGameObject.GetComponent<Animation>();
                                    if (ani != null)
                                        ani.Stop();
                                }
                                break;
                            }
                    }
                }
                m_Event.Invoke();
            }
        }

        /// <summary>What action to take when an eligible object enters the collider or trigger zone</summary>
        public ActionSettings m_OnObjectEnter = new ActionSettings(ActionSettings.Mode.Custom);

        /// <summary>What action to take when an eligible object exits the collider or trigger zone</summary>
        public ActionSettings m_OnObjectExit = new ActionSettings(ActionSettings.Mode.Custom);

        HashSet<GameObject> m_ActiveTriggerObjects = new HashSet<GameObject>();

        private bool Filter(GameObject other)
        {
            if (!enabled)
                return false;
            if (((1 << other.layer) & m_LayerMask) == 0)
                return false;
            if (m_WithTag.Length != 0 && !other.CompareTag(m_WithTag))
                return false;
            if (m_WithoutTag.Length != 0 && other.CompareTag(m_WithoutTag))
                return false;

            return true;
        }

        void InternalDoTriggerEnter(GameObject other)
        {
            if (!Filter(other))
                return;
            --m_SkipFirst;
            if (m_SkipFirst > -1)
                return;
            if (!m_Repeating && m_SkipFirst != -1)
                return;

            m_ActiveTriggerObjects.Add(other);
            m_OnObjectEnter.Invoke();
        }

        void InternalDoTriggerExit(GameObject other)
        {
            if (!m_ActiveTriggerObjects.Contains(other))
                return;
            m_ActiveTriggerObjects.Remove(other);
            if (enabled)
                m_OnObjectExit.Invoke();
        }

#if CINEMACHINE_PHYSICS
        void OnTriggerEnter(Collider other) { InternalDoTriggerEnter(other.gameObject); }
        void OnTriggerExit(Collider other) { InternalDoTriggerExit(other.gameObject); }
        void OnCollisionEnter(Collision other) { InternalDoTriggerEnter(other.gameObject); }
        void OnCollisionExit(Collision other) { InternalDoTriggerExit(other.gameObject); }
#endif
#if CINEMACHINE_PHYSICS_2D
        void OnTriggerEnter2D(Collider2D other) { InternalDoTriggerEnter(other.gameObject); }
        void OnTriggerExit2D(Collider2D other) { InternalDoTriggerExit(other.gameObject); }
        void OnCollisionEnter2D(Collision2D other) { InternalDoTriggerEnter(other.gameObject); }
        void OnCollisionExit2D(Collision2D other) { InternalDoTriggerExit(other.gameObject); }
#endif
        void OnEnable() {} // For the Enabled checkbox
    }
#endif
}
