using System;
using System.Collections.Generic;

using UnityEngine;
using UnityEngine.Events;

using Photon.Pun;

using BR.BattleRoyale.Items;
using UPG.Extensions;
using UPG.Debugging;
using BR.BattleRoyale.Building;

#region CLASS EVENTS
namespace UnityEngine.Events
{
    public class SpellEvent : UnityEvent<BR.BattleRoyale.Spells.Spell> { };
    public class SpellHitEvent : UnityEvent<BR.BattleRoyale.Spells.SpellHitResult> { };
}
#endregion

namespace BR.BattleRoyale.Spells
{
    [RequireComponent(typeof(Rigidbody))]
    public class Spell : MonoBehaviour
    {
        #region EVENTS
        public static SpellEvent OnSpellInstantiated = new SpellEvent();
        public SpellHitEvent OnSpellHit = new SpellHitEvent();
        #endregion

        #region COMPONENTS
        Rigidbody m_rigidbody;
        public Rigidbody Rigidbody =>
            m_rigidbody ?? (m_rigidbody = GetComponent<Rigidbody>());
        #endregion

        #region INSPECTOR FIELDS
        [Header("Photon IDs")]
        [Tooltip("The wand that cast this spell")]
        [ReadOnly] [SerializeField] Wand m_sourceWand;

        [Header("Spell Stats")]
        [Tooltip("The name of the spell inherited from the catalog spell stats.")]
        [ReadOnly] public string SpellName;
        [Tooltip("The primary type of the spell")]
        public SpellPrimaryType PrimaryType;
        [Tooltip("The secondary type of the spell")]
        public SpellSecondaryType SecondaryType;
        [Tooltip("The secondary type of the spell")]
        [ReadOnly] public Rarity Rarity;
        [Tooltip("The stats inherited on cast")]
        [ReadOnly] public SpellData SpellData;

        [Header("Spell Values")]
        [Tooltip("The current power of the spell")]
        [ReadOnly] public float Damage;
        [Tooltip("The current pushing power of the spell")]
        [ReadOnly] public float PushForce;
        [Tooltip("The current speed of the spell")]
        [ReadOnly] public float Speed;
        [Tooltip("The current size of the spell")]
        [ReadOnly] public float Size;
        [Tooltip("The current target of the spell")]
        [ReadOnly] public GameObject Target;
        [Tooltip("The degrees per second that the spell will change directions to point towards the target")]
        [ReadOnly] public float TrackingRate;
        [Tooltip("The seconds elapsed since the spell was cast")]
        [ReadOnly] public float FlightTime;
        [Tooltip("Cumulative distance travelled since the spell was cast.")]
        [ReadOnly] public float DistanceTravelled;
        [Tooltip("Amount of mana expended when the spell was fired")]
        [ReadOnly] public float ManaCharge;

        [Header("Spell Colors")]
        [ReadOnly] [SerializeField] Color SpellColor1;
        [ReadOnly] [SerializeField] Color SpellColor2;
        [ReadOnly] [SerializeField] Color SpellColor3;
        [ReadOnly] [SerializeField] Color SpellColor4;

        [Header("Spell Audio")]
        [SerializeField] AdjustableAudioClip m_castSound;
        [SerializeField] AdjustableAudioClip m_travelSound;

        [Header("Renderers")]
        [Tooltip("Components that will change color to match the first color of the spell")]
        [SerializeField] Renderer[] Color1Renderers = new Renderer[] { };
        [Tooltip("Components that will change color to match the second color of the spell")]
        [SerializeField] Renderer[] Color2Renderers = new Renderer[] { };
        [Tooltip("Components that will change color to match the third color of the spell")]
        [SerializeField] Renderer[] Color3Renderers = new Renderer[] { };
        [Tooltip("Components that will change color to match the fourth color of the spell")]
        [SerializeField] Renderer[] Color4Renderers = new Renderer[] { };

        [Header("Debug")]
        [Tooltip("Force the spell to remain in play even when they would normally be destroyed. WARNING - THIS WILL CAUSE GAME-BREAKING ERRORS")]
        [SerializeField] bool ForcePersist;
        [Tooltip("Force the spell to update its component colors on update. WARNING - THIS WILL CAUSE PERFORMANCE ISSUES IF ENABLED DURING GAMEPLAY")]
        [SerializeField] bool RealtimeColors;
        [SerializeField] DebugChannel m_debugChannel = null;
        #endregion

        #region PRIVATE VALUES
        bool m_initialized = false;
        float m_addedSpeed = 0;
        float m_hangTime = 0;

	    List<GameObject> m_timedObjects = new List<GameObject>();
	    Dictionary<GameObject, float> m_damageTimers = new Dictionary<GameObject, float>();

        float m_baseDamage;
        float m_basePushForce;
        float m_baseSpeed;
        float m_vertVel;
        bool m_onGround = false;
        bool m_canMove = true;
        float m_baseSize;
        float m_baseTrackingRate;
        float m_lifetime;
	    float m_range;
        #endregion

        #region PUBLIC FIELD
        public Wand SourceWand
        {
            get => m_sourceWand;
            private set
            {
                if (m_sourceWand == value)
                    return;

                if (m_sourceWand)
                {
                    if (transform.parent == m_sourceWand)
                        transform.parent = value?.transform;

                    m_sourceWand.OnItemDestroyed.RemoveListener(gameObject.SelfDestruct);
                }

                m_sourceWand = value;

                if (!m_sourceWand)
                    return;

                m_sourceWand.OnItemDestroyed.AddListener(gameObject.SelfDestruct);

                SetSpellData(m_sourceWand.Spell);

                SetBaseColors(m_sourceWand.Spell.defaultColor1, m_sourceWand.Spell.defaultColor2, m_sourceWand.Spell.defaultColor3, m_sourceWand.Spell.defaultColor4);
            }
        }

        int OwnerID => OwnerPlayer ? OwnerPlayer.photonView.Owner.ActorNumber: -1;
        #endregion

        #region READABLES
        Player OwnerPlayer =>
            SourceWand?.Owner;
        int OwnerActorNumber =>
            OwnerPlayer ? OwnerPlayer.photonView.Owner.ActorNumber : -1;
        #endregion

        #region DEFAULT METHODS
        void Start()
        {
            SFXService.InstantiateSFXRepeating(m_travelSound, transform.position, 1, 1, transform);

            InitializeSpell(SpellData);

            if (SpellData)
                OnSpellInstantiated?.Invoke(this);
        }
        void OnEnable()
        {
            SFXService.InstantiateSFX(m_castSound, transform.position, 1, 1, transform);
        }
        void OnDisable()
        {
            SFXService.InstantiateFX(SpellData.SpellDisableFX, transform.position, transform.forward);
        }
        void Update()
        {
            if (RealtimeColors)
                UpdateColors();
        }
        void FixedUpdate()
        {
            if (!m_initialized)
            {
                InitializeSpell(SpellData);

                if (!m_initialized)
                    return;
            }

            HandleFlightTracking();
            HandleFlightTravel();
            HandleTerrainTravel();
            HandleTargetTracking();

            HandleDynamicValues();
            HandleDOTs();
        }
        void OnTriggerEnter(Collider other)
        {
            if (other.gameObject == Player.LocalPlayer?.gameObject)
                HitPlayer(Player.LocalPlayer, transform.position - other.ClosestPoint(transform.position));
            else if (other.TryGetComponent(out MagicWall _wall))
                HitWall(_wall, transform.position);
            else if (other.TryGetComponent(out Spell _spell))
                HitOtherSpell(_spell, transform.position);
        }
        void OnTriggerExit(Collider other)
        {
            if (!m_damageTimers.ContainsKey(other.gameObject))
                return;

	        m_timedObjects.Remove(other.gameObject);
		    m_damageTimers.Remove(other.gameObject);
        }
        void OnDrawGizmosSelected()
        {
            m_debugChannel?.DrawText(
                new string[]
                {
                    "Spell [ID: " + name.Split('_')[1] + "]",
                    "Speed: " + Speed.ToString(),
                    "Vector: " + (Rigidbody ? Rigidbody.velocity.magnitude.ToString() : transform.forward.ToString())
                },
                transform.position,
                Vector2.one);
        }
        #endregion

        #region INITIALIZATION METHODS
        public void InitializeSpell(SpellData _stats)
        {
            if (_stats && SpellData != _stats)
                SpellData = _stats;

            if (!SpellData)
            {
                m_debugChannel?.Raise(this, "Spell failed to initialized because it is missing valid spell data!");

                return;
            }

            m_baseDamage = GetCurvedStat(SpellData.Stats.Graph_BaseDamage, SpellData.Stats.SP_BaseDamage);
            m_basePushForce = GetCurvedStat(SpellData.Stats.Graph_BasePushForce, SpellData.Stats.SP_PushForce);
            m_baseSpeed = GetCurvedStat(SpellData.Stats.Graph_BaseSpeed, SpellData.Stats.SP_BaseSpeed);
            m_baseSize = GetCurvedStat(SpellData.Stats.Graph_BaseSize, SpellData.Stats.SP_BaseSize);
            m_lifetime = GetCurvedStat(SpellData.Stats.Graph_Lifetime, SpellData.Stats.SP_Lifetime);
            m_range = GetCurvedStat(SpellData.Stats.Graph_Range, SpellData.Stats.SP_Range);
            m_baseTrackingRate = GetCurvedStat(SpellData.Stats.Graph_BaseTrackingRate, SpellData.Stats.SP_TrackingRate);
            m_damageTimers = new Dictionary<GameObject, float>();

            m_debugChannel?.Raise(this, 
                new string[]
                {
                    "New Spell Initialized:",
                    "Damage: " + m_baseDamage.ToString(),
                    "Push Force: " + m_basePushForce.ToString(),
                    "Speed: " + m_baseSpeed.ToString(),
                    "Size: " + m_baseSize.ToString(),
                    "Lifetime: " + m_lifetime.ToString(),
                    "Range: " + m_range.ToString(),
                    "Tracking Rate: " + m_baseTrackingRate.ToString()
                });

            Rigidbody.useGravity = SpellData.Stats.GravityFactor != 0;
            Rigidbody.mass = SpellData.Stats.GravityFactor == 0 ? 1 : SpellData.Stats.GravityFactor;
            
            ResetSpellAudio();

            foreach (Renderer _renderer in GetComponentsInChildren<Renderer>(true))
                foreach (Material _mat in _renderer.GetActiveMaterials())
                    _mat.enableInstancing = Application.platform != RuntimePlatform.WebGLPlayer;

            UpdateColors();

            m_initialized = true;
        }
        public void ResetInstance()
        {
            ClearTrails();

            Damage = m_baseDamage;
            PushForce = m_basePushForce;
            Speed = m_baseSpeed;
            Size = m_baseSize;
            TrackingRate = m_baseTrackingRate;
            DistanceTravelled = 0;
            FlightTime = 0;
            m_vertVel = 0;
            m_damageTimers = new Dictionary<GameObject, float>();

            ResetSpellAudio();
        }

        private void ClearTrails()
        {
            foreach (Renderer _trail in Color1Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).Clear();

            foreach (Renderer _trail in Color2Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).Clear();

            foreach (Renderer _trail in Color3Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).Clear();

            foreach (Renderer _trail in Color4Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).Clear();
        }

        void ResetSpellAudio()
        {
            foreach (AudioSource _source in GetComponentsInChildren<AudioSource>())
            {
                _source.Stop();
                _source.Play();
            }
        }
        #endregion

        #region DATA METHODS
        public void SetTargetViewID(int _targetViewID) =>
            Target = Player.GetByActorNumber(_targetViewID)?.gameObject;
        public void SetSourceWand(Wand _sourceWand)
        {
            if (m_sourceWand)
                return;

            if (!_sourceWand)
                return;

            SourceWand = _sourceWand;
        }
        public void SetSpellData(SpellData _spellData) =>
            SpellData = _spellData ?? SpellData;
        void HandleDynamicValues()
        {
            float _percScale = 0;

            if (SpellData.Stats.LimitLifespan || SpellData.Stats.LimitRange)
            {
                if (SpellData.Stats.LimitLifespan && SpellData.Stats.LimitRange)
                {
                    float _percThroughLifespan = Mathf.Clamp01(FlightTime / m_lifetime);
                    float _percThroughRange = Mathf.Clamp01(DistanceTravelled / m_range);

                    _percScale = _percThroughLifespan >= _percThroughRange ? _percThroughLifespan : _percThroughRange;
                }
                else if (SpellData.Stats.LimitLifespan)
                    _percScale = Mathf.Clamp01(FlightTime / m_lifetime);
                else
                    _percScale = Mathf.Clamp01(DistanceTravelled / m_range);
            }

            Damage = m_baseDamage * SpellData.Stats.Graph_DamageOverTime.Evaluate(_percScale);
            PushForce = m_basePushForce * SpellData.Stats.Graph_PushForceOverTime.Evaluate(_percScale);
            Size = m_baseSize * SpellData.Stats.Graph_SizeOverTime.Evaluate(_percScale);
            Speed = m_baseSpeed * SpellData.Stats.Graph_SpeedOverTime.Evaluate(_percScale) + m_addedSpeed;
            TrackingRate = m_baseTrackingRate * SpellData.Stats.Graph_TrackingRateOverTime.Evaluate(_percScale);

            if (m_hangTime > 0)
                m_hangTime -= Time.deltaTime;
            else if (m_hangTime < 0)
                m_hangTime = 0;

            if (m_addedSpeed > 0)
                m_addedSpeed -= Time.deltaTime;
            else if (m_addedSpeed < 0)
                m_addedSpeed = 0;

            Transform _parent = transform.parent;

            transform.parent = null;
            transform.localScale = Vector3.one * Size;
            transform.parent = _parent;

            foreach (Renderer _trail in Color1Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).widthMultiplier = Size / 2;
            foreach (Renderer _trail in Color2Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).widthMultiplier = Size / 2;
            foreach (Renderer _trail in Color3Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).widthMultiplier = Size / 2;
            foreach (Renderer _trail in Color4Renderers)
                if (_trail is TrailRenderer)
                    (_trail as TrailRenderer).widthMultiplier = Size / 2;

            Rigidbody.velocity = transform.forward * Speed;
        }
        float GetCurvedStat(AnimationCurve _statCurve, StatScalingParam _param)
        {
            float _value = _statCurve.Evaluate(0);

            if (_param == StatScalingParam.NoScaling)
                return _value;

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

            return _value;
        }
        #endregion

        #region MOVEMENT METHODS
        void HandleFlightTracking()
        {
            if (SpellData.Stats.LimitLifespan)
            {
                FlightTime += Time.fixedDeltaTime;

                if (FlightTime > m_lifetime)
                    switch (SpellData.Stats.CacheAtLifetime)
                    {
                        case (true):
                            if (m_sourceWand)
                            {

                                m_debugChannel?.Raise(this, "Caching Spell Instance because its lifetime has expired at " + FlightTime.ToString() + " out of " + m_lifetime.ToString() + " seconds.");

                                FlightTime = 0;

                                m_sourceWand.CacheSpellInstance(gameObject);
                            }
                            else
                                DestroySpell();
                            return;

                        case (false):
                            Rigidbody.velocity = Vector3.zero;
                            break;
                    }
            }

            if (SpellData.Stats.LimitRange)
            {
                DistanceTravelled += Time.fixedDeltaTime * Rigidbody.velocity.magnitude;

                if (DistanceTravelled > m_range)
                    switch (SpellData.Stats.CacheAtRange)
                    {
                        case (true):
                            m_debugChannel?.Raise(this, "Caching Spell Instance due to range constraints");

                            DistanceTravelled = 0;

                            m_sourceWand.CacheSpellInstance(gameObject);
                            return;
                        case (false):
                            Rigidbody.velocity = Vector3.zero;
                            break;
                    }
            }
        }
        void HandleFlightTravel()
        {
            if (m_onGround || !m_canMove)
            {
                m_debugChannel?.Raise(this, "Spell [ID: " + name.Split('_')[1] + "] is either on the ground or unable to move!");

                return;
            }

            if (SpellData.Stats.GravityFactor > 0 && Vector3.Angle(transform.forward, Vector3.down) > 15 && m_hangTime == 0)
                transform.rotation *= Quaternion.AngleAxis(SpellData.Stats.GravityFactor * Time.fixedDeltaTime, transform.right);

            Rigidbody.velocity = transform.forward * Speed;

            if (Rigidbody.velocity != Vector3.zero)
                transform.LookAt(transform.position + Rigidbody.velocity, Vector3.up);
        }
        void HandleTerrainTravel()
        {
            if (SpellData.Stats.DestroyedByTerrain || !SpellData.Stats.TravelOverTerrain || !m_canMove)
                return;

            LayerMask _mask = LayerMask.GetMask("Default", "Environment");

            if (!Physics.Raycast(transform.position + (transform.up * 0.1f), transform.forward, out RaycastHit hit, Rigidbody.velocity.magnitude * Time.fixedDeltaTime, _mask))
                if (!Physics.Raycast(transform.position + (transform.up * 0.1f), -transform.up, out hit, 0.7f, _mask))
                    if (!Physics.Raycast(transform.position + (transform.forward * 0.1f) + (-transform.up * 0.1f), -transform.forward, out hit, 0.2f, _mask))
                    {
                        if (m_onGround)
                            m_onGround = false;

                        m_vertVel += m_hangTime > 0 ? 0 : SpellData.Stats.GravityFactor * Time.fixedDeltaTime;

                        Rigidbody.velocity = ((transform.forward * Speed + Vector3.down * m_vertVel) / 2).normalized * Speed;

                        transform.LookAt(transform.position + Rigidbody.velocity.normalized, Vector3.up);

                        return;
                    }

            if (!m_onGround)
            {
                m_onGround = true;

                m_vertVel = 0;
            }

            Vector3 lookAt = Vector3.Cross(transform.right, hit.normal);
                
            transform.LookAt(transform.position + lookAt, hit.normal);

            Rigidbody.velocity = transform.forward * Speed;

            transform.position = hit.point;
        }
        void HandleTargetTracking()
        {
            // DEVELOPER NOTE - Terrain travel behaviors conflict with targetting navigation and vice versa. This is disabled until a solution can be found.
            if (SpellData.Stats.TravelOverTerrain)
                return;

            if (SpellData.Stats.TargettingBehavior == SpellTargettingBehavior.None || !Target)
                return;

            Vector3 _dirToTarget = (Target.transform.position - transform.position).normalized;

            if (transform.forward == _dirToTarget)
                return;

            Quaternion _toTargetRotation = Quaternion.FromToRotation(transform.forward, _dirToTarget);
            
            transform.rotation = Quaternion.Slerp(transform.rotation, _toTargetRotation, GetCurvedStat(SpellData.Stats.Graph_BaseTrackingRate, SpellData.Stats.SP_TrackingRate));

            Rigidbody.velocity = Rigidbody.velocity.magnitude * transform.forward;
        }
        public void BoostSpell(float _speedValue, float _hangTime = 0)
        {

        }
        #endregion

        #region HIT METHODS
        void HitPlayer(Player _player, Vector3 _position = default(Vector3), Vector3 _hitNormal = default(Vector3))
        {
            if (!SpellData.Stats.PlayerBehavior.CanHit)
                return;

            if (!_player)
                return;

            if (_player != Player.LocalPlayer)
                return;

            if (_player == OwnerPlayer && !SpellData.Stats.CanHitOwner)
                return;

            List<string> _hitDescription = new List<string>() { $"{name} registered a player hit!" };

            if (SpellData.Stats.PlayerBehavior.Damages)
            {
                float _bonusDamage = SpellData.Stats.PlayerBehavior.DealsBonusDamage ? (GetCurvedStat(SpellData.Stats.PlayerBehavior.Graph_BonusDamage, SpellData.Stats.PlayerBehavior.SP_BonusDamage)) : 0;

                _hitDescription.Add($"Dealing {Damage + _bonusDamage} damage to {_player.name}");

                if (PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
                    _player.photonView.RPC(nameof(_player.ProjectileHit), RpcTarget.Others, _player.photonView.Owner.ActorNumber , OwnerID, Damage + _bonusDamage);
                    _player.photonView.RPC(nameof(_player.ProjectileHit), RpcTarget.Others, _player.photonView.Owner.ActorNumber, OwnerActorNumber, Damage + _bonusDamage);

                _player.ProjectileHit(_player.photonView.Owner.ActorNumber, OwnerID, Damage + _bonusDamage);
                _player.ProjectileHit(_player.photonView.Owner.ActorNumber, OwnerActorNumber, Damage + _bonusDamage);
                
	            if (SpellData.Stats.CanHitRepeatedly && !m_damageTimers.ContainsKey(_player.gameObject)){
	            	m_timedObjects.Add(_player.gameObject);
		            m_damageTimers.Add(_player.gameObject, 0.5f);
	            }
            }

            if (PushForce != 0)
            {
                Vector3 _force = Vector3.zero;

                switch (SpellData.Stats.ForceDirection)
                {
                    case (SpellForceDirection.LocalForward):
                        _force = Vector3.forward;
                        break;
                    case (SpellForceDirection.LocalBackward):
                        _force = -Vector3.forward;
                        break;
                    case (SpellForceDirection.GlobalUp):
                        _force = Vector3.up;
                        break;
                    case (SpellForceDirection.CenterIn):
                        _force = -(_player.transform.position - transform.position).normalized;
                        break;
                    case (SpellForceDirection.CenterOut):
                        _force = (_player.transform.position - transform.position).normalized;
                        break;
                }

                _player.Push(_force * PushForce * 10);

                _hitDescription.Add($"{name} applying force of {PushForce} to {_player.name}");
            }

            switch (SpellData.Stats.PlayerBehavior.CollisionBehavior)
            {
                case (SpellCollisionBehavior.PassesThrough):
                    _hitDescription.Add($"{name} passes through players.");
                    break;
                case (SpellCollisionBehavior.DestroyedOnHit):
                    _hitDescription.Add($"{name} is destroyed upon hitting a player.");
                    m_sourceWand.CacheSpellInstance(gameObject);
                    break;
                case (SpellCollisionBehavior.BouncesOff):
                    _hitDescription.Add($"{name} bounces off of players.");
                    if (Rigidbody)
                        Rigidbody.velocity = Vector3.Reflect(Rigidbody.velocity, _hitNormal);
                    transform.LookAt(transform.position + Rigidbody.velocity.normalized, Vector3.up);
                    break;
                case (SpellCollisionBehavior.HaltsAt):
                    _hitDescription.Add($"{name} stops moving on contact with players.");
                    if (Rigidbody)
                        Rigidbody.velocity = Vector3.zero;
                    break;
                case (SpellCollisionBehavior.SticksTo):
                    _hitDescription.Add($"{name} sticks to players.");
                    if (!transform.parent || !GetComponentInParent<Player>())
                    {
                        if (Rigidbody)
                        {
                            Rigidbody.velocity = Vector3.zero;
                            Rigidbody.useGravity = false;
                        }

                        Speed = 0;
                        m_canMove = false;
                        transform.parent = _player.transform;
                    }
                    break;
                case (SpellCollisionBehavior.SnapsToCenter):
                    _hitDescription.Add($"{name} snaps to the center of players.");
                    if (!transform.parent || !GetComponentInParent<Player>())
                    {
                        if (Rigidbody)
                        {
                            Rigidbody.velocity = Vector3.zero;
                            Rigidbody.useGravity = false;
                        }

                        Speed = 0;
                        m_canMove = false;
                        transform.parent = _player.transform;
                        transform.position = _player.transform.position;
                    }
                    break;
            }

            m_debugChannel?.Raise(this, _hitDescription.ToArray());

            OnSpellHit?.Invoke(new SpellHitResult(_player.transform, _player.GetComponent<Collider>(), _position, SpellData.Stats.PlayerBehavior.Damages ? SpellHitResult.ResultType.AppliedDamage : SpellHitResult.ResultType.Pass));
        }
        void HitWall(MagicWall _wall, Vector3 _position = default(Vector3), Vector3 _hitNormal = default(Vector3))
        {
            if (!_wall)
                return;

            if (!SpellData.Stats.WallBehavior.CanHit)
                return;

            List<string> _hitDescription = new List<string>() { $"{name} registered a wall hit!" };

            if (SpellData.Stats.WallBehavior.Damages)
            {
                float _bonusDamage = SpellData.Stats.WallBehavior.DealsBonusDamage ? (GetCurvedStat(SpellData.Stats.WallBehavior.Graph_BonusDamage, SpellData.Stats.WallBehavior.SP_BonusDamage)) : 0;

                _wall.ReceiveHit(Damage + _bonusDamage);

                _hitDescription.Add($"{name} is dealing {Damage + _bonusDamage} to this wall!");

                if (SpellData.Stats.CanHitRepeatedly && !m_damageTimers.ContainsKey(_wall.gameObject))
                    m_damageTimers.Add(_wall.gameObject, 0.5f);
            }
            
            switch (SpellData.Stats.WallBehavior.CollisionBehavior)
            {
                case (SpellCollisionBehavior.PassesThrough):
                    _hitDescription.Add($"{name} passes through walls.");
                    break;
                case (SpellCollisionBehavior.DestroyedOnHit):
                    _hitDescription.Add($"{name} is cached on contact.");
                    m_sourceWand.CacheSpellInstance(gameObject);
                    break;
                case (SpellCollisionBehavior.BouncesOff):
                    _hitDescription.Add($"{name} bounces off of walls.");
                    if (Rigidbody)
                    {
                        Rigidbody.velocity = Vector3.Reflect(Rigidbody.velocity, (Vector3.Angle(transform.position - _wall.transform.position, transform.forward) > 90 ? _wall.transform.forward : -_wall.transform.forward ));
                        transform.LookAt(transform.position + Rigidbody.velocity.normalized, Vector3.up);
                    }
                    break;
                case (SpellCollisionBehavior.HaltsAt):
                    _hitDescription.Add($"{name} stops moving on contact with walls.");
                    if (Rigidbody)
                        Rigidbody.velocity = Vector3.zero;
                    break;
                case (SpellCollisionBehavior.SticksTo):
                    _hitDescription.Add($"{name} sticks to walls.");
                    if (!transform.parent || !GetComponentInParent<MagicWall>())
                    {
                        if (Rigidbody)
                        {
                            Rigidbody.velocity = Vector3.zero;
                            Rigidbody.useGravity = false;
                        }

                        Speed = 0;
                        m_canMove = false;
                        transform.parent = _wall.transform;
                    }
                    break;
                case (SpellCollisionBehavior.SnapsToCenter):
                    _hitDescription.Add($"{name} snaps to the center of walls.");
                    if (!transform.parent || !GetComponentInParent<MagicWall>())
                    {
                        if (Rigidbody)
                        {
                            Rigidbody.velocity = Vector3.zero;
                            Rigidbody.useGravity = false;
                        }

                        Speed = 0;
                        m_canMove = false;
                        transform.parent = _wall.transform;
                        transform.position = _wall.transform.position;
                    }
                    break;
            }

            m_debugChannel?.Raise(this, _hitDescription.ToArray());

            OnSpellHit?.Invoke(
                new SpellHitResult(
                    _wall.transform, 
                    _wall.GetComponent<Collider>(), 
                    _position, 
                    SpellData.Stats.WallBehavior.Damages ? SpellHitResult.ResultType.AppliedDamage : SpellHitResult.ResultType.Pass));
        }
        void HitOtherSpell(Spell _spell, Vector3 _position = default(Vector3), Vector3 _hitNormal = default(Vector3))
        {
            if (!_spell)
                return;

            if (PrimaryType == SpellPrimaryType.Barrier)
                return;

            if (_spell.PrimaryType != SpellPrimaryType.Barrier)
                return;

            m_debugChannel?.Raise(this, $"{name} hit a barrier!");

            SourceWand.CacheSpellInstance(gameObject);

            OnSpellHit?.Invoke(new SpellHitResult(_spell.transform, _spell.GetComponent<Collider>(), _position, SpellHitResult.ResultType.WasBlocked));
        }
        #endregion

        #region DAMAGE METHODS
        void HandleDOTs()
	    {
		    if (m_damageTimers == null)
		    	m_damageTimers = new Dictionary<GameObject, float>();
        	
		    if (m_damageTimers.Count < 1)
			    return;
        	
            foreach (GameObject key in m_timedObjects)
            {
                m_damageTimers[key] -= Time.fixedDeltaTime;

                if (m_damageTimers[key] <= 0)
                {
                    if (key.TryGetComponent(out Player _player))
                        HitPlayer(_player);
                    else if (key.TryGetComponentInParent(out MagicWall _wall))
                        HitWall(_wall);
                    else if (key.TryGetComponent(out Spell _spell))
                        HitOtherSpell(_spell);

                    m_damageTimers[key] += 0.5f;
                }
            }
        }
        #endregion

        #region COLOR METHODS
        public void SetBaseColors(Color _color1, Color _color2, Color _color3, Color _color4)
        {
            SpellColor1 = _color1;
            SpellColor2 = _color2;
            SpellColor3 = _color3;
            SpellColor4 = _color4;

            UpdateColors();
        }
        void UpdateColors()
        {
            SpellColor1.a = 0.5f;
            SpellColor2.a = 0.5f;
            SpellColor3.a = 0.5f;
            SpellColor4.a = 0.5f;

            SetRendererColors(Color1Renderers, SpellColor1);
            SetRendererColors(Color2Renderers, SpellColor2);
            SetRendererColors(Color3Renderers, SpellColor3);
            SetRendererColors(Color4Renderers, SpellColor4);
        }
        #endregion

        void SetRendererColors(Renderer[] _renderers, Color _color)
        {
            foreach (Renderer _renderer in _renderers)
            {
                if (_renderer == null)
                    continue;

                if (_renderer is MeshRenderer)
                {
                    if (_renderer.material.shader.name.Contains("Lit"))
                    {
                        _renderer.material.SetColor("_EmissionColor", _color);

                        m_debugChannel?.Raise(this, "Color set by URP material");
                    }
                    else if (_renderer.material.shader.name.Contains("Particles"))
                    {
                        _renderer.material.SetColor("_TintColor", _color);

                        m_debugChannel?.Raise(this, "Color set by particle material");
                    }
                    else
                    {
                        _renderer.material.color = _color;
                        _renderer.material.ToFadeMode();

                        m_debugChannel?.Raise(this, "Color set by default material");
                    }
                }
                else if (_renderer is ParticleSystemRenderer)
                {
                    var _main = _renderer.gameObject.GetComponent<ParticleSystem>().main;
                    _main.startColor = SpellColor1;
                }
                else if (_renderer is TrailRenderer)
                {
                    (_renderer as TrailRenderer).startColor = SpellColor1;
                    (_renderer as TrailRenderer).endColor = new Color(SpellColor1.r, SpellColor1.g, SpellColor1.b, 0);
                }
            }
        }

        #region EXPIRATION METHODS
        public void DestroySpell()
        {
            if (ForcePersist)
                return;

            Destroy(gameObject);
        }
        #endregion
    }
}
