using System;
using System.Linq;
using UnityEngine;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Abstract.Events;
using VOID.Generic.Objects.Unit.Animations;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Objects.Weapon;
using VOID.Generic.Objects.Wearable.Events;
using VOID.Generic.Settings;
using VOID.ScriptableObjects.Animations;

namespace VOID.Generic.Objects.Unit
{
    [Serializable]
    public class UnitObjectAnimator : ObjectComponent<UnitObject>
    {
        #region Initialize

        protected override void SelfInitialize()
        {
            unitAnimatorCallbacks = parent.GetComponentInChildren<UnitAnimatorCallbacks>();
            unitAnimatorCallbacks.OnAnimationEvent += HandleAnimationEvent;
            unitAnimatorCallbacks.OnApplyRootMotion += HandleRootMotion;
            animator = parent.GetComponentInChildren<Animator>();
            animatorOverrideController = new AnimatorOverrideController(animator.runtimeAnimatorController);
            animator.runtimeAnimatorController = animatorOverrideController;
        }

        protected override void EventInitialize()
        {
            parent.basicEvents.onFixedUpdate.AddListener(UpdateAnimatorVelocity);
            parent.basicEvents.onDeathAfter.AddListener(OnDeath);
            parent.unitEvents.onEquipAfter.AddListener(OnEquip);
            parent.unitEvents.onUnEquipAfter.AddListener(OnUnEquip);
            parent.basicEvents.onTurnQueueEnter.AddListener(OnTurnQueueEnter);
            parent.basicEvents.onTurnQueueLeave.AddListener(OnTurnQueueLeave);
            parent.basicEvents.onRevive.AddListener(OnRevive);
            parent.unitEvents.onMoveStart.AddListener(OnMoveStart);
        }

        protected override void Initialize()
        {
            var humanoidAnimationSettings = SettingsManager.humanoidAnimation;
            SetLoopAnimation(AnimationType.Traverse, humanoidAnimationSettings.defaultLadderUpAnimation);
            SetLoopAnimation(AnimationType.Death, humanoidAnimationSettings.defaultDeathAnimation);
            UpdateStanceAnimationOnce();
        }

        #endregion

        #region Animation Hashes
        
        // Layers
        private static readonly int StanceLayerIndex = 0;
        private static readonly int ActionLayerIndex = 1;

        // Movement
        private static readonly int VelocityHash = Animator.StringToHash("VELOCITY");
        private static readonly int VelocityVerticalNormalizedHash = Animator.StringToHash("VELOCITY_VERTICAL");
        private static readonly int VelocityHorizontalNormalizedHash = Animator.StringToHash("VELOCITY_HORIZONTAL");

        // Events inside animations
        private static readonly int TriggerEndLoopHash = Animator.StringToHash("TRIGGER_LOOP_END");
        private static readonly int IsLoopHash = Animator.StringToHash("IS_LOOP");
        private static readonly int HaveLoopStartHash = Animator.StringToHash("HAVE_LOOP_START");
        private static readonly int HaveLoopEndHash = Animator.StringToHash("HAVE_LOOP_END");
        
        // Animations
        private static readonly int TriggerCastingHash = Animator.StringToHash("TRIGGER_CASTING");
        private static readonly int TriggerAbilityHash = Animator.StringToHash("TRIGGER_ABILITY");
        private static readonly int TriggerTraverseHash = Animator.StringToHash("TRIGGER_TRAVERSE");
        private static readonly int TriggerDeathHash = Animator.StringToHash("TRIGGER_DEATH");
        
        // Stances
        private static readonly int TriggerStanceRelaxHash = Animator.StringToHash("TRIGGER_STANCE_RELAX");
        private static readonly int TriggerStanceUnarmedHash = Animator.StringToHash("TRIGGER_STANCE_UNARMED");
        private static readonly int TriggerStance1Hash = Animator.StringToHash("TRIGGER_STANCE_1");
        private static readonly int TriggerStance1WithShieldHash = Animator.StringToHash("TRIGGER_STANCE_1_WITH_SHIELD");
        private static readonly int TriggerStance2Hash = Animator.StringToHash("TRIGGER_STANCE_2");
        private static readonly int TriggerStance2SwordHash = Animator.StringToHash("TRIGGER_STANCE_2_SWORD");
        private static readonly int TriggerStance2BowHash = Animator.StringToHash("TRIGGER_STANCE_2_BOW");
        private static readonly int TriggerStance2CrossbowHash = Animator.StringToHash("TRIGGER_STANCE_2_CROSSBOW");
        private static readonly int TriggerStance2StaffHash = Animator.StringToHash("TRIGGER_STANCE_2_STAFF");
        
        // Actions - States
        private static readonly int CastingStartLoopHash = Animator.StringToHash("CASTING - START");
        private static readonly int CastingLoopHash = Animator.StringToHash("CASTING - LOOP");
        private static readonly int CastingEndLoopHash = Animator.StringToHash("CASTING - END");
        private static readonly int AbilitySimpleHash = Animator.StringToHash("ABILITY - SIMPLE");
        private static readonly int AbilityStartLoopHash = Animator.StringToHash("ABILITY - START");
        private static readonly int AbilityLoopHash = Animator.StringToHash("ABILITY - LOOP");
        private static readonly int AbilityEndLoopHash = Animator.StringToHash("ABILITY - END");
        private static readonly int TraverseStartLoopHash = Animator.StringToHash("TRAVERSE - START");
        private static readonly int TraverseLoopHash = Animator.StringToHash("TRAVERSE - LOOP");
        private static readonly int TraverseEndLoopHash = Animator.StringToHash("TRAVERSE - END");
        private static readonly int DeathStartLoopHash = Animator.StringToHash("DEATH - START");
        private static readonly int DeathLoopHash = Animator.StringToHash("DEATH - LOOP");

        #endregion

        protected UnitAnimatorCallbacks unitAnimatorCallbacks;
        protected Animator animator;
        protected AnimatorOverrideController animatorOverrideController;

        #region RootMotion

        public bool applyRootMotion;
        
        private void HandleRootMotion()
        {
            if (applyRootMotion) parent.transform.position += animator.deltaPosition;
        }

        #endregion
        
        #region AnimationEvents
        
        private AnimationGenericEvent _currentAnimationEvent;
        private int[] _currentExpectedStates;

        private void HandleAnimationEvent(AnimationEvent animationEvent)
        {
            // LITTLE FIX: Avoid executing events multiple times from different animation states
            var currentState = animationEvent.animatorStateInfo.shortNameHash;
            if (_currentExpectedStates.All(state => state != currentState)) return;
            
            switch (animationEvent.functionName)
            {
                case "OnStart": OnStartAnimationEvent(); break;
                case "OnLoopStart": OnLoopStartAnimationEvent(); break;
                case "OnPerform": OnPerformAnimationEvent(); break;
                case "OnLoopEnd": OnLoopEndAnimationEvent(); break;
                case "OnEnd": OnEndAnimationEvent(); break;
            }
        }

        private AnimationGenericEventSet animSet => parent.unitEvents.onAnimation[_currentAnimationEvent.animationType];
        private void OnStartAnimationEvent() => animSet.onStart.Invoke(_currentAnimationEvent);
        private void OnLoopStartAnimationEvent() => animSet.onLoopStart.Invoke(_currentAnimationEvent);
        private void OnPerformAnimationEvent() => animSet.onPerform.Invoke(_currentAnimationEvent);
        private void OnLoopEndAnimationEvent() => animSet.onLoopEnd.Invoke(_currentAnimationEvent);
        private void OnEndAnimationEvent() => animSet.onEnd.Invoke(_currentAnimationEvent);

        #endregion

        #region Events

        private void OnDeath(DeathEvent deathEvent)
        {
            var animationEvent = new AnimationGenericEvent();
            animationEvent.unit = deathEvent.obj as UnitObject;
            animationEvent.animationType = AnimationType.Death;
            StartLoopAnimation(animationEvent);
        }

        private void OnEquip(EquipEvent equipEvent)
        {
            if (equipEvent.wearable is WeaponObject)
            {
                parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
                parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
            }
        }

        private void OnUnEquip(UnEquipEvent unEquipEvent)
        {
            if (unEquipEvent.wearable is WeaponObject)
            {
                parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
                parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
            } 
        }

        private void OnTurnQueueEnter(TurnEvent turnEvent)
        {
            parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
            parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
        }

        private void OnTurnQueueLeave(TurnEvent turnEvent)
        {
            parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
            parent.basicEvents.onFixedUpdate.AddListener(UpdateStanceAnimationOnce);
        }

        private void OnRevive(ReviveEvent reviveEvent)
        {
            // Forcefully end DEATH animation
            animator.CrossFadeInFixedTime("IDLE", 0.1f, ActionLayerIndex);
        }

        private void OnMoveStart(MoveEvent moveEvent)
        {
            // Forcefully end (skipping exiting time) current action animation
            animator.CrossFadeInFixedTime("IDLE", 0.1f, ActionLayerIndex);
        }
        
        #endregion

        #region Animations

        private void UpdateAnimatorVelocity()
        {
            var localVelocity = parent.unitNavAgent.localVelocity;
            animator.SetFloat(VelocityHash, parent.unitState.isMoving ? parent.unitNavAgent.moveSpeed : 0f);
            animator.SetFloat(VelocityVerticalNormalizedHash, localVelocity.normalized.z);
            animator.SetFloat(VelocityHorizontalNormalizedHash, localVelocity.normalized.x);
        }

        public void SetAnimation(AnimationType animationType, ActionAnimationList animationList)
        {
            var baseClip = GetBaseAnimation(animationType).clip;
            animatorOverrideController[baseClip] = animationList ? animationList.list.clip : null;
        }

        public void SetLoopAnimation(AnimationType animationType, ActionLoopAnimationList animationList)
        {
            SetLoopAnimation(animationType, animationList ? animationList.list : null);
        }

        private void SetLoopAnimation(AnimationType animationType, ActionLoopAnimation animations)
        {
            var baseAnimations = GetBaseLoopAnimation(animationType);
            animatorOverrideController[baseAnimations.startClip] = animations?.startClip;
            animatorOverrideController[baseAnimations.loopClip] = animations?.loopClip;
            animatorOverrideController[baseAnimations.endClip] = animations?.endClip;
        }

        private void UpdateStanceAnimationOnce()
        {
            // Prevent running this more than once per frame
            parent.basicEvents.onFixedUpdate.RemoveListener(UpdateStanceAnimationOnce);
            
            if (!parent.basicState.turnManager)
            {
                // Disable (hide) weapons in hands
                parent.unitData.leftHandPosition.gameObject.SetActive(false);
                parent.unitData.rightHandPosition.gameObject.SetActive(false);
                
                // Out of combat idle and move animations
                animator.SetTrigger(TriggerStanceRelaxHash);
                return;
            }
            
            // Stance depends on currently equipped weapons
            var mainHand = parent.unitEquipment.GetEquipped(EquipmentSlotType.MainHand) as WeaponObject;
            var offHand = parent.unitEquipment.GetEquipped(EquipmentSlotType.OffHand) as WeaponObject;
            var stanceHash = GetCombatStanceAnimationHash(mainHand, offHand);
            // TODO: pomyśleć o lepszym ogarnięciu animacji postawy poza i w trakcie walki dla każdej broni
            animator.SetTrigger(stanceHash);
            
            // Activate (show) weapons in hands
            parent.unitData.leftHandPosition.gameObject.SetActive(true);
            parent.unitData.rightHandPosition.gameObject.SetActive(true);
        }

        public void StartAnimation(AnimationGenericEvent tEvent)
        {
            // Send event info to callback class so it can be used when calling its events
            _currentAnimationEvent = tEvent;
            _currentExpectedStates = GetExpectedStates(_currentAnimationEvent.animationType);

            animator.SetBool(IsLoopHash, false);
            animator.SetTrigger(GetAnimationHash(tEvent.animationType));
        }

        public void StartLoopAnimation(AnimationGenericEvent tEvent)
        {
            // Make sure that loop ending trigger is off (from previous loop)  
            animator.ResetTrigger(TriggerEndLoopHash);
            
            // Send event info to callback class so it can be used when calling its events
            _currentAnimationEvent = tEvent;
            _currentExpectedStates = GetExpectedStates(_currentAnimationEvent.animationType);

            // Run start and end loop animation if present, else skip them
            var baseLoopClips = GetBaseLoopAnimation(tEvent.animationType);
            animator.SetBool(HaveLoopStartHash, baseLoopClips.startClip);
            animator.SetBool(HaveLoopEndHash, baseLoopClips.endClip);

            // There is no animation for loop starting - we need to execute events manually
            if (animator.GetBool(HaveLoopStartHash) == false)
            {
                OnStartAnimationEvent();
                OnLoopStartAnimationEvent();
                OnPerformAnimationEvent();
            }

            animator.SetBool(IsLoopHash, true);
            animator.SetTrigger(GetAnimationHash(tEvent.animationType));
        }

        public void EndLoopAnimation()
        {
            // IMPORTANT! This prevent STACKOVERFLOW!
            // Only looped animation should proceed here or else below code can make endless loop,
            // all thanks to AbilityAction that started non-looped animation
            if (animator.GetBool(IsLoopHash) == false) return;
            
            // There is no animation for loop ending - we need to execute events manually
            if (animator.GetBool(HaveLoopEndHash) == false)
            {
                OnLoopEndAnimationEvent();
                OnEndAnimationEvent();
            }
            
            animator.SetTrigger(TriggerEndLoopHash);
        }
        
        #endregion

        #region Helpers

        private int[] GetExpectedStates(AnimationType animationType)
        {
            return animationType switch
            {
                AnimationType.Casting => new[] { CastingStartLoopHash, CastingLoopHash, CastingEndLoopHash },
                AnimationType.Ability => new[] { AbilitySimpleHash, AbilityStartLoopHash, AbilityLoopHash, AbilityEndLoopHash },
                AnimationType.Traverse => new[] { TraverseStartLoopHash, TraverseLoopHash, TraverseEndLoopHash },
                AnimationType.Death => new[] { DeathStartLoopHash, DeathLoopHash },
                _ => throw new ArgumentOutOfRangeException()
            };
        }
        
        private int GetCombatStanceAnimationHash(WeaponObject mainHand, WeaponObject offHand)
        {
            // UNARMED
            if (!mainHand && !offHand) return TriggerStanceUnarmedHash;
            
            // SHIELD
            if (offHand && offHand.weaponData.weaponType is WeaponType.Shield) return TriggerStance1WithShieldHash;
            
            // ONE-HANDED
            if (!mainHand || (mainHand && mainHand.weaponData.weaponCarryType is not WeaponCarryType.TwoHand)) return TriggerStance1Hash;
            
            // BOW
            if (mainHand.weaponData.weaponType is WeaponType.Bow) return TriggerStance2BowHash;
            
            // CROSSBOW
            if (mainHand.weaponData.weaponType is WeaponType.Crossbow) return TriggerStance2CrossbowHash;
            
            // STAFF
            if (mainHand.weaponData.weaponType is WeaponType.Staff) return TriggerStance2StaffHash;

            // SWORD
            if (mainHand.weaponData.weaponType is WeaponType.Sword) return TriggerStance2SwordHash;
            
            // TWO-HANDED basic - AXE or MACE
            return TriggerStance2Hash;
        }
        
        private ActionAnimation GetBaseAnimation(AnimationType animationType)
        {
            var humanoidAnimationSettings = SettingsManager.humanoidAnimation;
            return animationType switch
            {
                AnimationType.Ability => humanoidAnimationSettings.baseAbility,
                _ => throw new Exception($"Base animation don't exists for: {animationType}")
            };
        }
        
        private ActionLoopAnimation GetBaseLoopAnimation(AnimationType animationType)
        {
            var humanoidAnimationSettings = SettingsManager.humanoidAnimation;
            return animationType switch
            {
                AnimationType.Casting => humanoidAnimationSettings.baseCastingLoop,
                AnimationType.Ability => humanoidAnimationSettings.baseAbilityLoop,
                AnimationType.Traverse => humanoidAnimationSettings.baseTraverseLoop,
                AnimationType.Death => humanoidAnimationSettings.baseDeath,
                _ => throw new Exception($"Loop base animation don't exists for: {animationType}")
            };
        }

        private int GetAnimationHash(AnimationType animationType)
        {
            return animationType switch
            {
                AnimationType.Casting => TriggerCastingHash,
                AnimationType.Ability => TriggerAbilityHash,
                AnimationType.Traverse => TriggerTraverseHash,
                AnimationType.Death => TriggerDeathHash,
                _ => throw new ArgumentOutOfRangeException(nameof(animationType), animationType, null)
            };
        }

        #endregion
    }
}