﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using Photon.Pun;
using BattleRoyale.Spells;
using System;

public enum Rarity
{
    Basic = 1,
    Normal = 2,
    Unique = 3,
    Super = 4, 
    Extraordinary = 5
}

public class NewWandScript : ItemBase, IPunObservable
{
    [BoolToButton("saveAsCustomWand")] public bool saveAsCustomWand = false;
    [BoolToButton("")] public bool emptyBool = false;

    [Header("Customizable Settings")]
    #region
    [Tooltip("Name displayed on the item pickup menu. If left blank, the prefab name will be used.")]
    public string DisplayName;
    [Tooltip("The rarity of the wand.")]
    public Rarity Rarity;
    [Tooltip("The spell type that the spell will fire. Uses a SpellCatalog to get spell data.")]
    public SpellType SpellType;
    [Tooltip("Changes the wand's type to a random type from the Spell Catalog if the catalog does not contain the preset type. Otherwise, the wand will be destroyed.")]
    public bool MutateIfInvalid = true;
    [Tooltip("Custom color for spell effects.")]
    public Color SpellColor1, SpellColor2, SpellColor3, SpellColor4;
    #endregion

    [Header("Spell Data")]
    #region
    [Tooltip("Name of the spell used in the pickup UI.")]
    [ReadOnly] public string SpellName;
    [Tooltip("Spell prefab that the wand will fire.")]
    [ReadOnly] public GameObject SpellPrefab;
    [Tooltip("The spell's stat preset.")]
    [ReadOnly] public SpellStats Stats;
    [Tooltip("The icon color used by the Pickup and Inventory UI.")]
    [ReadOnly] public Color IconColor;
    [Tooltip("The sound used when firing a wand.")]
    [ReadOnly] public AudioClip FireSound;
    [Tooltip("A list of all active spells that the wand has instantiated")]
    [ReadOnly] public List<GameObject> m_activeSpellInstances;
    [Tooltip("A list of all spells that the wand has cached")]
    [ReadOnly] public List<GameObject> m_cachedSpellInstances;
    #endregion

    [Header("Critical Components")]
    #region
    [Tooltip("The position used by the wand when instantiating spells.")]
    public Transform MagicOrigin;
    [Tooltip("The wand's display model seen during gameplay and in the inventory UI.")]
    public Transform Model;
    [Tooltip("The wand's charge effect prefab.")]
    public GameObject ChargeFX;
    [Tooltip("The wand's fire effect prefab.")]
    public GameObject FireFX;
    [Tooltip("The wand's audio source.")]
    public AudioSource aSource;
    #endregion

    [Header("Debug")]
    #region
    [Tooltip("Hides children of the wand to minimize user error.")]
	public bool hideChildObjects;
    public bool enableDebugMessages;
    #endregion

    // READONLY VALUES //
    #region
    public bool SpellIsSet { get; private set; }

    public bool Firing { get; private set; }
    public bool Charging { get; private set; }
    public bool NeedsMana { get; private set; }

    public float ManaCharged { get; private set; }
    public float MinimumManaCost { get; private set; }
    public float ChargeCeiling { get; private set; }
    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

    // PRIVATE VALUES //
    #region
    private int m_spellIndex = 0;
    private Dictionary<GameObject, float> m_spellInstanceLifetimes;
    private Dictionary<GameObject, float> m_spellInstanceDistances;
    private bool m_firstCast = true;
    private bool m_canCharge = true;
    private bool m_minCharge = false;
    private bool m_fullCharge = false;
    private IEnumerator m_coCharge;
    #endregion

    // DEFAULT METHODS //
    #region
    private void OnValidate()
    {
        GetComponent<Rigidbody>().useGravity = false;
        GetComponent<SphereCollider>().isTrigger = true;

        SpellColor1.a = 1;
        SpellColor2.a = 1;
        SpellColor3.a = 1;
        SpellColor4.a = 1;

        gameObject.name = gameObject.name.Replace(" ", "_");
    }
    void Start()
	{
		base.Awake();
    
        ValidateSpell();
        SetWandDefaults();
	}
    new void Update()
	{
        #if UNITY_EDITOR
        HandlePrefabSave();
        #endif

        // Run default ItemScript update method
        base.Update();

        // If we don't have a catalog, return. Nothing else will work without it.
        if (!SpellCatalog.GetCatalog())
            return;

        // If we have an invalid spell, attempt to validate it
        if (!HasValidSpell())
            ValidateSpell();

        // If we haven't successfully loaded a spell yet, return!
        if (!SpellIsSet)
            return;

        // By now, we should be sure that we have a valid spell
        // If the wand is equipped by a player but is not the active wand of the owner player
		if (isEquipped && OwnerPB.ActiveWand != gameObject)
            // Set this wand to the player wand
            OwnerPB.ActiveWand = gameObject;

        HandleActiveSpells();
    }
    private void FixedUpdate()
    {
        if (OwnerPB && !OwnerPB.IsNetworkPlayer())
            MagicOrigin.LookAt(MagicOrigin.position + OwnerPB.MagicRay.direction);

        if (OwnerPB && transform.parent == OwnerPB.RightHand && transform.position != OwnerPB.RightHand.position)
            transform.localPosition = Vector3.zero;
    }
    void OnTriggerEnter(Collider col)
	{
        // DEVELOPER NOTE - It would be good to work out a cleaner pickup system to avoid item overlapping confusion

        HandlePlayerEnter(col);
	}
	void OnTriggerStay(Collider col)
	{
        HandlePlayerStay(col);
	}
	void OnTriggerExit(Collider col)
	{
        HandlePlayerExit(col);
	}
    #endregion

    // HANDLER METHODS //
    #region
    #if UNITY_EDITOR
    private void HandlePrefabSave()
    {
        string _removeChars = "!@#$%^&*()_+-={}[]|;:'<>,.?/";

        foreach (char _char in _removeChars)
            gameObject.name = gameObject.name.Replace(_char, '_');

        if (saveAsCustomWand)
        {
            if (enableDebugMessages)
                Debug.Log("Saving wand as prefab...");

            UnityEngine.Object _obj = AssetDatabase.LoadAssetAtPath("Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab", typeof(GameObject));
            
            if (_obj != null)
            {
                Debug.Log("Found a prefab with the same name.");

                if (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"))
                {
                    PrefabUtility.SaveAsPrefabAsset(gameObject, "Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab");

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

                if (PrefabUtility.SaveAsPrefabAsset(gameObject, "Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab"))
                    Debug.Log("Wand prefab successfully created at Assets/Prefabs/Resources/Custom Wands/" + gameObject.name + ".prefab");
                else
                    Debug.LogError("Failed to create prefab! Did something go wrong?");
            }
        }

        saveAsCustomWand = false;
    }
    #endif
    /// <summary>
    /// Handles player entry behavior
    /// </summary>
    /// <param name="_col"></param>
    private void HandlePlayerEnter(Collider _col)
    {
        // If this collider is a player...
        if (_col.gameObject.tag == "Player")
        {
            // Get its PlayerBase script
            PlayerBase _pb = _col.GetComponent<PlayerBase>();

            // If we couldn't find one, return
            if (!_pb)
                return;

            // If this player is a network instance, return
            if (_pb.IsNetworkPlayer())
                return;

            // Otherwise, enable the item pickup UI for this wand
            _col.gameObject.GetComponent<PlayerBase>().ItemUI.AssignUINewWand(this);
        }
    }
    /// <summary>
    /// Handles player stay behavior
    /// </summary>
    /// <param name="_col"></param>
    private void HandlePlayerStay(Collider _col)
    {
        // If the collider is a player...
        if (_col.gameObject.tag == "Player")
        {
            // Get its PlayerBase script
            PlayerBase _pb = _col.GetComponent<PlayerBase>();

            // If we couldn't find one, return
            if (!_pb)
                return;

            // If this player is a network instance, return
            if (_pb.IsNetworkPlayer())
                return;

            // If the player presses the pickup key...
            if (Input.GetKeyDown(KeyCode.E))
            {
                // If this object is what is being shown on the display, null the display
                if (_pb.ItemUI.displayObj == Model.gameObject)
                    _pb.ItemUI.displayObj = null;

                // Initialize the wand data for the player
                // !!! - THIS SHOULD BE INTERNAL TO THE WAND - !!!
                HandlePickupByPlayer(_col.gameObject);

                // Assign this item to the item display UI
                ItemDisplays.itemDisplay.AssignWandItem(gameObject, gameObject.GetComponent<NewWandScript>());

                // If we are connected to a network...
                if (photonView && PhotonNetwork.IsConnected)
                {
                    // Run the PickUpWand method on all network instances of this wand
                    photonView.RPC("PickUpWand", RpcTarget.OthersBuffered, _pb.photonView.Owner.ActorNumber, photonView.ViewID);
                    // PhotonDebugger.IncrementNumberOfMessages("pickup wand");
                }
            }
        }
    }
    /// <summary>
    /// Handles player exit behavior
    /// </summary>
    /// <param name="_col"></param>
    private void HandlePlayerExit(Collider _col)
    {
        // If the collider is a player...
        if (_col.gameObject.tag == "Player")
        {
            // Get its PlayerBase script
            PlayerBase _pb = _col.GetComponent<PlayerBase>();

            // If we couldn't find one, return
            if (!_pb)
                return;

            // If this player is a network instance, return
            if (_pb.IsNetworkPlayer())
                return;

            // If the player's item display UI is currently showing this object, null it
            if (_pb.ItemUI.displayObj == Model.gameObject)
                _pb.ItemUI.displayObj = null;
        }
    }
    /// <summary>
    /// Handles pickup behavior by a given player gameobject
    /// </summary>
    /// <param name="_player"></param>
	private void HandlePickupByPlayer(GameObject _player)
    {
        // Set the owner to the given player
        myOwner = _player;

        // Get that player's PlayerBase script reference
        OwnerPB = myOwner.GetComponent<PlayerBase>();

        // Mark the wand as being in a player's inventory and equipped by default
        inInventory = true;
        isEquipped = true;

        // Disable the sphere collider to get rid of pickup behaviors and UI
        gameObject.GetComponent<SphereCollider>().enabled = false;

        // Zero out the velocities of the base item script
        rBody.velocity = Vector3.zero;
        
        // Make the wand a child of the owner's right hand bone
        transform.parent = OwnerPB.RightHand;

        // Sets the local transforms of the wand to "held" defaults
        SetTransformsToHand();
        
        // Enable particle effects
        SetParticleEffectsActive(true);

        // If the player is currently holding an item...
        if (OwnerPB.EquippedItem != null)
        {
            // And the player is on a network...
            if (photonView && PhotonNetwork.IsConnected)
            {
                // Make an RPC call to drop that item
                PhotonDebugger.IncrementNumberOfMessages("drop wand");
                photonView.RPC("DropItem", RpcTarget.OthersBuffered, OwnerPB.photonView.Owner.ActorNumber, (OwnerPB.EquippedItem.GetPhotonView()) ? OwnerPB.EquippedItem.GetPhotonView().ViewID : -1, 0f, Vector3.zero);
            }

            // Call the local player to drop that item
            OwnerPB.DropItem(OwnerPB.EquippedItem);
        }

        // Set the player's active item as this wand
        OwnerPB.SetActiveItem(gameObject);

        // If our owner is a network instance, simply add this to the inventory
        if (OwnerPB.photonView && !OwnerPB.photonView.IsMine && PhotonNetwork.IsConnected)
            OwnerPB.Inventory.Add(gameObject);
        // Otherwise, for local players, overwrite their active inventory slot pointer towards this gameobject
        else
            OwnerPB.Inventory[OwnerPB.activeItemNum] = gameObject;
    }
    /// <summary>
    /// Handles lifetime and range tracking behaviors 
    /// </summary>
    private void HandleActiveSpells()
    {
        // If we are set to release when our mana is empty and the owner is  currently empty, end the firing routine
        if (Firing && Stats.ReleaseOnManaEmpty && OwnerPB.photonView.IsMine && OwnerPB.Mana <= 0)
        {
            photonView.RPC("CacheAllSpells", RpcTarget.OthersBuffered);

            EndFireWand();
        }

        // If there are any active spells...
        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>();

            // Create a temporary list of spells to move to the cache
            List<GameObject> _spellsToCache = new List<GameObject>();

            // For each active spell instance...
            foreach (GameObject _spellInstance in m_activeSpellInstances)
            {
                // If the instance is null, continue
                if (!_spellInstance)
                    continue;
                
                // If for some reason our lifetimes dictionary doesn't contain a reference to this instance, add it
                if (!m_spellInstanceLifetimes.ContainsKey(_spellInstance))
                    m_spellInstanceLifetimes.Add(_spellInstance, 0);

                // If for some reason our lifetimes dictionary doesn't contain a reference to this instance, add it
                if (!m_spellInstanceDistances.ContainsKey(_spellInstance))
                    m_spellInstanceDistances.Add(_spellInstance, 0);

                // Increase the lifetime of the instance by the frame time
                m_spellInstanceLifetimes[_spellInstance] += Time.deltaTime;

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

                // Is the spell out of time or range?
                bool _outOfTime = Stats.LimitLifespan && m_spellInstanceLifetimes[_spellInstance] >= GetCurvedStat(Stats.Graph_Lifetime, Stats.SP_Lifetime);
                bool _outOfRange = Stats.LimitRange && m_spellInstanceDistances[_spellInstance] >= GetCurvedStat(Stats.Graph_Range, Stats.SP_Range);

                // If we are out of time or range, cache the spell
                if (_outOfTime || _outOfRange)
                    _spellsToCache.Add(_spellInstance);
            }
            
            // If we have at least one spell to cache, cache the list
            if (_spellsToCache.Count > 0)
                foreach (GameObject _spellInstance in _spellsToCache)
                    CacheSpellInstance(_spellInstance);
        }
    }
    #endregion

    // VALIDATE METHODS //
    #region
    /// <summary>
    /// Loads a spell from the spell catalog singleton.
    /// </summary>
    private void ValidateSpell()
    {
        // If we have a spell and its prefab/stats are valid, return
        if (SpellPrefab && Stats)
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] already has a prefab and stats and does not need to be validated.");

            return;
        }

        // If we don't have a catalog to pull from, return
        if (!SpellCatalog.GetCatalog())
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] failed to validate because there is no spell catalog in the scene.");

            return;
        }

        // If we don't have any spells in the catalog, return as well
        if (!SpellCatalog.GetCatalog().ContainsSpells())
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] failed to validate because the spell catalog does not contain any spells.");

            return;
        }

        // If the catalog contains a spell of the wand's type, return
        if (SpellCatalog.GetCatalog().ContainsSpellOfType(SpellType.Primary, SpellType.Secondary))
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] has a valid Spell Type.");

            // Get the spell of the wand's type from the catalog
            Spell _spell = SpellCatalog.GetCatalog().GetSpellOfType(SpellType.Primary, SpellType.Secondary);

            SpellName = _spell.Name == "" ? _spell.name : _spell.Name;
            SpellPrefab = _spell.SpellPrefab;
            Stats = _spell.Stats;
            IconColor = SpellCatalog.GetCatalog().IconColors.GetRarityColor(Rarity);
            FireSound = _spell.wandFire;
        }
        // Otherwise, if the catalog does not have the wand's spell type...
        else
        {
            // If we are set to randomize the wand to a valid type, do so
            if (MutateIfInvalid)
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] has an invalid type combination and will mutate to a valid type.");

                MutateSpellToRandomType();
            }
            // Otherwise, destroy the gameobject
            else
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] has an invalid type combination and will be destroyed.");
                
                Destroy(gameObject);
            }

            // And return!
            return;
        }

        // If we have everything we need, we're all set!
        if (SpellPrefab && Stats)
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] successfully validated.");

            SpellIsSet = true;

            return;
        }
    }
#endregion

    // SETTER METHODS //
    #region
    /// <summary>
    /// Sets up default wand values
    /// </summary>
    private void SetWandDefaults()
    {
        if (enableDebugMessages)
            Debug.Log("Setting Wand [ID: " + photonView.ViewID.ToString() + "] defaults.");

        SpellIsSet = false;
        Firing = false;

        myOwner = null;
        OwnerPB = null;

        inInventory = false;
        isEquipped = false;

        if (!Stats)
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] failed to initialize due to missing stats.");

            return;
        }

        // Get our minimum mana cost
        MinimumManaCost = Stats.Graph_ManaCost.Evaluate((int)Rarity / (Enum.GetValues(typeof(Rarity)).Length - 1));

        // If we have charging behavior...
        if (Stats.ChargingBehavior != ChargingBehavior.None)
        {
            if (enableDebugMessages)
                Debug.Log("Initializing Wand [ID: " + photonView.ViewID.ToString() + "] charging behaviors.");

            // Set the default maximum charge
            ChargeCeiling = 0;

            switch (Stats.ChargingBehavior)
            {
                case (ChargingBehavior.FireAtMin):
                    ChargeCeiling = MinimumManaCost;
                    break;
                case (ChargingBehavior.HoldAtMin):
                    ChargeCeiling = MinimumManaCost;
                    break;
                case (ChargingBehavior.FireAtMax):
                    ChargeCeiling = Stats.MaxChargeValue > MinimumManaCost ? Stats.MaxChargeValue : MinimumManaCost;
                    break;
                case (ChargingBehavior.HoldAtMax):
                    ChargeCeiling = Stats.MaxChargeValue > MinimumManaCost ? Stats.MaxChargeValue : MinimumManaCost;
                    break;
                case (ChargingBehavior.Unbounded):
                    break;
            }
        }

        if (enableDebugMessages)
            Debug.Log("Initializing Wand [ID: " + photonView.ViewID.ToString() + "] instance lists.");

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

#if UNITY_STANDALONE
        if (Application.platform != RuntimePlatform.WebGLPlayer)
            InitializeFX();
#endif

    }

    private void InitializeFX()
    {
        if (ChargeFX)
        {
            ChargeFX = Instantiate(ChargeFX, MagicOrigin.position, MagicOrigin.rotation);
            ChargeFX.transform.parent = MagicOrigin;
        }

        if (FireFX)
        {
            FireFX = Instantiate(FireFX, MagicOrigin.position, MagicOrigin.rotation);
            FireFX.transform.parent = MagicOrigin;
        }
    }

    /// <summary>
    /// Sets the transforms of a wand to fit into the player's hand
    /// </summary>
    private 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>
	private void SetParticleEffectsActive(bool _active)
	{
		for (int i = 0; i < transform.childCount; i++)
		{
			transform.GetChild(i).gameObject.SetActive(_active);
		}
	}
    /// <summary>
    /// Converts the wand's active spell to a random type from the catalog
    /// </summary>
    public void MutateSpellToRandomType()
    {
        // Get a random spell from the catalog
        Spell _spell = SpellCatalog.GetCatalog().GetRandomSpell();

        // If we couldn't get one, destroy the wand
        if (!_spell)
            Destroy(gameObject);

        // Override the wand type
        SpellType = _spell.Type;

        // Set our spell name, prefab, stats and icon color
        SpellName = _spell.Name == "" ? _spell.name : _spell.Name;
        SpellPrefab = _spell.SpellPrefab;
        Stats = _spell.Stats;
        IconColor = SpellCatalog.GetCatalog().IconColors.GetRarityColor(Rarity);
    }
#endregion

    // GETTER METHODS //
#region
    /// <summary>
    /// Returns true if we have a spell to use
    /// </summary>
    public bool HasValidSpell()
    {
        // If we have a loaded spell, return true
        if (SpellPrefab && Stats)
            return true;

        // Attempt to validate our spell
        ValidateSpell();
        
        // Return if we have a spell prefab and stats
        return SpellPrefab && Stats;
    }
    /// <summary>
    /// Returns the value of a given spell cast by its stat.
    /// </summary>
    /// <param name="_accuracy"></param>
    /// <returns></returns>
    private float GetCurvedStat(AnimationCurve _statCurve, StatScalingParam _param)
    {
        // If there is no scaling in the accuracy stat, return the base value
        if (_param != StatScalingParam.NoScaling)
            return _statCurve.Evaluate(0);

        // Create a value field at the base value by default
        float _value = _statCurve.Evaluate(0);
        
        // Differentiate our scaled behavior by the scaling parameter
        switch (_param)
        {
            // If we scale by rarity, get the stat by the wand's rarity divided by the number of rarities (minus one)
            case (StatScalingParam.Rarity):
                _value = _statCurve.Evaluate((int)Rarity / (Enum.GetValues(typeof(Rarity)).Length - 1));
                break;
            // If we scale by mana...
            case (StatScalingParam.ManaCharged):
                // If we have a charging behavior, use the amount of mana we have charged
                if (Stats.ChargingBehavior != ChargingBehavior.None && Stats.ChargingBehavior != ChargingBehavior.FireAtMin)
                    _value = _statCurve.Evaluate((int)ManaCharged / Stats.MaxChargeValue);
                // Otherwise, evaluate the mana cost value divided by the total possible mana pool
                else
                    _value = _statCurve.Evaluate(Stats.Graph_ManaCost.Evaluate((int)Rarity / (Enum.GetValues(typeof(Rarity)).Length - 1)) / 100);
                break;
        }

        // Return the accuracy
        return _value;
    }
    /// <summary>
    /// Returns either a spell instance to use in our firing behavior. If a cached instance is available, use that instead.
    /// </summary>
    /// <returns></returns>
    private GameObject GetPrefabInstance()
    {
        if (m_activeSpellInstances == null)
            m_activeSpellInstances = new List<GameObject>();

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

        // If we are trying to instantiate a spell past our max limit...
        if (m_activeSpellInstances.Count >= Stats.SpellInstanceLimit && Stats.SpellInstanceLimit > 0)
        {
            switch (Stats.OverLimitBehavior)
            {
                // If we prevent casting at the limit, return null
                case (SpellOverLimitBehavior.NoCast):
                    if (enableDebugMessages)
                        Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] failed to retrieve a spell instance because we are over the limit.");
                    return null;
                // Otherwise, we cache the oldest instance of a spell we have
                case (SpellOverLimitBehavior.CacheOldestInstance):
                    CacheSpellInstance(GetOldestSpellInstance());
                    break;
            }
        }

        // Try to get a cached spell instance
        GameObject _instance = GetCachedSpellInstance();

        // If we got one...
        if (_instance)
        {
            // Remove the instance from the cache and return it
            DecacheSpellInstance(_instance);

            // Size it to zero to avoid "blinking" behaviors
            _instance.transform.localScale = Vector3.zero;

            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] retrieved a cached instance");

            // Return the instance
            return _instance;
        }

        // If we didn't get a cached instance, instantiate a new one
        _instance = Instantiate(SpellPrefab, MagicOrigin.position, SpellPrefab.transform.rotation);

        // If it failed to instantiate, return null
        if (!_instance)
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] failed to instantiate a spell instance.");

            return null;
        }

        // Size it to zero to avoid "blinking" behaviors
        _instance.transform.localScale = Vector3.zero;

        // Get the instance's spell script...
        SpellBase _instanceSpellScript = _instance.GetComponent<SpellBase>();

        // If the instance is not a base spell, destroy the new instance and return
        if (!_instanceSpellScript)
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] instantiated a spell instance without a BaseSpell script attached.");

            Destroy(_instance);
            return null;
        }

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

        m_spellIndex++;

        // Set the spell's source reference to this wand
        _instanceSpellScript.SetSourceWand(this);

        // Set the spell colors to our wand's colors
        _instanceSpellScript.SetBaseColors(SpellColor1, SpellColor2, SpellColor3, SpellColor4);

        // Pass in the spell instance's stats
        _instanceSpellScript.Stats = Stats;

        // Same with its rarity
        _instanceSpellScript.Rarity = Rarity;

        // Set the spell to be a child of the wand if it is set to do so
        if (Stats.SpellIsChild)
            _instance.transform.parent = MagicOrigin;

        // Add the instance to our tracking lists/dictionaries
        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 the new instance
        return _instance;
    }
    /// <summary>
    /// Returns the oldest active spell in the scene
    /// </summary>
    /// <returns></returns>
    private GameObject GetOldestSpellInstance()
    {
        // If we don't have any active spells, return null
        if (m_activeSpellInstances.Count == 0)
            return null;

        // Get the first spell in our instance dictionary
        GameObject _oldestInstance = m_activeSpellInstances[0];

        // For each active spell, if its lifetime is greater than our stored instance's, make it the new oldest instance
        for (int i = 1; i < m_activeSpellInstances.Count; i++)
            if (m_spellInstanceLifetimes[m_activeSpellInstances[i]] > m_spellInstanceLifetimes[_oldestInstance])
                _oldestInstance = m_activeSpellInstances[i];

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

        GameObject _cachedSpell = !_cacheIsPopulated ? null : m_cachedSpellInstances[0];
        
        return _cachedSpell;
    }
#endregion
    
    // EXECUTABLE METHODS //
#region
    /// <summary>
    /// Called when a player begins firing a wand.
    /// </summary>
    public void FireWand()
    {
        // If we are already firing, return
        if (Firing)
            return;

        Firing = true;

        // Update the number of spells cast
        CastNum = Stats.SpellInstancesPerCast;

        if (enableDebugMessages)
            Debug.Log("Player [ID: " + OwnerPB.photonView.ViewID.ToString() + "] started firing Wand [ID: " + photonView.ViewID + "]");

        // Check if the spell has a charging behavior
        if (Stats.HoldFireBehavior == HoldFireBehavior.Charge)
        {
            if (enableDebugMessages)
                Debug.Log("Wand[ID: " + photonView.ViewID + "] has a charging behavior. Beginning charge coroutine.");

            // If so, set up and execute the coroutine.
            m_coCharge = ChargeSpell();
            StartCoroutine(m_coCharge);
            return;
        }
        else if (OwnerPB.Mana >= MinimumManaCost)
        {
            if (enableDebugMessages)
                Debug.Log("Wand[ID: " + photonView.ViewID + "] has sufficient charge to fire. Beginning cast coroutine.");

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

            StartCoroutine(CastSpell());
        }
        else if (enableDebugMessages)
            Debug.Log("Wand[ID: " + photonView.ViewID + "] has insufficient charge to fire.");
    }
    /// <summary>
    /// Called when a player's fire input ends. Include a true value to cull any charging behaviors without firing.
    /// </summary>
    /// <param name="_cancelSpellOnExit"></param>
    public void EndFireWand(bool _cullCharge = false)
    {
        // Set Firing state to false
        Firing = false;

        if (enableDebugMessages)
            Debug.Log("Player [ID: " + OwnerPB.photonView.ViewID.ToString() + "] stopped firing Wand [ID: " + photonView.ViewID + "]");

        // If we are culling an active culling procedure, handle it
        if (_cullCharge && Charging)
            CullCharge();

        if (ChargeFX && ChargeFX.GetComponent<SpellFX>())
            ChargeFX.GetComponent<SpellFX>().StopAll();

        // If our spell instantly caches all active spells on release, do so
        if (Stats.CacheSpellsOnRelease)
        {
            if (enableDebugMessages)
                Debug.Log("Caching " + m_activeSpellInstances.Count.ToString() + " active spell instances of Wand [ID: " + photonView.ViewID + "]");

            if (photonView && PhotonNetwork.IsConnected)
                photonView.RPC("CacheAllSpells", RpcTarget.OthersBuffered);

            // Move all instances to an array to prevent editing active lists
            GameObject[] _instances = m_activeSpellInstances.ToArray();
            
            // For each instance in the array, cache them
            foreach (GameObject _instance in _instances)
                CacheSpellInstance(_instance);
        }
    }
    /// <summary>
    /// Ends the charging coroutine and sets charging to false.
    /// </summary>
    private void CullCharge()
    {
        // If there is no coroutine to cull, return
        if (m_coCharge == null)
            return;

        if (enableDebugMessages)
            Debug.Log("Culling charging behaviors for Wand [ID: " + photonView.ViewID + "]");

        // Stop the coroutine
        StopCoroutine(m_coCharge);

        // Null the coroutine reference
        m_coCharge = null;

        // Reset charging state values
        m_canCharge = true;
        m_minCharge = false;
        m_fullCharge = false;
        Charging = false;

        // Reset mana charged values
        ManaCharged = 0;
    }
    /// <summary>
    /// Caches all active spells
    /// </summary>
    private void CullSpells()
    {
        foreach (GameObject _instance in m_activeSpellInstances)
            CacheSpellInstance(_instance);
    }
    /// <summary>
    /// Removes a spell from active play, but keeps it around if new instances are needed.
    /// </summary>
    /// <param name="_instance"></param>
    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
        SpellBase _baseSpell = _instance.GetComponent<SpellBase>();

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

        if (enableDebugMessages)
            Debug.Log("Caching Spell Instance");

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

        // Remove the instance from the active list
        if (m_activeSpellInstances.Contains(_instance))
            m_activeSpellInstances.Remove(_instance);

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

        if (enableDebugMessages)
            Debug.Log("Lifetime/Distance values reset to " + m_spellInstanceLifetimes[_instance] + "/" + m_spellInstanceDistances[_instance]);

        // Set the spell as inactive
        _instance.SetActive(false);
    }
    /// <summary>
    /// Returns a cached spell to active play.
    /// </summary>
    /// <param name="_instance"></param>
    private void DecacheSpellInstance(GameObject _instance)
    {
        // If we were passed a null reference, return
        if (!_instance)
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID.ToString() + "] failed to decache because the instance is null.");
            return;
        }
        
        // Add the instance to the active list
        if (!m_activeSpellInstances.Contains(_instance))
            m_activeSpellInstances.Add(_instance);

        // Remove the instance from the cached list
        if (m_cachedSpellInstances.Contains(_instance))
            m_cachedSpellInstances.Remove(_instance);
        
        // Set the spell as active
        _instance.SetActive(true);
    }
    /// <summary>
    /// Loads charged mana into the spell instance.
    /// </summary>
    /// <param name="_spell"></param>
    private void StashMana(SpellBase _spell)
    {
        // Set the spell mana to our charge
        if (_spell)
            _spell.ManaCharge = ManaCharged;

        if (enableDebugMessages)
            Debug.Log("Stashed " + ManaCharged.ToString() + " Mana in Spell Instance");
    }
#endregion

    // COROUTINES //
#region
    /// <summary>
    /// Handles persistent mana drain behaviors
    /// </summary>
    /// <returns></returns>
    private IEnumerator DrainMana()
    {
        // While the firing input is being held and we are above zero mana
        while (Firing && OwnerPB.Mana > 0)
        {
            // Expend the mana for this interval
            OwnerPB.ExpendMana(MinimumManaCost * Time.fixedDeltaTime);

            // Here we wait for the next physics update
            yield return new WaitForFixedUpdate();
        }

        // If we are still firing and we force release on empty mana, end the firing routine
        if (Firing && Stats.ReleaseOnManaEmpty)
        {
            if (photonView && PhotonNetwork.IsConnected)
                photonView.RPC("CacheAllSpells", RpcTarget.OthersBuffered);

            EndFireWand();
        }

        yield break;
    }
    /// <summary>
    /// Handles charging behavior
    /// </summary>
    /// <param name="_chargeBehavior"></param>
    /// <returns></returns>
    private IEnumerator ChargeSpell()
    {
        if (enableDebugMessages)
            Debug.Log("Wand [ID: " + photonView.ViewID + "] has begun charging.");

        if (ChargeFX && ChargeFX.GetComponent<SpellFX>())
            ChargeFX.GetComponent<SpellFX>().PlayAll();

        // While the fire input is being held...
        while (Firing)
        {
            // If we are able to accrue charge...
            if (m_canCharge)
            {
                // ACCRUE MANA THIS FRAME //
#region
                // Get the amount of mana per second that this wand gains and multiply it by the frame time
                float _manaChargedThisFrame = GetCurvedStat(Stats.Graph_ChargeRate, Stats.SP_ChargeRate) * Time.deltaTime;

                // Get the amount of mana our player has left to expend before we're maxed out
                float _manaRemainder = OwnerPB.Mana - ManaCharged;
            
                // If that amount is above zero...
                if (_manaRemainder >= 0)
                {
                    // Get the amount of mana we would be left with after expending the mana this frame
                    float _manaDebt = _manaRemainder - _manaChargedThisFrame;

                    // Increment the mana charge by the amount this frame. If it would go over into debt, add the remainder and half of the debt.
                    ManaCharged += _manaDebt >= 0 ? _manaChargedThisFrame : _manaRemainder + (_manaDebt / 2);
                }
                // If we are already in debt, just apply half the charge amount.
                else
                    ManaCharged += _manaChargedThisFrame / 2;

                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID + "] has a total charge of " + ManaCharged.ToString() + " Mana");

#endregion

                // CHECK FOR CHARGE THRESHOLDS //
#region
                // If we have just hit the minimum mana cost value...
                if (ManaCharged >= MinimumManaCost && !m_minCharge)
                {
                    if (enableDebugMessages)
                        Debug.Log("Wand [ID: " + photonView.ViewID + "] has sufficent charge to fire.");

                    // Mark that we have hit the minimum charge threshold
                    m_minCharge = true;

                    switch (Stats.ChargingBehavior)
                    {
                        // If we automatically fire at the minimum value...
                        case (ChargingBehavior.FireAtMin):

                            if (enableDebugMessages)
                                Debug.Log("Wand [ID: " + photonView.ViewID + "] will fire automatically.");

                            // Clamp the mana charge value
                            ManaCharged = MinimumManaCost;

                            // Reset our charge state values
                            m_canCharge = false;
                            m_minCharge = false;
                            m_fullCharge = false;

                            // Cast the spell
                            StartCoroutine(CastSpell());

                            yield break;

                        // If we simply hold the charge...
                        case (ChargingBehavior.HoldAtMin):

                            if (enableDebugMessages)
                                Debug.Log("Wand [ID: " + photonView.ViewID + "] is holding its charge.");

                            // Clamp the charge value
                            ManaCharged = MinimumManaCost;

                            // Set the charge state to wait for firing input to end
                            m_canCharge = false;
                            m_fullCharge = true;

                            break;
                    }
                }

                // If we have just hit the maximum charge value...
                if (ManaCharged >= ChargeCeiling && !m_fullCharge)
                {
                    if (enableDebugMessages)
                        Debug.Log("Wand [ID: " + photonView.ViewID + "] has reached maximum charge of " + ChargeCeiling.ToString());

                    m_fullCharge = true;

                    switch (Stats.ChargingBehavior)
                    {
                        // If we automatically fire at the minimum value...
                        case (ChargingBehavior.FireAtMax):

                            if (enableDebugMessages)
                                Debug.Log("Wand [ID: " + photonView.ViewID + "] will fire automatically.");

                            // Clamp the mana charge value
                            ManaCharged = ChargeCeiling;

                            // Reset our charge state values
                            m_canCharge = false;
                            m_minCharge = false;
                            m_fullCharge = false;

                            // Cast the spell
                            StartCoroutine(CastSpell());

                            yield break;

                        // If we simply hold the charge...
                        case (ChargingBehavior.HoldAtMax):

                            if (enableDebugMessages)
                                Debug.Log("Wand [ID: " + photonView.ViewID + "] is holding its charge.");

                            // Clamp the charge value
                            ManaCharged = ChargeCeiling;

                            // Set the charge state to wait for firing input to end
                            m_canCharge = false;
                            m_fullCharge = true;

                            break;
                    }
                }
#endregion
            }

            // HOLD BEHAVIOR //
#region
            // If we are at full charge, hold the full charge
            if (m_fullCharge)
                while (Firing)
                    yield return null;
            // Otherwise, just wait until the next frame
            else
                yield return null;
#endregion
        }

        if (enableDebugMessages)
            Debug.Log("Wand [ID: " + photonView.ViewID + "] has stopped charging due to input release.");

        // FIRE BEHAVIOR //
#region
        // If we have the minimum required amount of mana, fire
        if (m_minCharge)
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID + "] has sufficient charge to fire.");

            StartCoroutine(CastSpell());
        }
        // Otherwise, reset the charge value
        else
        {
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID + "] has insufficient charge to fire.");

            ManaCharged = 0;
        }

        // There is no longer a charge behavior to cull, so null the coroutine reference
        m_coCharge = null;

        // Reset our charge state tracking
        m_canCharge = true;
        m_minCharge = false;
        m_fullCharge = false;

        if (ChargeFX && ChargeFX.GetComponent<SpellFX>() && !Firing)
            ChargeFX.GetComponent<SpellFX>().StopAll();
#endregion

        yield break;
    }
    /// <summary>
    /// Handles spellcasting behavior
    /// </summary>
    /// <returns></returns>
    private IEnumerator CastSpell()
    {
        // ABORT CIRCUMSTANCES //
#region
            // Abort the cast if we are unable to cast due to our instance limit behavior
            if (Stats.SpellInstanceLimit > 0 && m_activeSpellInstances.Count > Stats.SpellInstanceLimit && Stats.OverLimitBehavior == SpellOverLimitBehavior.NoCast)
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID + "] cannot fire because there are " + m_activeSpellInstances.Count.ToString() + " out of " + Stats.SpellInstanceLimit.ToString() + " active spell instances.");

                yield break;
            }

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

                yield break;
            }
#endregion

        // UPDATE STATES //
#region
            // Charging is disabled while we are still firing
            m_canCharge = false;

            // We create a temporary state here to break repeat casts if we detect that we released fire at any point
            bool _held = true;

            float _delay = 0;
#endregion

        // GENERATE SPELL SEAL //
#region
        if (Stats.CreatesSeal)
            Debug.Log("Seal time!");
#endregion

        // WAIT FOR WINDUP //
#region
            // If this is the first cast after a new fire input and we did not charge beforehand...
            if (m_firstCast && Stats.HoldFireBehavior != HoldFireBehavior.Charge)
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID + "] is winding up to cast.");

                // Toggle the first cast state
                m_firstCast = false;

                _delay = GetCurvedStat(Stats.Graph_WindupDelay, Stats.SP_WindupDelay);

                while (_delay > 0)
                {
                    _delay -= Time.deltaTime;

                    if (!Firing && _held)
                        _held = false;

                    yield return null;
                }

                // If we aren't holding the fire input, cancel the spell cast
                if (!_held)
                {
                    if (enableDebugMessages)
                        Debug.Log("Wand [ID: " + photonView.ViewID + "] has aborted firing because it stopped firing before the windup was complete.");

                    yield break;
                }
            }
#endregion

        // GENERATE CAST DATA //
#region
            // Create an array of target IDs
            int[] _targetIDs = new int[CastNum];

            // Get the point at which the spell should appear
            Vector3 _castPoint = MagicOrigin.position;

            // Get the direction the camera is pointing in
            Vector3 _castDir = OwnerPB.MagicRay.direction;

            // Get the spell's accuracy stat
            float _accuracy = GetCurvedStat(Stats.Graph_Accuracy, Stats.SP_Accuracy);
        
            // Create an array of rotation indices for our spell instances
            float[] _xRots = new float[CastNum];
            float[] _yRots = new float[CastNum];

            // Get the multicast delay
            float _multiCastDelay = GetCurvedStat(Stats.Graph_MultiCastDelay, Stats.SP_MultiCastDelay);
        
            if (enableDebugMessages)
                Debug.Log("Wand [ID: " + photonView.ViewID + "] is generating " + CastNum.ToString() + " new spellcast(s).");

            // We will instantiate a number of spells equal to the CastNum integer
            for (int i = 0; i < CastNum; i++)
            {
                // If we have an accuracy other than 0...
                if (_accuracy != 0)
                {
                    // Add random rotations to the rotation indices to the pitch and yaw based on our accuracy
                    _xRots[i] = UnityEngine.Random.Range(-_accuracy, _accuracy);
                    _yRots[i] = UnityEngine.Random.Range(-_accuracy, _accuracy);

                }
                // Otherwise, set the rotation indices to 0
                else
                {
                    _xRots[i] = 0;
                    _yRots[i] = 0;
                }
            
                // For now we're setting target IDs to -1 for no target
                _targetIDs[i] = -1;
            }
#endregion

        // MAKE RPC CALL //
#region
        // Only make an RPC call if we are on a network
        // We do this now because multi-casts across networks should START at the same time
        if (photonView && PhotonNetwork.IsConnected)
            photonView.RPC("FireSpell", RpcTarget.OthersBuffered, OwnerPB.photonView.ViewID, _targetIDs, _castPoint, _castDir, ManaCharged, _xRots, _yRots);
#endregion

        // EXECUTE LOCAL CAST //
#region
        for (int i = 0; i < CastNum; i++)
        {
            // INSTANTIATE A SPELL PREFAB //
#region
            // Get a spell instance from our cache or prefab
            GameObject _spellInstance = GetPrefabInstance();

            // If we didn't get a spell instance, break out of the cast routine.
            if (!_spellInstance)
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID + "] cannot cast because there is no available spell instance.");

                yield break;
            }
            else
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID + "] will use Spell " + _spellInstance.name + " for this cast.");
            }

            // Set the instance's position to the cast point
            _spellInstance.transform.position = _castPoint;
            
            // Rotate the instance to face towards player's magic direction
            _spellInstance.transform.LookAt(_castPoint + _castDir, Vector3.up);
            
            // Apply them to our prefab's rotation
            _spellInstance.transform.Rotate(_xRots[i], _yRots[i], 0);

            if (FireSound)
            {
                if (!GetComponent<AudioSource>())
                {
                    gameObject.AddComponent<AudioSource>();
                }

                if (!aSource)
                {
                    aSource = GetComponent<AudioSource>();
                    aSource.clip = FireSound;
                    aSource.loop = false;
                }

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

            // WRITE CAST DATA TO SPELL //
#region
            // Get the instance's Base Spell component
            SpellBase _baseSpell = _spellInstance.GetComponent<SpellBase>();

            // If we got no component, break out
            if (!_baseSpell)
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID + "] was unable to cast because the instance it retrieved was not a BaseSpell.");

                yield break;
            }

            // Set the spell script's values to the base values
            _baseSpell.ResetInstance();
            
            // Add targetting behaviors later.
            _baseSpell.Target = null;

            // Apply photon-relevent data if applicable
            if (photonView && PhotonNetwork.IsConnected)
            {
                _baseSpell.SetTargetViewID(_targetIDs[i]);
                _baseSpell.SetOwnerViewID(photonView.ViewID);
            }

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

#endregion


            if (CastNum > 1 && _multiCastDelay > 0)
            {
                if (enableDebugMessages)
                    Debug.Log("Wand [ID: " + photonView.ViewID + "] must wait " + _multiCastDelay.ToString() + " to generate its next spell instance.");

                // Wait for the multicast delay to pass
                yield return new WaitForSeconds(_multiCastDelay);
            }
        }
#endregion

        // UPDATE STATE VALUES //
#region
        // Enable charging again.
        m_canCharge = true;
        m_fullCharge = false;

        // Update held state
        if (!Firing && _held)
            _held = false;
#endregion

        // DEPLETE MANA AT END OF CAST //
#region
        // If we are not consuming mana by holding the fire input
        if (!Stats.DrainByHolding)
        {
            ManaCharged = ManaCharged == 0 ? MinimumManaCost : ManaCharged;

            if (enableDebugMessages)
                Debug.Log("Wand firing with a mana cost of " + ManaCharged.ToString());

            // Deplete the mana charge from the player base if we have a base AND we are not a networked player
            if (OwnerPB && (PhotonNetwork.IsConnected ? !OwnerPB.IsNetworkPlayer() : true))
                OwnerPB.ExpendMana(ManaCharged);
        }

        // Reset the amount of charged mana
        ManaCharged = 0;
#endregion

        // RECAST DELAY //
#region
            _delay = GetCurvedStat(Stats.Graph_RecastDelay, Stats.SP_RecastDelay);

            while (_delay > 0)
            {
                _delay -= Time.deltaTime;

                if (!Firing && _held)
                    _held = false;

                yield return null;
            }
#endregion

        // EXIT FIRE BEHAVIORS //
#region
            // If we are still holding the fire input...
            if (_held)
            {
                switch (Stats.HoldFireBehavior)
                {
                    // If we are set to repeat...
                    case (HoldFireBehavior.Repeat):

                        // If we have enough mana to recast, do so and break the coroutine
                        if (OwnerPB.Mana >= MinimumManaCost)
                            StartCoroutine(CastSpell());

                        // Regardless, end the current coroutine
                        yield break;

                    // If we are set to charge, start charging again
                    // This only occurs in the case of FireAt charging behaviors
                    case (HoldFireBehavior.Charge):

                        // Set the coroutine reference to the new charge
                        m_coCharge = ChargeSpell();

                        // Kick it off!
                        StartCoroutine(m_coCharge);

                        // End the coroutine
                        yield break;
                }
            }

            // If we are not using charging behaviors or repeating, simply empty the charge coroutine reference and wrap things up
            m_coCharge = null;
#endregion

        yield break;
    }
    /// <summary>
    /// Handles spellcasting behavior from network instances
    /// </summary>
    /// <param name="_ownerViewID"></param>
    /// <param name="_spellViewIDs"></param>
    /// <param name="_targetIDs"></param>
    /// <param name="_initialPosition"></param>
    /// <param name="_castDirection"></param>
    /// <param name="_xRots"></param>
    /// <param name="_yRots"></param>
    /// <param name="_multiCastDelay"></param>
    /// <returns></returns>
    private IEnumerator RemoteCastSpell(int _ownerViewID, int[] _targetIDs, Vector3 _initialPosition, Vector3 _castDirection, float _manaCharged, float[] _xRots, float[] _yRots)
    {
        // ABORT CIRCUMSTANCES //
#region
        // Avoid empty casts
        if (_xRots.Length == 0)
            yield break;
#endregion

        // APPLY REMOTE MANA CHARGE //
#region
        // Assign our remote mana charge to the wand's charge value
        ManaCharged = _manaCharged;
#endregion
        
        // For each spell...
        for (int c = 0; c < _xRots.Length; c++)
        {
            // GET A SPELL PREFAB //
#region
            // Instantiate at the given position and rotation
            GameObject _spellInstance = GetPrefabInstance();

            // If we didn't get an instance, break
            if (!_spellInstance)
            {
                if (enableDebugMessages)
                    Debug.Log("Failed to remote cast due to missing spell instance");

                yield break;
            }

            // Set the instance's position to the initial position
            _spellInstance.transform.position = _initialPosition;
            
            // Rotate the instance to face towards cast direction
            _spellInstance.transform.LookAt(_spellInstance.transform.position + _castDirection, Vector3.up);

            // Rotate the instance by the given rotations
            _spellInstance.transform.Rotate(_xRots[c], _yRots[c], 0);
#endregion

            // WRITE CAST DATA TO SPELL //
#region
            // Get the Base Spell component of the spell instance
            SpellBase _baseSpellInstance = _spellInstance.GetComponent<SpellBase>();

            // Set the Owner ID
            _baseSpellInstance.SetOwnerViewID(_ownerViewID);
            
            // If we received a valid target ID for this spell, set its target
            if (_targetIDs[c] != -1)
                _baseSpellInstance.SetTargetViewID(_targetIDs[c]);

            // Stash our mana into the spell
            StashMana(_baseSpellInstance);

            // Initialize the spell and let it fly!
            _baseSpellInstance.InitializeSpell();
#endregion

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

            // Wait for the multi-cast delay to initialize the new spell
            yield return new WaitForSeconds(GetCurvedStat(Stats.Graph_MultiCastDelay, Stats.SP_MultiCastDelay));
        }

        // RESET MANA CHARGED //
#region
        // Reset the amount of charged mana
        ManaCharged = 0;
#endregion

        yield return null;
    }
#endregion

    // RPC METHODS //
#region
    /// <summary>
    /// Handles wand pickup behaviors and calls it across the photon network
    /// </summary>
    /// <param name="_ownerViewID"></param>
    /// <param name="_wandViewID"></param>
    [PunRPC] public void PickUpWand(int _ownerViewID, int _wandViewID)
	{
        // If this wand does not match the given wand ID, ignore
        // DEVELOPER NOTE - Why would this wand be recieving another wand's ID?
		if (photonView.ViewID != _wandViewID)
			return;

        // Get the owner player GameObject by their ID
        GameObject _owner = PlayerBase.GetPlayerByID(_ownerViewID);

        // If one is found, handle pickup behaviors
        if (_owner)
            HandlePickupByPlayer(_owner);
	}
    /// <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[] _targetIDs, Vector3 _initialPosition, Vector3 _castDirection, float _manaCharge, float[] _xRots, float[] _yRots)
    {
        // If there is no spell catalog in the scene, return
        if (!SpellCatalog.GetCatalog())
            return;

        // If we do not have a spell catalog...
        if (!SpellCatalog.GetCatalog())
            return;
        
        // 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, _targetIDs, _initialPosition, _castDirection, ManaCharged, _xRots, _yRots));
    }
    /// <summary>
    /// Handles caching spells for input-based caching
    /// </summary>
    [PunRPC] public void CacheAllSpells()
    {
        // Move all instances to an array to prevent editing active lists
        GameObject[] _instances = m_activeSpellInstances.ToArray();

        // For each instance in the array, cache them
        foreach (GameObject _instance in _instances)
            CacheSpellInstance(_instance);
    }
    /// <summary>
    /// Handles networked item drop calls
    /// </summary>
    /// <param name="_wandViewID"></param>
    [PunRPC] public void DropWand(int _wandViewID)
    {
        if (photonView.ViewID != _wandViewID)
            return;

        HandleDrop();
    }
#endregion

    // PHOTON SERIALIZERS //
#region
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            stream.SendNext(transform.localPosition);
            stream.SendNext(MagicOrigin.localRotation.eulerAngles);
        }
        
        if (stream.IsReading)
        {
            transform.localPosition = (Vector3)stream.ReceiveNext();
            MagicOrigin.localRotation = Quaternion.Euler((Vector3)stream.ReceiveNext());
        }
    }
#endregion
}

// DEVELOPER NOTE - At some point, this method should be the basis for random wand spawns
/*
/// <summary>
/// Generates a random wand from available rarities, types and colors.
/// </summary>
/// <returns></returns>
public WandData RandomWand()
{
    // if we have no catalog, return a default WandData
    if (!SpellCatalog.GetCatalog())
        return default(WandData);

    // Create a temporary WandData field
    WandData _randomWand = new WandData();

    // Get a random rarity and valid type from the catalog
    _randomWand.Rarity = (Rarity)UnityEngine.Random.Range(0, Enum.GetValues(typeof(Rarity)).Length - 1);
    SpellType _randomType = SpellCatalog.GetCatalog().GetRandomValidType();
    _randomWand.PrimaryType = _randomType.Primary;
    _randomWand.SecondaryType = _randomType.Secondary;

    // Get a spell from the catalog based on that type
    Spell _randomSpell = SpellCatalog.GetCatalog().GetSpellOfType(_randomType);

    // If we got a null spell, it means that the catalog is empty, so return a default WandData
    if (!_randomSpell)
        return default(WandData);

    // Get the name and prefab of the spell
    _randomWand.SpellName = _randomSpell.Name == "" ? _randomSpell.SpellPrefab.name : _randomSpell.Name;
    _randomWand.SpellPrefab = _randomSpell.SpellPrefab;

    // Get the icon color according to the catalog's rarity settings
    _randomWand.iconColor = SpellCatalog.GetCatalog().IconColors.GetRarityColor(_randomWand.Rarity);

    // Generate random color values for the spell primary and secondary colors
    _randomWand.SpellColor1 = new Color(UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1));
    _randomWand.SpellColor2 = new Color(UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1));
    _randomWand.SpellColor3 = new Color(UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1));
    _randomWand.SpellColor4 = new Color(UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1), UnityEngine.Random.Range(0.5f, 1));

    // Return the randomized wand!
    return _randomWand;
}
*/


