using System;
using System.Collections.Generic;
using System.Linq;
using Cinemachine.Utility;
using UnityEngine;
using UnityEngine.Serialization;

namespace Cinemachine
{
    /// <summary>
    /// Base class for a Monobehaviour that represents a Virtual Camera within the Unity scene.
    ///
    /// This is intended to be attached to an empty Transform GameObject.
    /// Inherited classes can be either standalone virtual cameras such
    /// as CinemachineVirtualCamera, or meta-cameras such as
    /// CinemachineClearShot or CinemachineFreeLook.
    ///
    /// A CinemachineVirtualCameraBase exposes a Priority property.  When the behaviour is
    /// enabled in the game, the Virtual Camera is automatically placed in a queue
    /// maintained by the static CinemachineCore singleton.
    /// The queue is sorted by priority.  When a Unity camera is equipped with a
    /// CinemachineBrain behaviour, the brain will choose the camera
    /// at the head of the queue.  If you have multiple Unity cameras with CinemachineBrain
    /// behaviours (say in a split-screen context), then you can filter the queue by
    /// setting the culling flags on the virtual cameras.  The culling mask of the
    /// Unity Camera will then act as a filter for the brain.  Apart from this,
    /// there is nothing that prevents a virtual camera from controlling multiple
    /// Unity cameras simultaneously.
    /// </summary>
    [SaveDuringPlay]
    public abstract class CinemachineVirtualCameraBase : MonoBehaviour, ICinemachineCamera, ISerializationCallbackReceiver
    {
        /// <summary>Inspector control - Use for hiding sections of the Inspector UI.</summary>
        [HideInInspector, SerializeField, NoSaveDuringPlay]
        public string[] m_ExcludedPropertiesInInspector = new string[] { "m_Script" };

        /// <summary>Inspector control - Use for enabling sections of the Inspector UI.</summary>
        [HideInInspector, SerializeField, NoSaveDuringPlay]
        public CinemachineCore.Stage[] m_LockStageInInspector;

        /// <summary>Version that was last streamed, for upgrading legacy</summary>
        public int ValidatingStreamVersion
        {
            get { return m_OnValidateCalled ? m_ValidatingStreamVersion : CinemachineCore.kStreamingVersion; }
            private set { m_ValidatingStreamVersion = value; }
        }
        private int m_ValidatingStreamVersion = 0;
        private bool m_OnValidateCalled = false;

        [HideInInspector, SerializeField, NoSaveDuringPlay]
        private int m_StreamingVersion;

        /// <summary>The priority will determine which camera becomes active based on the
        /// state of other cameras and this camera.  Higher numbers have greater priority.
        /// </summary>
        [NoSaveDuringPlay]
        [Tooltip("The priority will determine which camera becomes active based on the state of "
            + "other cameras and this camera.  Higher numbers have greater priority.")]
        public int m_Priority = 10;

        /// <summary>A sequence number that represents object activation order of vcams.  
        /// Used for priority sorting.</summary>
        internal int m_ActivationId;

        /// <summary>
        /// This must be set every frame at the start of the pipeline to relax the virtual camera's
        /// attachment to the target.  Range is 0...1.  
        /// 1 is full attachment, and is the normal state.
        /// 0 is no attachment, and virtual camera will behave as if no Follow 
        /// targets are set.
        /// </summary>
        [NonSerialized]
        public float FollowTargetAttachment;

        /// <summary>
        /// This must be set every frame at the start of the pipeline to relax the virtual camera's
        /// attachment to the target.  Range is 0...1.  
        /// 1 is full attachment, and is the normal state.
        /// 0 is no attachment, and virtual camera will behave as if no LookAt
        /// targets are set.
        /// </summary>
        [NonSerialized]
        public float LookAtTargetAttachment;

        /// <summary>
        /// How often to update a virtual camera when it is in Standby mode
        /// </summary>
        public enum StandbyUpdateMode
        {
            /// <summary>Only update if the virtual camera is Live</summary>
            Never,
            /// <summary>Update the virtual camera every frame, even when it is not Live</summary>
            Always,
            /// <summary>Update the virtual camera occasionally, the exact frequency depends
            /// on how many other virtual cameras are in Standby</summary>
            RoundRobin
        };

        /// <summary>When the virtual camera is not live, this is how often the virtual camera will
        /// be updated.  Set this to tune for performance. Most of the time Never is fine, unless
        /// the virtual camera is doing shot evaluation.
        /// </summary>
        [Tooltip("When the virtual camera is not live, this is how often the virtual camera will be updated.  "
            + "Set this to tune for performance. Most of the time Never is fine, "
            + "unless the virtual camera is doing shot evaluation.")]
        public StandbyUpdateMode m_StandbyUpdate = StandbyUpdateMode.RoundRobin;

        /// <summary>
        /// Query components and extensions for the maximum damping time.
        /// Base class implementation queries extensions.
        /// Only used in editor for timeline scrubbing.
        /// </summary>
        /// <returns>Highest damping setting in this vcam</returns>
        public virtual float GetMaxDampTime()
        {
            float maxDamp = 0;
            if (mExtensions != null)
                for (int i = 0; i < mExtensions.Count; ++i)
                    maxDamp = Mathf.Max(maxDamp, mExtensions[i].GetMaxDampTime());
            return maxDamp;
        }

        /// <summary>Get a damped version of a quantity.  This is the portion of the
        /// quantity that will take effect over the given time.
        /// This method takes the target attachment into account.  For general
        /// damping without consideration of target attachment, use Damper.Damp()</summary>
        /// <param name="initial">The amount that will be damped</param>
        /// <param name="dampTime">The rate of damping.  This is the time it would
        /// take to reduce the original amount to a negligible percentage</param>
        /// <param name="deltaTime">The time over which to damp</param>
        /// <returns>The damped amount.  This will be the original amount scaled by
        /// a value between 0 and 1.</returns>
        public float DetachedFollowTargetDamp(float initial, float dampTime, float deltaTime)
        {
            dampTime = Mathf.Lerp(Mathf.Max(1, dampTime), dampTime, FollowTargetAttachment);
            deltaTime = Mathf.Lerp(0, deltaTime, FollowTargetAttachment);
            return Damper.Damp(initial, dampTime, deltaTime);
        }

        /// <summary>Get a damped version of a quantity.  This is the portion of the
        /// quantity that will take effect over the given time.
        /// This method takes the target attachment into account.  For general
        /// damping without consideration of target attachment, use Damper.Damp()</summary>
        /// <param name="initial">The amount that will be damped</param>
        /// <param name="dampTime">The rate of damping.  This is the time it would
        /// take to reduce the original amount to a negligible percentage</param>
        /// <param name="deltaTime">The time over which to damp</param>
        /// <returns>The damped amount.  This will be the original amount scaled by
        /// a value between 0 and 1.</returns>
        public Vector3 DetachedFollowTargetDamp(Vector3 initial, Vector3 dampTime, float deltaTime)
        {
            dampTime = Vector3.Lerp(Vector3.Max(Vector3.one, dampTime), dampTime, FollowTargetAttachment);
            deltaTime = Mathf.Lerp(0, deltaTime, FollowTargetAttachment);
            return Damper.Damp(initial, dampTime, deltaTime);
        }

        /// <summary>Get a damped version of a quantity.  This is the portion of the
        /// quantity that will take effect over the given time.
        /// This method takes the target attachment into account.  For general
        /// damping without consideration of target attachment, use Damper.Damp()</summary>
        /// <param name="initial">The amount that will be damped</param>
        /// <param name="dampTime">The rate of damping.  This is the time it would
        /// take to reduce the original amount to a negligible percentage</param>
        /// <param name="deltaTime">The time over which to damp</param>
        /// <returns>The damped amount.  This will be the original amount scaled by
        /// a value between 0 and 1.</returns>
        public Vector3 DetachedFollowTargetDamp(Vector3 initial, float dampTime, float deltaTime)
        {
            dampTime = Mathf.Lerp(Mathf.Max(1, dampTime), dampTime, FollowTargetAttachment);
            deltaTime = Mathf.Lerp(0, deltaTime, FollowTargetAttachment);
            return Damper.Damp(initial, dampTime, deltaTime);
        }

        /// <summary>Get a damped version of a quantity.  This is the portion of the
        /// quantity that will take effect over the given time.
        /// This method takes the target attachment into account.  For general
        /// damping without consideration of target attachment, use Damper.Damp()</summary>
        /// <param name="initial">The amount that will be damped</param>
        /// <param name="dampTime">The rate of damping.  This is the time it would
        /// take to reduce the original amount to a negligible percentage</param>
        /// <param name="deltaTime">The time over which to damp</param>
        /// <returns>The damped amount.  This will be the original amount scaled by
        /// a value between 0 and 1.</returns>
        public float DetachedLookAtTargetDamp(float initial, float dampTime, float deltaTime)
        {
            dampTime = Mathf.Lerp(Mathf.Max(1, dampTime), dampTime, LookAtTargetAttachment);
            deltaTime = Mathf.Lerp(0, deltaTime, LookAtTargetAttachment);
            return Damper.Damp(initial, dampTime, deltaTime);
        }

        /// <summary>Get a damped version of a quantity.  This is the portion of the
        /// quantity that will take effect over the given time.
        /// This method takes the target attachment into account.  For general
        /// damping without consideration of target attachment, use Damper.Damp()</summary>
        /// <param name="initial">The amount that will be damped</param>
        /// <param name="dampTime">The rate of damping.  This is the time it would
        /// take to reduce the original amount to a negligible percentage</param>
        /// <param name="deltaTime">The time over which to damp</param>
        /// <returns>The damped amount.  This will be the original amount scaled by
        /// a value between 0 and 1.</returns>
        public Vector3 DetachedLookAtTargetDamp(Vector3 initial, Vector3 dampTime, float deltaTime)
        {
            dampTime = Vector3.Lerp(Vector3.Max(Vector3.one, dampTime), dampTime, LookAtTargetAttachment);
            deltaTime = Mathf.Lerp(0, deltaTime, LookAtTargetAttachment);
            return Damper.Damp(initial, dampTime, deltaTime);
        }

        /// <summary>Get a damped version of a quantity.  This is the portion of the
        /// quantity that will take effect over the given time.
        /// This method takes the target attachment into account.  For general
        /// damping without consideration of target attachment, use Damper.Damp()</summary>
        /// <param name="initial">The amount that will be damped</param>
        /// <param name="dampTime">The rate of damping.  This is the time it would
        /// take to reduce the original amount to a negligible percentage</param>
        /// <param name="deltaTime">The time over which to damp</param>
        /// <returns>The damped amount.  This will be the original amount scaled by
        /// a value between 0 and 1.</returns>
        public Vector3 DetachedLookAtTargetDamp(Vector3 initial, float dampTime, float deltaTime)
        {
            dampTime = Mathf.Lerp(Mathf.Max(1, dampTime), dampTime, LookAtTargetAttachment);
            deltaTime = Mathf.Lerp(0, deltaTime, LookAtTargetAttachment);
            return Damper.Damp(initial, dampTime, deltaTime);
        }

        /// <summary>
        /// A delegate to hook into the state calculation pipeline.
        /// This will be called after each pipeline stage, to allow others to hook into the pipeline.
        /// See CinemachineCore.Stage.
        /// </summary>
        /// <param name="extension">The extension to add.</param>
        public virtual void AddExtension(CinemachineExtension extension)
        {
            if (mExtensions == null)
                mExtensions = new List<CinemachineExtension>();
            else
                mExtensions.Remove(extension);
            mExtensions.Add(extension);
        }

        /// <summary>Remove a Pipeline stage hook callback.</summary>
        /// <param name="extension">The extension to remove.</param>
        public virtual void RemoveExtension(CinemachineExtension extension)
        {
            if (mExtensions != null)
                mExtensions.Remove(extension);
        }

        /// <summary> The extensions connected to this vcam</summary>
        internal List<CinemachineExtension> mExtensions { get; private set; }

        /// <summary>
        /// Invokes the PostPipelineStageDelegate for this camera, and up the hierarchy for all
        /// parent cameras (if any).
        /// Implementaion must be sure to call this after each pipeline stage, to allow
        /// other services to hook into the pipeline.
        /// See CinemachineCore.Stage.
        /// </summary>
        /// <param name="vcam">The virtual camera being processed</param>
        /// <param name="stage">The current pipeline stage</param>
        /// <param name="newState">The current virtual camera state</param>
        /// <param name="deltaTime">The current applicable deltaTime</param>
        protected void InvokePostPipelineStageCallback(
            CinemachineVirtualCameraBase vcam, CinemachineCore.Stage stage,
            ref CameraState newState, float deltaTime)
        {
            if (mExtensions != null)
            {
                for (int i = 0; i < mExtensions.Count; ++i)
                {
                    var e = mExtensions[i];
                    if (e == null)
                    {
                        // Object was deleted (possibly because of Undo in the editor)
                        mExtensions.RemoveAt(i);
                        --i;
                    }
                    else if (e.enabled)
                        e.InvokePostPipelineStageCallback(vcam, stage, ref newState, deltaTime);
                }
            }
            CinemachineVirtualCameraBase parent = ParentCamera as CinemachineVirtualCameraBase;
            if (parent != null)
                parent.InvokePostPipelineStageCallback(vcam, stage, ref newState, deltaTime);
        }
        
        /// <summary>
        /// Invokes the PrePipelineMutateCameraStateCallback for this camera, 
        /// and up the hierarchy for all parent cameras (if any).
        /// Implementaion must be sure to call this after each pipeline stage, to allow
        /// other services to hook into the pipeline.
        /// See CinemachineCore.Stage.
        /// </summary>
        /// <param name="vcam">The virtual camera being processed</param>
        /// <param name="newState">The current virtual camera state</param>
        /// <param name="deltaTime">The current applicable deltaTime</param>
        protected void InvokePrePipelineMutateCameraStateCallback(
            CinemachineVirtualCameraBase vcam, ref CameraState newState, float deltaTime)
        {
            if (mExtensions != null)
            {
                for (int i = 0; i < mExtensions.Count; ++i)
                {
                    var e = mExtensions[i];
                    if (e == null)
                    {
                        // Object was deleted (possibly because of Undo in the editor)
                        mExtensions.RemoveAt(i);
                        --i;
                    }
                    else if (e.enabled)
                        e.PrePipelineMutateCameraStateCallback(vcam, ref newState, deltaTime);
                }
            }
            CinemachineVirtualCameraBase parent = ParentCamera as CinemachineVirtualCameraBase;
            if (parent != null)
                parent.InvokePrePipelineMutateCameraStateCallback(vcam, ref newState, deltaTime);
        }

        /// <summary>
        /// Invokes the OnTransitionFromCamera for all extensions on this camera
        /// </summary>
        /// <param name="fromCam">The camera being deactivated.  May be null.</param>
        /// <param name="worldUp">Default world Up, set by the CinemachineBrain</param>
        /// <param name="deltaTime">Delta time for time-based effects (ignore if less than or equal to 0)</param>
        /// <returns>True to request a vcam update of internal state</returns>
        protected bool InvokeOnTransitionInExtensions(
            ICinemachineCamera fromCam, Vector3 worldUp, float deltaTime)
        {
            bool forceUpdate = false;
            if (mExtensions != null)
            {
                for (int i = 0; i < mExtensions.Count; ++i)
                {
                    var e = mExtensions[i];
                    if (e == null)
                    {
                        // Object was deleted (possibly because of Undo in the editor)
                        mExtensions.RemoveAt(i);
                        --i;
                    }
                    else if (e.enabled && e.OnTransitionFromCamera(fromCam, worldUp, deltaTime))
                        forceUpdate = true;
                }
            }
            return forceUpdate;
        }

        /// <summary>Get the name of the Virtual Camera.  Base implementation
        /// returns the owner GameObject's name.</summary>
        public string Name => name;

        /// <summary>Gets a brief debug description of this virtual camera, for use when displayiong debug info</summary>
        public virtual string Description => "";

        /// <summary>Get the Priority of the virtual camera.  This determines its placement
        /// in the CinemachineCore's queue of eligible shots.</summary>
        public int Priority { 
            get => m_Priority;
            set => m_Priority = value;
        }

        /// <summary>Hint for blending to and from this virtual camera</summary>
        public enum BlendHint
        {
            /// <summary>Standard linear position and aim blend</summary>
            None,
            /// <summary>Spherical blend about LookAt target position if there is a LookAt target, linear blend between LookAt targets</summary>
            SphericalPosition,
            /// <summary>Cylindrical blend about LookAt target position if there is a LookAt target (vertical co-ordinate is linearly interpolated), linear blend between LookAt targets</summary>
            CylindricalPosition,
            /// <summary>Standard linear position blend, radial blend between LookAt targets</summary>
            ScreenSpaceAimWhenTargetsDiffer
        }

        /// <summary>Applies a position blend hint to a camera state</summary>
        /// <param name="state">The state to apply the hint to</param>
        /// <param name="hint">The hint to apply</param>
        protected void ApplyPositionBlendMethod(ref CameraState state, BlendHint hint)
        {
            switch (hint)
            {
                default:
                    break;
                case BlendHint.SphericalPosition:
                    state.BlendHint |= CameraState.BlendHintValue.SphericalPositionBlend;
                    break;
                case BlendHint.CylindricalPosition:
                    state.BlendHint |= CameraState.BlendHintValue.CylindricalPositionBlend;
                    break;
                case BlendHint.ScreenSpaceAimWhenTargetsDiffer:
                    state.BlendHint |= CameraState.BlendHintValue.RadialAimBlend;
                    break;
            }
        }

        /// <summary>The GameObject owner of the Virtual Camera behaviour.</summary>
        public GameObject VirtualCameraGameObject
        {
            get
            {
                if (this == null)
                    return null; // object deleted
                return gameObject;
            }
        }

        /// <summary>Returns false if the object has been deleted</summary>
        public bool IsValid => !(this == null);

        /// <summary>The CameraState object holds all of the information
        /// necessary to position the Unity camera.  It is the output of this class.</summary>
        public abstract CameraState State { get; }

        /// <summary>Support for meta-virtual-cameras.  This is the situation where a
        /// virtual camera is in fact the public face of a private army of virtual cameras, which
        /// it manages on its own.  This method gets the VirtualCamera owner, if any.
        /// Private armies are implemented as Transform children of the parent vcam.</summary>
        public ICinemachineCamera ParentCamera
        {
            get
            {
                if (!mSlaveStatusUpdated || !Application.isPlaying)
                    UpdateSlaveStatus();
                return m_parentVcam;
            }
        }

        /// <summary>Check whether the vcam a live child of this camera.
        /// This base class implementation always returns false.</summary>
        /// <param name="vcam">The Virtual Camera to check</param>
        /// <param name="dominantChildOnly">If truw, will only return true if this vcam is the dominat live child</param>
        /// <returns>True if the vcam is currently actively influencing the state of this vcam</returns>
        public virtual bool IsLiveChild(ICinemachineCamera vcam, bool dominantChildOnly = false) { return false; }

        /// <summary>Get the LookAt target for the Aim component in the Cinemachine pipeline.</summary>
        public abstract Transform LookAt { get; set; }

        /// <summary>Get the Follow target for the Body component in the Cinemachine pipeline.</summary>
        public abstract Transform Follow { get; set; }

        /// <summary>Set this to force the next update to ignore state from the previous frame.  
        /// This is useful, for example, if you want to cancel damping or other time-based processing.</summary>
        public virtual bool PreviousStateIsValid { get; set; }

        /// <summary>
        /// Update the camera's state.
        /// The implementation must guarantee against multiple calls per frame, and should
        /// use CinemachineCore.UpdateVirtualCamera(ICinemachineCamera, Vector3, float), which
        /// has protection against multiple calls per frame.
        /// </summary>
        /// <param name="worldUp">Default world Up, set by the CinemachineBrain</param>
        /// <param name="deltaTime">Delta time for time-based effects (ignore if less than 0)</param>
        public void UpdateCameraState(Vector3 worldUp, float deltaTime)
        {
            CinemachineCore.Instance.UpdateVirtualCamera(this, worldUp, deltaTime);
        }

        /// <summary>Internal use only.  Do not call this method.
        /// Called by CinemachineCore at designated update time
        /// so the vcam can position itself and track its targets.
        /// Do not call this method.  Let the framework do it at the appropriate time</summary>
        /// <param name="worldUp">Default world Up, set by the CinemachineBrain</param>
        /// <param name="deltaTime">Delta time for time-based effects (ignore if less than 0)</param>
        public abstract void InternalUpdateCameraState(Vector3 worldUp, float deltaTime);

        /// <summary> Collection of parameters that influence how this virtual camera transitions from
        /// other virtual cameras </summary>
        [Serializable]
        public struct TransitionParams
        {
            /// <summary>Hint for blending positions to and from this virtual camera</summary>
            [Tooltip("Hint for blending positions to and from this virtual camera")]
            [FormerlySerializedAs("m_PositionBlending")]
            public BlendHint m_BlendHint;

            /// <summary>When this virtual camera goes Live, attempt to force the position to be the same as the current position of the Unity Camera</summary>
            [Tooltip("When this virtual camera goes Live, attempt to force the position to be the same as the current position of the Unity Camera")]
            public bool m_InheritPosition;

            /// <summary>This event fires when the virtual camera goes Live</summary>
            [Tooltip("This event fires when the virtual camera goes Live")]
            public CinemachineBrain.VcamActivatedEvent m_OnCameraLive;
        }

        /// <summary>Notification that this virtual camera is going live.
        /// Base class implementation must be called by any overridden method.</summary>
        /// <param name="fromCam">The camera being deactivated.  May be null.</param>
        /// <param name="worldUp">Default world Up, set by the CinemachineBrain</param>
        /// <param name="deltaTime">Delta time for time-based effects (ignore if less than or equal to 0)</param>
        public virtual void OnTransitionFromCamera(
            ICinemachineCamera fromCam, Vector3 worldUp, float deltaTime)
        {
            if (!gameObject.activeInHierarchy)
                PreviousStateIsValid = false;
        }

        /// <summary>Maintains the global vcam registry.  Always call the base class implementation.</summary>
        protected virtual void OnDestroy()
        {
            CinemachineCore.Instance.CameraDestroyed(this);
        }

        /// <summary>Base class implementation makes sure the priority queue remains up-to-date.</summary>
        protected virtual void OnTransformParentChanged()
        {
            CinemachineCore.Instance.CameraDisabled(this);
            CinemachineCore.Instance.CameraEnabled(this);
            UpdateSlaveStatus();
            UpdateVcamPoolStatus();
        }

        bool m_WasStarted;

        /// <summary>Derived classes should call base class implementation.</summary>
        protected virtual void Start()
        {
            m_WasStarted = true;
        }
        
        /// <summary>
        /// Returns true, when the vcam has an extension that requires user input.
        /// </summary>
        internal virtual bool RequiresUserInput()
        {
            return mExtensions != null && mExtensions.Any(extension => extension != null && extension.RequiresUserInput); 
        }

        /// <summary>
        /// Called on inactive object when being artificially activated by timeline.
        /// This is necessary because Awake() isn't called on inactive gameObjects.
        /// </summary>
        internal void EnsureStarted()
        {
            if (!m_WasStarted)
            {
                m_WasStarted = true;
                var extensions = GetComponentsInChildren<CinemachineExtension>();
                for (int i = 0; i < extensions.Length; ++i)
                    extensions[i].EnsureStarted();
            }
        }

#if UNITY_EDITOR
        [UnityEditor.Callbacks.DidReloadScripts]
        static void OnScriptReload()
        {
            var vcams = Resources.FindObjectsOfTypeAll(
                typeof(CinemachineVirtualCameraBase)) as CinemachineVirtualCameraBase[];
            foreach (var vcam in vcams)
                vcam.LookAtTargetChanged = vcam.FollowTargetChanged = true;
        }
#endif

        /// <summary>
        /// Locate the first component that implements AxisState.IInputAxisProvider.
        /// </summary>
        /// <returns>The first AxisState.IInputAxisProvider or null if none</returns>
        public AxisState.IInputAxisProvider GetInputAxisProvider()
        {
            var components = GetComponentsInChildren<MonoBehaviour>();
            for (int i = 0; i < components.Length; ++i)
            {
                var provider = components[i] as AxisState.IInputAxisProvider;
                if (provider != null)
                    return provider;
            }
            return null;
        }

        /// <summary>Enforce bounds for fields, when changed in inspector.
        /// Call base class implementation at the beginning of overridden method.
        /// After base method is called, ValidatingStreamVersion will be valid.</summary>
        protected virtual void OnValidate()
        {
            m_OnValidateCalled = true;
            ValidatingStreamVersion = m_StreamingVersion;
            m_StreamingVersion = CinemachineCore.kStreamingVersion;
        }

        /// <summary>Base class implementation adds the virtual camera from the priority queue.</summary>
        protected virtual void OnEnable()
        {
            UpdateSlaveStatus();
            UpdateVcamPoolStatus();    // Add to queue
            if (!CinemachineCore.Instance.IsLive(this))
                PreviousStateIsValid = false;
            CinemachineCore.Instance.CameraEnabled(this);
            InvalidateCachedTargets();
            // Sanity check - if another vcam component is enabled, shut down
            var vcamComponents = GetComponents<CinemachineVirtualCameraBase>();
            for (int i = 0; i < vcamComponents.Length; ++i)
            {
                if (vcamComponents[i].enabled && vcamComponents[i] != this)
                {
                    Debug.LogError(Name
                        + " has multiple CinemachineVirtualCameraBase-derived components.  Disabling "
                        + GetType().Name + ".");
                    enabled = false;
                }
            }
        }

        /// <summary>Base class implementation makes sure the priority queue remains up-to-date.</summary>
        protected virtual void OnDisable()
        {
            UpdateVcamPoolStatus();    // Remove from queue
            CinemachineCore.Instance.CameraDisabled(this);
        }

        /// <summary>Base class implementation makes sure the priority queue remains up-to-date.</summary>
        protected virtual void Update()
        {
            if (m_Priority != m_QueuePriority)
            {
                UpdateVcamPoolStatus(); // Force a re-sort
            }
        }

        private bool mSlaveStatusUpdated = false;
        private CinemachineVirtualCameraBase m_parentVcam = null;

        private void UpdateSlaveStatus()
        {
            mSlaveStatusUpdated = true;
            m_parentVcam = null;
            Transform p = transform.parent;
            if (p != null)
            {
#if UNITY_2019_2_OR_NEWER
                p.TryGetComponent(out m_parentVcam);
#else
                m_parentVcam = p.GetComponent<CinemachineVirtualCameraBase>();
#endif
            }
        }

        /// <summary>Returns this vcam's LookAt target, or if that is null, will retrun
        /// the parent vcam's LookAt target.</summary>
        /// <param name="localLookAt">This vcam's LookAt value.</param>
        /// <returns>The same value, or the parent's if null and a parent exists.</returns>
        public Transform ResolveLookAt(Transform localLookAt)
        {
            Transform lookAt = localLookAt;
            if (lookAt == null && ParentCamera != null)
                lookAt = ParentCamera.LookAt; // Parent provides default
            return lookAt;
        }

        /// <summary>Returns this vcam's Follow target, or if that is null, will retrun
        /// the parent vcam's Follow target.</summary>
        /// <param name="localFollow">This vcam's Follow value.</param>
        /// <returns>The same value, or the parent's if null and a parent exists.</returns>
        public Transform ResolveFollow(Transform localFollow)
        {
            Transform follow = localFollow;
            if (follow == null && ParentCamera != null)
                follow = ParentCamera.Follow; // Parent provides default
            return follow;
        }

        private int m_QueuePriority = int.MaxValue;
        private void UpdateVcamPoolStatus()
        {
            CinemachineCore.Instance.RemoveActiveCamera(this);
            if (m_parentVcam == null && isActiveAndEnabled)
                CinemachineCore.Instance.AddActiveCamera(this);
            m_QueuePriority = m_Priority;
        }

        /// <summary>When multiple virtual cameras have the highest priority, there is
        /// sometimes the need to push one to the top, making it the current Live camera if
        /// it shares the highest priority in the queue with its peers.
        ///
        /// This happens automatically when a
        /// new vcam is enabled: the most recent one goes to the top of the priority subqueue.
        /// Use this method to push a vcam to the top of its priority peers.
        /// If it and its peers share the highest priority, then this vcam will become Live.</summary>
        public void MoveToTopOfPrioritySubqueue()
        {
            UpdateVcamPoolStatus(); // Force a re-sort
        }

        /// <summary>This is called to notify the component that a target got warped,
        /// so that the component can update its internal state to make the camera
        /// also warp seamlessy.</summary>
        /// <param name="target">The object that was warped</param>
        /// <param name="positionDelta">The amount the target's position changed</param>
        public virtual void OnTargetObjectWarped(Transform target, Vector3 positionDelta)
        {
            // inform the extensions
            if (mExtensions != null)
            {
                for (int i = 0; i < mExtensions.Count; ++i)
                    mExtensions[i].OnTargetObjectWarped(target, positionDelta);
            }
        }

        /// <summary>
        /// Force the virtual camera to assume a given position and orientation
        /// </summary>
        /// <param name="pos">Worldspace pposition to take</param>
        /// <param name="rot">Worldspace orientation to take</param>
        public virtual void ForceCameraPosition(Vector3 pos, Quaternion rot)
        {
            // inform the extensions
            if (mExtensions != null)
            {
                for (int i = 0; i < mExtensions.Count; ++i)
                    mExtensions[i].ForceCameraPosition(pos, rot);
            }
        }
        
        // Doesn't really belong here but putting it here to avoid changing
        // the API (belongs in the caller of CreateBlend).  GML todo: Fix in next version.
        float m_blendStartPosition;

        // This is a total hack, because of missing API call.  GML todo: Fix in next version.
        bool GetInheritPosition(ICinemachineCamera cam)
        {
            if (cam is CinemachineVirtualCamera)
                return (cam as CinemachineVirtualCamera).m_Transitions.m_InheritPosition;
            if (cam is CinemachineFreeLook)
                return (cam as CinemachineFreeLook).m_Transitions.m_InheritPosition;
            return false;
        }

        /// <summary>Create a blend between 2 virtual cameras, taking into account
        /// any existing active blend, with special case handling if the new blend is 
        /// effectively an undo of the current blend</summary>
        /// <param name="camA">Outgoing virtual camera</param>
        /// <param name="camB">Incoming virtual camera</param>
        /// <param name="blendDef">Definition of the blend to create</param>
        /// <param name="activeBlend">The current active blend</param>
        /// <returns>The new blend</returns>
        protected CinemachineBlend CreateBlend(
            ICinemachineCamera camA, ICinemachineCamera camB,
            CinemachineBlendDefinition blendDef,
            CinemachineBlend activeBlend)
        {
            if (blendDef.BlendCurve == null || blendDef.BlendTime <= 0 || (camA == null && camB == null))
            {
                m_blendStartPosition = 0;
                return null;
            }
            if (activeBlend != null)
            {
                // Special case: if backing out of a blend-in-progress
                // with the same blend in reverse, adjust the blend time
                // to cancel out the progress made in the opposite direction
                if (activeBlend != null && !activeBlend.IsComplete && activeBlend.CamA == camB && activeBlend.CamB == camA)
                {
                    // How far have we blended?  That is what we must undo
                    var progress = m_blendStartPosition 
                        + (1 - m_blendStartPosition) * activeBlend.TimeInBlend / activeBlend.Duration;
                    blendDef.m_Time *= progress;
                    m_blendStartPosition = 1 - progress;
                }
                else
                    m_blendStartPosition = 0;

                if (GetInheritPosition(camB))
                    camA = null;  // otherwise we get a pop when camB is moved
                else
                    camA = new BlendSourceVirtualCamera(activeBlend);
            }
            if (camA == null)
                camA = new StaticPointVirtualCamera(State, "(none)");
            return new CinemachineBlend(
                camA, camB, blendDef.BlendCurve, blendDef.BlendTime, 0);
        }

        /// <summary>
        /// Create a camera state based on the current transform of this vcam
        /// </summary>
        /// <param name="worldUp">Current World Up direction, as provided by the brain</param>
        /// <param name="lens">Lens settings to serve as base, will be combined with lens from brain, if any</param>
        /// <returns></returns>
        protected CameraState PullStateFromVirtualCamera(Vector3 worldUp, ref LensSettings lens)
        {
            CameraState state = CameraState.Default;
            state.RawPosition = TargetPositionCache.GetTargetPosition(transform);
            state.RawOrientation = TargetPositionCache.GetTargetRotation(transform);
            state.ReferenceUp = worldUp;

            CinemachineBrain brain = CinemachineCore.Instance.FindPotentialTargetBrain(this);
            if (brain != null)
                lens.SnapshotCameraReadOnlyProperties(brain.OutputCamera);

            state.Lens = lens;
            return state;
        }

        private Transform m_CachedFollowTarget;
        private CinemachineVirtualCameraBase m_CachedFollowTargetVcam;
        private ICinemachineTargetGroup m_CachedFollowTargetGroup;

        private Transform m_CachedLookAtTarget;
        private CinemachineVirtualCameraBase m_CachedLookAtTargetVcam;
        private ICinemachineTargetGroup m_CachedLookAtTargetGroup;

        private void InvalidateCachedTargets()
        {
            m_CachedFollowTarget = null;
            m_CachedFollowTargetVcam = null;
            m_CachedFollowTargetGroup = null;
            m_CachedLookAtTarget = null;
            m_CachedLookAtTargetVcam = null;
            m_CachedLookAtTargetGroup = null;
        }

#if UNITY_EDITOR
        [UnityEditor.InitializeOnLoad]
        class OnDomainReload 
        { 
            static OnDomainReload() 
            {
#if UNITY_2023_1_OR_NEWER
                var vcams = FindObjectsByType<CinemachineVirtualCameraBase>
                    (FindObjectsInactive.Include, FindObjectsSortMode.None);
#elif UNITY_2020_1_OR_NEWER
                var vcams = FindObjectsOfType<CinemachineVirtualCameraBase>(true);
#else
                var vcams = FindObjectsOfType<CinemachineVirtualCameraBase>();
#endif
                foreach (var vcam in vcams)
                    vcam.InvalidateCachedTargets();
            }
        }
#endif

        /// <summary>
        /// This property is true if the Follow target was changed this frame.
        /// </summary>
        public bool FollowTargetChanged { get; private set; }

        /// <summary>
        /// This property is true if the LookAttarget was changed this frame.
        /// </summary>
        public bool LookAtTargetChanged { get; private set; }

        /// <summary>
        /// Call this from InternalUpdateCameraState() to check for changed 
        /// targets and update the target cache.  This is needed for tracking
        /// when a target object changes.
        /// </summary>
        protected void UpdateTargetCache()
        {
            var target = ResolveFollow(Follow);
            FollowTargetChanged = target != m_CachedFollowTarget;
            if (FollowTargetChanged)
            {
                m_CachedFollowTarget = target;
                m_CachedFollowTargetVcam = null;
                m_CachedFollowTargetGroup = null;
                if (m_CachedFollowTarget != null)
                {
                    target.TryGetComponent<CinemachineVirtualCameraBase>(out m_CachedFollowTargetVcam);
                    target.TryGetComponent<ICinemachineTargetGroup>(out m_CachedFollowTargetGroup);
                }
            }
            target = ResolveLookAt(LookAt);
            LookAtTargetChanged = target != m_CachedLookAtTarget;
            if (LookAtTargetChanged)
            {
                m_CachedLookAtTarget = target;
                m_CachedLookAtTargetVcam = null;
                m_CachedLookAtTargetGroup = null;
                if (target != null)
                {
                    target.TryGetComponent<CinemachineVirtualCameraBase>(out m_CachedLookAtTargetVcam);
                    target.TryGetComponent<ICinemachineTargetGroup>(out m_CachedLookAtTargetGroup);
                }
            }
        }

        /// <summary>Get Follow target as ICinemachineTargetGroup, 
        /// or null if target is not a ICinemachineTargetGroup</summary>
        public ICinemachineTargetGroup AbstractFollowTargetGroup => m_CachedFollowTargetGroup;

        /// <summary>Get Follow target as CinemachineVirtualCameraBase, 
        /// or null if target is not a CinemachineVirtualCameraBase</summary>
        public CinemachineVirtualCameraBase FollowTargetAsVcam => m_CachedFollowTargetVcam;

        /// <summary>Get LookAt target as ICinemachineTargetGroup, 
        /// or null if target is not a ICinemachineTargetGroup</summary>
        public ICinemachineTargetGroup AbstractLookAtTargetGroup => m_CachedLookAtTargetGroup;

        /// <summary>Get LookAt target as CinemachineVirtualCameraBase, 
        /// or null if target is not a CinemachineVirtualCameraBase</summary>
        public CinemachineVirtualCameraBase LookAtTargetAsVcam => m_CachedLookAtTargetVcam;

        /// <summary>Pre-Serialization handler - delegates to derived classes</summary>
        void ISerializationCallbackReceiver.OnBeforeSerialize() => OnBeforeSerialize();

        /// <summary>Post-Serialization handler - performs legacy upgrade</summary>
        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
            if (m_StreamingVersion < CinemachineCore.kStreamingVersion)
                LegacyUpgrade(m_StreamingVersion);
            m_StreamingVersion = CinemachineCore.kStreamingVersion;
        }
        
        /// <summary>
        /// Override this to handle any upgrades necessitated by a streaming version change
        /// </summary>
        /// <param name="streamedVersion">The version that was streamed</param>
        internal protected virtual void LegacyUpgrade(int streamedVersion) {}
        internal virtual void OnBeforeSerialize() {}

        /// <summary>
        /// Temporarily cancel damping for this frame.  The camera will sanp to its target 
        /// position when it is updated.
        /// </summary>
        /// <param name="updateNow">If true, snap the camera to its target immediately, otherwise wait 
        /// until the end of the frame when cameras are normally updated.</param>
        public void CancelDamping(bool updateNow = false)
        {
            PreviousStateIsValid = false;
            if (updateNow)
            {
                var up = State.ReferenceUp;
                var brain = CinemachineCore.Instance.FindPotentialTargetBrain(this);
                if (brain != null)
                    up = brain.DefaultWorldUp;
                InternalUpdateCameraState(up, -1);
            }
        }
    }
}