using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

using UnityEngine;
using UnityEngine.Events;

using Photon.Pun;

using BR.BattleRoyale.Spells;
using UPG.Extensions;
using UPG.Debugging;

namespace BR.BattleRoyale.Items
{
    public class Wand : Item
    {
        #region EVENTS
        [HideInInspector] public UnityEvent OnChargeStart = new UnityEvent();
        [HideInInspector] public UnityEvent OnChargeEnded = new UnityEvent();
        [HideInInspector] public UnityEvent OnCast = new UnityEvent();
        [HideInInspector] public UnityEvent OnStartSustainedFire = new UnityEvent();
        [HideInInspector] public UnityEvent OnEndSustainedFire = new UnityEvent();
        [HideInInspector] public UnityEvent OnMinimumManaCharged = new UnityEvent();
        [HideInInspector] public UnityEvent OnMaximumManaCharged = new UnityEvent();
        [HideInInspector] public FloatEvent OnManaConsumed = new FloatEvent();
        public static FloatEvent OnLocalManaChargeUpdated = new FloatEvent();
        #endregion

        #region COMPONENTS
        public AudioSource AudioSource =>
            m_audioSource && m_audioSource.gameObject == gameObject ? m_audioSource : m_audioSource = GetComponent<AudioSource>();
        AudioSource m_audioSource;
        #endregion

        #region INSPECTOR FIELDS
        [Header("Customizable Settings")]
        [Tooltip("Name displayed on the item pickup menu. If left blank, the prefab name will be used.")]
        [SerializeField] [UnityEngine.Serialization.FormerlySerializedAs("DisplayName")] string m_displayName;
        [Tooltip("The rarity of the wand.")]
        [SerializeField] [UnityEngine.Serialization.FormerlySerializedAs("Rarity")] Rarity m_rarity;
        [Tooltip("Customizable Spell for the wand")]
        [SerializeField] SpellData m_spell;

        [Header("Wand FX")]
        [Tooltip("The wand's charge effect prefab.")]
        [SerializeField] GameObject m_chargeFX;
        [Tooltip("The wand's fire effect prefab.")]
        [SerializeField] GameObject m_fireFX;
        #endregion

        #region PUBLIC FIELDS
        public string DisplayName =>
            m_displayName;
        public Rarity Rarity =>
            m_rarity;
        public float ManaCharged
        {
            get => m_manaCharged;
            private set
            {
                if (value == m_manaCharged)
                    return;

                if (value > m_manaCharged)
                {
                    float _manaAdded = value - m_manaCharged;
                    float _priorValue = m_manaCharged;

                    m_manaCharged = value;

                    if (_priorValue < MinimumManaCost && m_manaCharged >= MinimumManaCost)
                        OnMinimumCharge();

                    if (_priorValue < MaximumManaCharge && m_manaCharged >= MaximumManaCharge)
                        OnMaximumCharge();
                }
                else
                {
                    m_manaCharged = value;
                }

                OnLocalManaChargeUpdated?.Invoke(Mathf.Clamp01(m_manaCharged / Owner.Mana.Value));
            }
        }
        public int CastNum { get; private set; }
        public float CastPower { get; private set; }
        public float CastSpeed { get; private set; }
        public float CastSize { get; private set; }
        public float CastDelay { get; private set; }
        #endregion

        #region PRIVATE FIELDS
        int m_spellIndex = 0;

        List<GameObject> m_activeSpellInstances;
        List<GameObject> m_cachedSpellInstances;
        Dictionary<GameObject, float> m_spellInstanceLifetimes;
        Dictionary<GameObject, float> m_spellInstanceDistances;

        bool m_firing = false;
        bool m_firstCast = true;
        bool m_canFire = true;

        float m_manaCharged = 0;

        AudioClip FireSound;
        #endregion

        #region READABLES
        public SpellData Spell => 
            m_spell;
        public SpellStats SpellStats =>
            Spell?.Stats;
        public GameObject SpellPrefab =>
            Spell?.SpellPrefab;

        bool IsValid =>
            m_spell && m_spell.Stats && m_spell.SpellPrefab;
        bool OwnerHasEnoughManaToCast =>
            Owner && (Owner.Mana.Value >= MinimumManaCost);
        bool Firing =>
            m_firing;
        float MinimumManaCost =>
            SpellStats.Graph_ManaCost.Evaluate((int)Rarity / (Enum.GetValues(typeof(Rarity)).Length - 1));

        bool HasChargeBehavior =>
            SpellStats.HoldFireBehavior == HoldFireBehavior.Charge;
        bool Charging => 
            co_charge != null;
        bool FullyCharged =>
            ManaCharged >= MaximumManaCharge;
        float ChargeRate =>
            GetCurvedStat(SpellStats.Graph_ChargeRate, SpellStats.SP_ChargeRate);
        float ManaChargedThisFrame
        {
            get
            {
                float _baseRate = ChargeRate * Time.deltaTime;

                float _manaRemainder = Owner.Mana.Value - ManaCharged;

                if (_manaRemainder < 0)
                {
                    m_debugChannel?.Raise(this,
                        new string[]
                        {
                            (_baseRate / 2).ToString() + " mana charged this frame",
                            "Charge Rate: " + ChargeRate
                        });

                    return _baseRate / 2;
                }
                
                float _manaDebt = _manaRemainder - _baseRate;

                float _manaCharged = _manaDebt >= 0 ? _baseRate : _manaRemainder + (_manaDebt / 2);

                m_debugChannel?.Raise(this,
                    new string[]
                    {
                            (_manaCharged).ToString() + " mana charged this frame",
                            "Charge Rate: " + ChargeRate
                    });

                return _manaCharged;
            }
        }
        float MaximumManaCharge
        {
            get
            {
                switch (SpellStats.ChargingBehavior)
                {
                    case (ChargingBehavior.FireAtMin):
                        return MinimumManaCost;
                    case (ChargingBehavior.HoldAtMin):
                        return MinimumManaCost;
                    case (ChargingBehavior.FireAtMax):
                        return SpellStats.MaxChargeValue > MinimumManaCost ? SpellStats.MaxChargeValue : MinimumManaCost;
                    case (ChargingBehavior.HoldAtMax):
                        return SpellStats.MaxChargeValue > MinimumManaCost ? SpellStats.MaxChargeValue : MinimumManaCost;
                    case (ChargingBehavior.Unbounded):
                        return Mathf.Infinity;
                    default:
                        return 0;
                }
            }
        }

        float Accuracy =>
            GetCurvedStat(SpellStats.Graph_Accuracy, SpellStats.SP_Accuracy);
        float RecastDelay =>
            GetCurvedStat(SpellStats.Graph_RecastDelay, SpellStats.SP_RecastDelay);
        float MultiCastDelaySeconds =>
            GetCurvedStat(SpellStats.Graph_MultiCastDelay, SpellStats.SP_MultiCastDelay);
        WaitForSeconds MultiCastDelay =>
            new WaitForSeconds(MultiCastDelaySeconds);
        bool SpellInstancesMaxed =>
            SpellStats.SpellInstanceLimit > 0 && m_activeSpellInstances.Count > SpellStats.SpellInstanceLimit && SpellStats.OverLimitBehavior == SpellOverLimitBehavior.NoCast;

        readonly Vector3 MagicOriginOffset = new Vector3(0.8f, 1.8f, 1.5f);
        Vector3 MagicOrigin =>
            Owner ?
            Owner.transform.position + Owner.transform.rotation * MagicOriginOffset : 
            (CameraController.Instance ?
            CameraController.Instance.transform.position + CameraController.Instance.transform.forward * 2 :
            transform.position + transform.up);
        Vector3 MagicDirection
        {
            get
            {
                if (Owner) 
                {
                    //m_debugChannel?.Raise(this, "Using Owner forward: " + Owner.transform.forward);
                    return Owner.transform.forward;
                }

                if (CameraController.Instance)
                {
                    //m_debugChannel?.Raise(this, "Using Camera forward: " + CameraController.Instance.transform.forward);
                    return CameraController.Instance.transform.forward;
                }

                //m_debugChannel?.Raise(this, "Using wand Up");
                return transform.up;
            }
        }
        #endregion

        #region DEFAULT METHODS
        new void OnValidate()
        {
            base.OnValidate();

            string _currentName = gameObject.name;
            
            _currentName = _currentName.Replace(" ", "_");

            foreach (char _char in Constants.InvalidCharacters)
                _currentName = _currentName.Replace(_char.ToString(), "");

            if (gameObject.name != _currentName)
                gameObject.name = _currentName;
        }
        new void Awake()
        {
            base.Awake();

            if (!Spell)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] does not have a spell.", DebugChannel.Severity.Error);

                Destroy(gameObject);

                return;
            }

            if (!SpellStats)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] spell does not have any stats.", DebugChannel.Severity.Error);

                Destroy(gameObject);

                return;
            }

            if (!SpellPrefab)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] spell does not have a prefab.", DebugChannel.Severity.Error);

                Destroy(gameObject);

                return;
            }

            FireSound = Spell.wandFire;
            m_canFire = true;

            m_activeSpellInstances = new List<GameObject>();
            m_cachedSpellInstances = new List<GameObject>();

            m_spellInstanceLifetimes = new Dictionary<GameObject, float>();
            m_spellInstanceDistances = new Dictionary<GameObject, float>();

            OnMaximumManaCharged = new UnityEvent();
            OnMinimumManaCharged = new UnityEvent();

            if (HasChargeBehavior)
            {
                switch (SpellStats.ChargingBehavior)
                {
                    case (ChargingBehavior.FireAtMax):
                        OnMaximumManaCharged.AddListener(ExecuteCast);
                        break;
                    case (ChargingBehavior.FireAtMin):
                        OnMinimumManaCharged.AddListener(ExecuteCast);
                        break;
                }
            }


            m_chargeFX = Instantiate(m_chargeFX, transform.position, transform.rotation, transform);

            if (m_chargeFX)
                m_chargeFX.transform.localScale = Vector3.one;

            m_fireFX = Instantiate(m_fireFX, transform.position, transform.rotation, transform);

            if (m_fireFX)
                m_fireFX.transform.localScale = Vector3.one;

            if (Application.platform != RuntimePlatform.WebGLPlayer)
                InitializeFX();
        }
        new void FixedUpdate()
        {
            base.FixedUpdate();

            if (Owner && transform.localPosition.magnitude != 0)
                transform.localPosition = Vector3.zero;
        }
        void OnDestroy()
        {
            m_debugChannel?.Raise(this, "Destroyed");
        }
        #endregion

        #region EDITOR METHODS
#if UNITY_EDITOR
        [ContextMenu("Save as Custom Wand")]
        void HandlePrefabSave()
        {
            m_debugChannel?.Raise(this, "Saving wand as prefab...");

            UnityEngine.Object _obj = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab", typeof(GameObject));

            if (_obj != null)
            {
                m_debugChannel?.Raise(this, "Found a prefab with the same name.", DebugChannel.Severity.Error);

                if (UnityEditor.EditorUtility.DisplayDialog("Overwrite Existing Wand", "A custom wand prefab with the name '" + gameObject.name + "' already exists. Would you like to overwrite the existing prefab, assign this wand a new name, or cancel?", "Overwrite", "Cancel"))
                {
                    UnityEditor.PrefabUtility.SaveAsPrefabAsset(gameObject, "Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab");

                    m_debugChannel?.Raise(this, "Prefab at Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab was successfully overwritten.");
                }
                else
                    m_debugChannel?.Raise(this, "Prefab overwrite cancelled.");
            }
            else
            {
                m_debugChannel?.Raise(this, "Creating a new wand prefab...");

                if (UnityEditor.PrefabUtility.SaveAsPrefabAsset(gameObject, "Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab"))
                    m_debugChannel?.Raise(this, "Wand prefab successfully created at Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab");
                else
                    m_debugChannel?.Raise(this, "Failed to create prefab! Did something go wrong?");
            }
        }
#endif
        #endregion

        #region INITIALIZE METHODS
        void InitializeFX()
        {
            if (m_chargeFX)
            {
                m_chargeFX = Instantiate(m_chargeFX, MagicOrigin, Quaternion.FromToRotation(m_chargeFX.transform.forward, MagicDirection));
                m_chargeFX.transform.parent = transform;

                if (m_chargeFX.TryGetComponent(out SpellFX _fx))
                {
                    OnChargeStart.AddListener(_fx.PlayAll);

                    OnChargeEnded.AddListener(_fx.StopAll);
                }
            }

            if (m_fireFX)
            {
                m_fireFX = Instantiate(m_fireFX, MagicOrigin, Quaternion.FromToRotation(m_chargeFX.transform.forward, MagicDirection));
                m_fireFX.transform.parent = transform;

                if (m_fireFX.TryGetComponent(out SpellFX _fx))
                    OnCast.AddListener(_fx.PlayAll);
            }
        }
        /// <summary>
        /// Sets the transforms of a wand to fit into the player's hand
        /// </summary>
        void SetTransformsToHand()
        {
            transform.localPosition = Vector3.zero;
            transform.localEulerAngles = new Vector3(90f, 0f, 0f);
        }
        /// <summary>
        /// Sets the active state of the wand's particle systems
        /// </summary>
        void SetParticleEffectsActive(bool _active)
        {
            for (int i = 0; i < transform.childCount; i++)
                transform.GetChild(i).gameObject.SetActive(_active);
        }
        #endregion

        #region STAT METHODS
        float GetCurvedStat(AnimationCurve _statCurve, StatScalingParam _param)
        {
            if (_param != StatScalingParam.NoScaling)
                return _statCurve.Evaluate(0);

            float _value = _statCurve.Evaluate(0);

            switch (_param)
            {
                case (StatScalingParam.Rarity):
                    _value = _statCurve.Evaluate((int)Rarity / (Enum.GetValues(typeof(Rarity)).Length - 1));
                    break;
                case (StatScalingParam.ManaCharged):
                    if (SpellStats.ChargingBehavior != ChargingBehavior.None && SpellStats.ChargingBehavior != ChargingBehavior.FireAtMin)
                        _value = _statCurve.Evaluate((int)ManaCharged / SpellStats.MaxChargeValue);
                    else
                        _value = _statCurve.Evaluate(SpellStats.Graph_ManaCost.Evaluate((int)Rarity / (Enum.GetValues(typeof(Rarity)).Length - 1)) / 100);
                    break;
            }

            return _value;
        }
        #endregion

        #region CACHE METHODS
        GameObject GetPrefabInstance()
        {
            if (m_activeSpellInstances == null)
                m_activeSpellInstances = new List<GameObject>();

            if (m_cachedSpellInstances == null)
                m_cachedSpellInstances = new List<GameObject>();

            if (photonView.ViewID == 32 || photonView.ViewID == 31 || photonView.ViewID == 29 || photonView.ViewID == 8 || photonView.ViewID == 1)
            {
                m_activeSpellInstances.Clear();
            }

            if (m_activeSpellInstances.Count >= SpellStats.SpellInstanceLimit && SpellStats.SpellInstanceLimit > 0)
            {
                switch (SpellStats.OverLimitBehavior)
                {
                    case (SpellOverLimitBehavior.NoCast):
                        m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] failed to retrieve a spell instance because we are over the limit.");
                        return null;
                    case (SpellOverLimitBehavior.CacheOldestInstance):
                        CacheSpellInstance(GetOldestSpellInstance());
                        break;
                }
            }

            GameObject _instance = GetCachedSpellInstance();

            if (_instance)
            {
                DecacheSpellInstance(_instance);

                _instance.transform.parent = Spell.Stats.SpellIsChild ? Owner?.transform : null;

                _instance.transform.position = MagicOrigin;
                _instance.transform.forward = MagicDirection;
                _instance.transform.localScale = Vector3.zero;

                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] retrieved a cached instance");

                return _instance;
            }

            _instance = Instantiate(SpellPrefab, MagicOrigin, SpellPrefab.transform.rotation);

            if (!_instance)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] failed to instantiate a spell instance.", DebugChannel.Severity.Error);

                return null;
            }

            _instance.transform.parent = null;

            _instance.transform.position = MagicOrigin;
            _instance.transform.forward = MagicDirection;
            _instance.transform.localScale = Vector3.zero;

            if (Spell.Stats.SpellIsChild)
                _instance.transform.parent = Owner?.transform;

            Spell _instanceSpellScript = _instance.GetComponent<Spell>();

            if (!_instanceSpellScript)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] instantiated a spell instance without a BaseSpell script attached.", DebugChannel.Severity.Error);

                Destroy(_instance);
                return null;
            }

            _instanceSpellScript.transform.name = photonView.ViewID.ToString() + "_" + m_spellIndex.ToString();

            m_spellIndex++;

            _instanceSpellScript.SetSourceWand(this);

            _instanceSpellScript.SetBaseColors(Spell.defaultColor1, Spell.defaultColor2, Spell.defaultColor3, Spell.defaultColor4);

            _instanceSpellScript.SpellData = Spell;

            _instanceSpellScript.Rarity = Rarity;

            m_activeSpellInstances.Add(_instance);

            if (m_spellInstanceLifetimes == null)
                m_spellInstanceLifetimes = new Dictionary<GameObject, float>();

            m_spellInstanceLifetimes.Add(_instance, 0);

            if (m_spellInstanceDistances == null)
                m_spellInstanceDistances = new Dictionary<GameObject, float>();

            m_spellInstanceDistances.Add(_instance, 0);

            return _instance;
        }
        GameObject GetOldestSpellInstance()
        {
            if (m_activeSpellInstances.Count == 0)
                return null;

            m_activeSpellInstances.Max(x => m_spellInstanceLifetimes[x]);

            GameObject _oldestInstance = m_activeSpellInstances[0];

            for (int i = 1; i < m_activeSpellInstances.Count; i++)
                if (m_spellInstanceLifetimes[m_activeSpellInstances[i]] > m_spellInstanceLifetimes[_oldestInstance])
                    _oldestInstance = m_activeSpellInstances[i];

            return _oldestInstance;
        }
        /// <summary>
        /// Returns a cached spell instance if one is available to use
        /// </summary>
        /// <returns></returns>
        GameObject GetCachedSpellInstance()
        {
            bool _cacheIsPopulated = m_cachedSpellInstances.Count > 0;

            GameObject _cachedSpell = !_cacheIsPopulated ? null : m_cachedSpellInstances[0];

            return _cachedSpell;
        }
        public void CacheSpellInstance(GameObject _instance)
        {
            // If we were passed a null reference, return
            if (!_instance)
                return;

            // Double-check to make sure this instance has a base spell
            Spell _baseSpell = _instance.GetComponent<Spell>();

            // Avoid running cache behaviors on non-spells
            // DEVELOPER NOTE - Seals will need a differentiation when implemented
            if (!_baseSpell)
                return;

            m_debugChannel?.Raise(this, "Caching Spell Instance");

            // Add the instance to the cache list
            if (!m_cachedSpellInstances.Contains(_instance))
                m_cachedSpellInstances.Add(_instance);

            // Reset the tracking values
            m_spellInstanceLifetimes[_instance] = 0;
            m_spellInstanceDistances[_instance] = 0;

            m_debugChannel?.Raise(this, "Lifetime/Distance values reset to " + m_spellInstanceLifetimes[_instance] + "/" + m_spellInstanceDistances[_instance]);

            _instance.transform.parent = transform;

            // Set the spell as inactive
            _instance.SetActive(false);
        }
        void DecacheSpellInstance(GameObject _instance)
        {
            if (!_instance)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID.ToString() + "] failed to decache because the instance is null.", DebugChannel.Severity.Error);

                return;
            }

            if (!m_activeSpellInstances.Contains(_instance))
                m_activeSpellInstances.Add(_instance);

            if (m_cachedSpellInstances.Contains(_instance))
                m_cachedSpellInstances.Remove(_instance);

            _instance.transform.parent = Spell.Stats.SpellIsChild ? Owner?.transform : null;

            _instance.SetActive(true);
        }
        #endregion

        #region MANA METHODS
        void StashMana(Spell _spell)
        {
            if (_spell)
                _spell.ManaCharge = ManaCharged;

            m_debugChannel?.Raise(this, "Stashed " + ManaCharged.ToString() + " Mana in Spell Instance");
        }
        #endregion

        #region ACTIVE SPELL METHODS
        void StartHandlingActiveSpells()
        {
            if (co_handleActiveSpells == null)
                co_handleActiveSpells = HandleActiveSpells();
            else
                StopCoroutine(co_handleActiveSpells);

            StartCoroutine(co_handleActiveSpells);
        }
        void StopHandlingActiveSpells()
        {
            if (co_handleActiveSpells == null)
                return;

            StopCoroutine(co_handleActiveSpells);

            co_handleActiveSpells = null;
        }
        void CullSpells()
        {
            m_activeSpellInstances.ForEach(CacheSpellInstance);

            m_activeSpellInstances.Clear();
        }
        #endregion

        #region COROUTINES
        IEnumerator co_waitToPreventFireMashing;
        IEnumerator WaitToPreventFireMashing()
        {
            m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID + "] is waiting to enable fire again.");

            m_canFire = false;

            yield return new WaitForSeconds(0.2f);

            m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID + "] can fire again.");

            m_canFire = true;

            co_waitToPreventFireMashing = null;

            yield break;
        }
        IEnumerator CastSpell()
        {
            while (!m_canFire)
                yield return null;

            //if (!m_firing)
            //{
            //    Debug.Log("Test2");
            //    yield break;
            //}

            if (SpellInstancesMaxed)
            {
                m_debugChannel?.Raise(this, 
                    "Wand [ID: " + photonView.ViewID + "] cannot fire because there are " + 
                    m_activeSpellInstances.Count.ToString() + " out of " +
                    SpellStats.SpellInstanceLimit.ToString() + " active spell instances.");

                yield break;
            }

            if (CastNum <= 0)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID + "] cannot fire because the number of spells cast is zero.");

                yield break;
            }

            if (SpellStats.CreatesSeal)
            {
                m_debugChannel?.Raise(this, 
                    new string[] { 
                        "Seals have not yet been implemented.", 
                        "Behavior ignored." 
                    }, 
                    DebugChannel.Severity.Warning);
            }

            if (m_firstCast && !HasChargeBehavior)
            {
                m_debugChannel?.Raise(this, 
                    "Wand [ID: " + photonView.ViewID + "] is winding up to cast.");

                m_firstCast = false;

                for (float t = 0; t < CastDelay && Firing; t += Time.deltaTime)
                    yield return null;

                if (!Firing)
                {
                    m_debugChannel?.Raise(this, 
                        "Wand [ID: " + photonView.ViewID + "] firing cancelled during windup.");

                    yield break;
                }
            }


            OnCast?.Invoke();

            float[] _xRots = new float[CastNum];
            float[] _yRots = new float[CastNum];
            int[] _targetActorNumbers = new int[CastNum];

            m_debugChannel?.Raise(this, 
                "Wand [ID: " + photonView.ViewID + "] is generating " + CastNum.ToString() + " new spellcast(s).");

            for (int i = 0; i < CastNum; i++)
            {
                _xRots[i] = Accuracy != 0 ? UnityEngine.Random.Range(-Accuracy, Accuracy) : 0;
                _yRots[i] = Accuracy != 0 ? UnityEngine.Random.Range(-Accuracy, Accuracy) : 0;

                _targetActorNumbers[i] = -1;
            }

            if (photonView && PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
                photonView.RPC(nameof(FireSpell), RpcTarget.Others, Owner.photonView.ViewID, _targetActorNumbers, MagicOrigin, MagicDirection, ManaCharged, _xRots, _yRots);
            
            for (int i = 0; i < CastNum; i++)
            {
                GameObject _spellInstance = GetPrefabInstance();

                if (!_spellInstance)
                {
                    m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID + "] cannot cast because there is no available spell instance.", DebugChannel.Severity.Error);

                    yield break;
                }

                if (!_spellInstance.TryGetComponent(out Spell _baseSpell))
                {
                    m_debugChannel?.Raise(this, 
                        "Wand [ID: " + photonView.ViewID + "] was unable to cast because the instance it retrieved was not a BaseSpell.", 
                        DebugChannel.Severity.Error);

                    yield break;
                }

                _baseSpell.Rigidbody.isKinematic = true;

                _baseSpell.Rigidbody.velocity = Vector3.zero;
                _spellInstance.transform.position = MagicOrigin;

                _spellInstance.transform.Rotate(_xRots[i], _yRots[i], 0);

                _baseSpell.Rigidbody.isKinematic = false;

                TryPlayFireSound();

                _baseSpell.ResetInstance();
                _baseSpell.Target = null;

                if (photonView && PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
                {
                    _baseSpell.SetTargetViewID(_targetActorNumbers[i]);
                }

                if (m_fireFX && m_fireFX.TryGetComponent(out SpellFX _fx))
                    _fx.PlayBurst();

                if (CastNum > 1 && MultiCastDelaySeconds > 0)
                {
                    m_debugChannel?.Raise(this, 
                        "Wand [ID: " + photonView.ViewID + "] must wait " + MultiCastDelaySeconds.ToString() + " to generate its next spell instance.");

                    yield return MultiCastDelay;
                }
            }

            if (!SpellStats.DrainByHolding)
            {
                ManaCharged = ManaCharged == 0 ? MinimumManaCost : ManaCharged;

                m_debugChannel?.Raise(this,
                    "Wand [ID: " + photonView.ViewID + "] firing with a mana cost of " + ManaCharged.ToString());
            
                OnManaConsumed?.Invoke(ManaCharged);
            }

            ManaCharged = 0;

            for (float t = 0; t < RecastDelay && Firing; t += Time.deltaTime)
                yield return null;

            if (Firing)
                switch (SpellStats.HoldFireBehavior)
                {
                    case (HoldFireBehavior.Repeat):

                        if (Owner.Mana.Value >= MinimumManaCost)
                            StartCoroutine(CastSpell());

                        yield break;

                    case (HoldFireBehavior.Charge):
                        StartCharge();
                        yield break;
                }

            co_charge = null;

            yield break;
        }
        IEnumerator RemoteCastSpell(int _ownerViewID, int[] _targetIDs, Vector3 _initialPosition, Vector3 _castDirection, float _manaCharged, float[] _xRots, float[] _yRots)
        {
            if (_xRots.Length == 0)
                yield break;

            ManaCharged = _manaCharged;

            for (int c = 0; c < _xRots.Length; c++)
            {
                GameObject _spellInstance = GetPrefabInstance();

                if (!_spellInstance)
                {
                    m_debugChannel?.Raise(this, "Failed to remote cast due to missing spell instance", DebugChannel.Severity.Error);

                    yield break;
                }

                Spell _baseSpellInstance = _spellInstance.GetComponent<Spell>();

                _baseSpellInstance.Rigidbody.isKinematic = true;
                _baseSpellInstance.Rigidbody.velocity = Vector3.zero;

                _spellInstance.transform.position = _initialPosition;

                _spellInstance.transform.LookAt(_spellInstance.transform.position + _castDirection, Vector3.up);

                _spellInstance.transform.Rotate(_xRots[c], _yRots[c], 0);

                _baseSpellInstance.Rigidbody.isKinematic = false;

                if (_targetIDs[c] != -1)
                    _baseSpellInstance.SetTargetViewID(_targetIDs[c]);

                StashMana(_baseSpellInstance);

                _baseSpellInstance.InitializeSpell(Spell);

                if (m_fireFX && m_fireFX.GetComponent<SpellFX>())
                    m_fireFX.GetComponent<SpellFX>().PlayBurst();

                yield return new WaitForSeconds(GetCurvedStat(SpellStats.Graph_MultiCastDelay, SpellStats.SP_MultiCastDelay));
            }

            ManaCharged = 0;

            yield return null;
        }
        IEnumerator DrainMana()
        {
            while (Firing && Owner.Mana > 0)
            {
                OnManaConsumed?.Invoke(MinimumManaCost * Time.fixedDeltaTime);

                yield return new WaitForFixedUpdate();
            }

            if (Firing && SpellStats.ReleaseOnManaEmpty)
            {
                if (photonView && PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
                    photonView.RPC(nameof(CacheAllSpells), RpcTarget.Others);

                StopCasting();
            }

            yield break;
        }
        IEnumerator co_handleActiveSpells;
        IEnumerator HandleActiveSpells()
        {
            while (true)
            {
                if (Owner.photonView.IsMine && Firing && SpellStats.ReleaseOnManaEmpty && Owner.Mana.Value <= Owner.Mana.MinimumValue)
                {
                    StopCasting();
                }

                if (m_activeSpellInstances.Count > 0)
                {
                    if (m_spellInstanceLifetimes == null)
                        m_spellInstanceLifetimes = new Dictionary<GameObject, float>();

                    if (m_spellInstanceDistances == null)
                        m_spellInstanceDistances = new Dictionary<GameObject, float>();

                    List<GameObject> _spellsToCache = new List<GameObject>();

                    foreach (GameObject _spellInstance in m_activeSpellInstances)
                    {
                        if (!_spellInstance)
                            continue;

                        if (!m_spellInstanceLifetimes.ContainsKey(_spellInstance))
                            m_spellInstanceLifetimes.Add(_spellInstance, 0);

                        if (!m_spellInstanceDistances.ContainsKey(_spellInstance))
                            m_spellInstanceDistances.Add(_spellInstance, 0);

                        m_spellInstanceLifetimes[_spellInstance] += Time.deltaTime;

                        if (_spellInstance.GetComponent<Rigidbody>())
                            m_spellInstanceDistances[_spellInstance] += Time.deltaTime * _spellInstance.GetComponent<Rigidbody>().velocity.magnitude;

                        bool _outOfTime = SpellStats.LimitLifespan && m_spellInstanceLifetimes[_spellInstance] >= GetCurvedStat(SpellStats.Graph_Lifetime, SpellStats.SP_Lifetime);
                        bool _outOfRange = SpellStats.LimitRange && m_spellInstanceDistances[_spellInstance] >= GetCurvedStat(SpellStats.Graph_Range, SpellStats.SP_Range);

                        if (_outOfTime || _outOfRange)
                            _spellsToCache.Add(_spellInstance);
                    }

                    if (_spellsToCache.Count > 0)
                        _spellsToCache.ForEach(CacheSpellInstance);
                }

                yield return null;
            }
        }
        #endregion

        #region CAST METHODS
        void ExecuteCast()
        {
            if (!Owner || !Owner.IsLocal)
            {
                m_debugChannel?.Raise(this, 
                    new string[]
                    {
                        "Received a cast call without an owner!",
                        "How did that happen?" 
                    }, 
                    DebugChannel.Severity.Warning);

                return;
            }

            if (m_chargeFX && m_chargeFX.TryGetComponent(out SpellFX _fx))
                _fx.StopAll();

            CastNum = SpellStats.SpellInstancesPerCast;

            m_debugChannel?.Raise(this, "Player [ID: " + Owner.photonView.ViewID.ToString() + "] started firing Wand [ID: " + photonView.ViewID + "]");

            if (SpellStats.HoldFireBehavior == HoldFireBehavior.Charge)
            {
                if (ManaCharged < MinimumManaCost)
                {
                    m_debugChannel?.Raise(this, "Wand[ID: " + photonView.ViewID + "] has insufficient charge to fire.");

                    return;
                }
                m_debugChannel?.Raise(this, "Wand[ID: " + photonView.ViewID + "] has sufficient charge to fire. Beginning cast coroutine.");

                StartCoroutine(CastSpell());

                return;
            }
            else if (Owner.Mana.Value >= MinimumManaCost)
            {
                m_debugChannel?.Raise(this, "Wand[ID: " + photonView.ViewID + "] is beginning the cast coroutine.");

                if (SpellStats.DrainByHolding)
                    StartCoroutine(DrainMana());

                StartCoroutine(CastSpell());
            }
            else
                m_debugChannel?.Raise(this, "Wand[ID: " + photonView.ViewID + "] cannot fire due to lack of mana.");
        }
        void StopCasting()
        {
            if (Charging)
            {
                if (ManaCharged >= MinimumManaCost)
                {
                    ExecuteCast();

                    m_debugChannel?.Raise(this, "Wand[ID: " + photonView.ViewID + "] will has sufficient charge to fire");
                }
                else
                {
                    m_debugChannel?.Raise(this, "Wand[ID: " + photonView.ViewID + "] has insufficient charge to fire");

                    StopCharge();
                }

                if (m_chargeFX)
                    m_chargeFX.GetComponent<SpellFX>()?.StopAll();
            }
            else
                m_debugChannel?.Raise(this, "Wand[ID: " + photonView.ViewID + "] was not charging");

            if (!SpellStats.CacheSpellsOnRelease)
                return;

            m_debugChannel?.Raise(this, "Caching " + m_activeSpellInstances.Count.ToString() + " active spell instances of Wand [ID: " + photonView.ViewID + "]");

            if (PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
                photonView?.RPC(nameof(CacheAllSpells), RpcTarget.Others);

            CacheAllSpells();
        }
        #endregion

        #region CHARGE METHODS
        void StartCharge()
        {
            if (co_charge != null)
                StopCoroutine(co_charge);

            StartCoroutine(co_charge = Charge());
        }
        void StopCharge()
        {
            if (!Charging)
                return;

            m_debugChannel?.Raise(this, "Stopping charging of Wand [ID: " + photonView.ViewID + "]");

            if (m_chargeFX && m_chargeFX.TryGetComponent(out SpellFX _fx))
                _fx.StopAll();

            if (co_charge != null)
            {
                StopCoroutine(co_charge);

                co_charge = null;
            }

            OnChargeEnded?.Invoke();

            ManaCharged = 0;
        }
        IEnumerator co_charge;
        IEnumerator Charge()
        {
            m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID + "] has begun charging.");

            if (m_chargeFX && m_chargeFX.TryGetComponent(out SpellFX _fx))
                _fx.PlayAll();

            OnChargeStart?.Invoke();

            for (ManaCharged = 0; !FullyCharged; ManaCharged += ManaChargedThisFrame)
                yield return null;

            if (SpellStats.ChargingBehavior != ChargingBehavior.HoldAtMax && SpellStats.ChargingBehavior != ChargingBehavior.HoldAtMin)
            {
                m_debugChannel?.Raise(this, "Wand [ID: " + photonView.ViewID + "] will end charging automatically.");

                co_charge = null;
            }

            yield break;
        }
        void OnMinimumCharge()
        {
            m_debugChannel?.Raise(this, 
                "Wand [ID: " + photonView.ViewID + "] has sufficent charge to fire.");

            switch (SpellStats.ChargingBehavior)
            {
                case (ChargingBehavior.FireAtMin):
                    m_debugChannel?.Raise(this, 
                        "Wand [ID: " + photonView.ViewID + "] will fire automatically.");

                    ManaCharged = MinimumManaCost;

                    ExecuteCast();
                    break;
                case (ChargingBehavior.HoldAtMin):
                    m_debugChannel?.Raise(this, 
                        "Wand [ID: " + photonView.ViewID + "] is holding its charge.");

                    ManaCharged = MinimumManaCost;

                    break;
            }

            OnMinimumManaCharged?.Invoke();
        }
        void OnMaximumCharge()
        {
            m_debugChannel?.Raise(this, 
                "Wand [ID: " + photonView.ViewID + "] has reached maximum charge of " + MaximumManaCharge.ToString());

            StopCharge();

            ManaCharged = MaximumManaCharge;

            switch (SpellStats.ChargingBehavior)
            {
                case ChargingBehavior.HoldAtMax:
                    m_debugChannel?.Raise(this, 
                        "Wand [ID: " + photonView.ViewID + "] is holding its charge.");
                    break;
                case ChargingBehavior.FireAtMax:
                default:
                    m_debugChannel?.Raise(this, 
                        "Wand [ID: " + photonView.ViewID + "] will fire automatically.");

                    ExecuteCast();
                    break;
            }

            OnMaximumManaCharged?.Invoke();
        }
        #endregion

        #region SFX METHODS
        void TryPlayFireSound()
        {
            if (!FireSound)
                return;

            if (!gameObject.TryGetComponent(out m_audioSource))
            {
                m_audioSource = gameObject.AddComponent<AudioSource>();
                m_audioSource.clip = FireSound;
                m_audioSource.loop = false;
            }

            AudioSource.Stop();
            AudioSource.Play();
        }
        #endregion

        #region RPC METHODS
        /// <summary>
        /// Instantiates a wand with the preset information
        /// </summary>
        /// <param name="_ownerViewID"></param>
        /// <param name="_spellViewIDs"></param>
        /// <param name="_prefabForward"></param>
        /// <param name="_initialPosition"></param>
        /// <param name="_castDirection"></param>
        /// <param name="_initialPower"></param>
        /// <param name="_initialSize"></param>
        /// <param name="_initialSpeed"></param>
        /// <param name="_accruementValue"></param>
        /// <param name="_xRots"></param>
        /// <param name="_yRots"></param>
        [PunRPC]
        public void FireSpell(int _ownerViewID, int[] _targetActorNumbers, Vector3 _initialPosition, Vector3 _castDirection, float _manaCharge, float[] _xRots, float[] _yRots)
        {
            // Return if the spell count is at or below zero
            if (_xRots.Length <= 0)
                return;

            // Set the wand's mana charge to the provided value
            ManaCharged = _manaCharge;

            // Start the remote cast coroutine
            StartCoroutine(RemoteCastSpell(_ownerViewID, _targetActorNumbers, _initialPosition, _castDirection, ManaCharged, _xRots, _yRots));
        }
        /// <summary>
        /// Handles caching spells for input-based caching
        /// </summary>
        [PunRPC]
        public void CacheAllSpells()
        {
            m_activeSpellInstances.ForEach(CacheSpellInstance);

            m_activeSpellInstances.Clear();
        }
        #endregion

        #region INPUT METHODS
        public override void StartPrimary()
        {
            if (!Owner || !Owner.IsLocal)
            {
                m_debugChannel?.Raise(this, "Wand has no owner");

                return;
            }

            if (!OwnerHasEnoughManaToCast)
            {
                m_debugChannel?.Raise(this, 
                    new string[]
                    {
                        "Owner does not have enough mana to cast",
                        "Requires: " + MinimumManaCost + ", Has: " + Owner.Mana.Value
                    });

                return;
            }

            m_firstCast = true;

            m_firing = true;

            m_debugChannel?.Raise(this, "Starting primary fire");

            switch (SpellStats.HoldFireBehavior)
            {
                case (HoldFireBehavior.Charge):
                    StartCharge();
                    break;
                case (HoldFireBehavior.Repeat):
                case (HoldFireBehavior.SingleCast):
                default:
                    ExecuteCast();
                    break;
            }
        }
        public override void EndPrimary()
        {
            m_debugChannel?.Raise(this, "Ending primary fire");

            //if (!(photonView.ViewID == 32 || photonView.ViewID == 29 || photonView.ViewID == 18 || photonView.ViewID == 1 || photonView.ViewID == 33))
            //{
            //    m_firing = false;
            //}
            


            StopCasting();
            m_firing = false;

            if (co_waitToPreventFireMashing != null)
                StopCoroutine(co_waitToPreventFireMashing);
            else
                co_waitToPreventFireMashing = WaitToPreventFireMashing();

            StartCoroutine(co_waitToPreventFireMashing);


        }
        public override void StartSecondary()
        {

        }
        public override void EndSecondary()
        {

        }
        #endregion
    }
}
