using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.Utilities;
using UnityEngine;
using VOID.Generic.Dict;
using VOID.Generic.Dict.Ability;
using VOID.Generic.Objects.Abstract;
using VOID.Generic.Objects.Unit.Actions.Exceptions;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Projectile;
using VOID.Generic.Util;
using VOID.ScriptableObjects;
using VOID.ScriptableObjects.Abilities;
using Object = UnityEngine.Object;

namespace VOID.Generic.Objects.Unit.Actions
{
    /// <summary>
    /// Makes <see cref="UnitObject"/> cast given <see cref="BasicAbility"/>.
    /// <br/><br/>
    /// <see cref="AbstractAction"/>:<br/>
    /// <inheritdoc cref="AbstractAction"/>
    /// </summary>
    public class AbilityAction : AbstractAction
    {
        private class Target
        {
            public readonly AbstractObject targetObject;
            public readonly Vector3 targetPosition;

            public Target(AbstractObject targetObject, Vector3 targetPosition)
            {
                this.targetObject = targetObject;
                this.targetPosition = targetPosition;
            }
        }

        private readonly UnitObject _unit;
        private readonly BasicAbility _ability;
        private readonly Target[] _targets;

        private int _projectilesEnded;
        private AbilityCastEvent _abilityEvent;

        public AbilityAction(UnitObject unit, BasicAbility ability)
            : this(unit, ability, Array.Empty<Target>())
        {
        }

        public AbilityAction(UnitObject unit, BasicAbility ability, Vector3 target)
            : this(unit, ability, new[] { new Target(null, target) })
        {
        }

        public AbilityAction(UnitObject unit, BasicAbility ability, AbstractObject target)
            : this(unit, ability, new[] { new Target(target, target.transform.position + Vector3.up * target.basicData.height/2) })
        {
        }

        private AbilityAction(UnitObject unit, BasicAbility ability, Target target)
            : this(unit, ability, new[] { target })
        {
        }

        public AbilityAction(UnitObject unit, BasicAbility ability, IEnumerable<Vector3> targets)
            : this(unit, ability, targets.Select(p => new Target(null, p)).ToArray())
        {
        }

        public AbilityAction(UnitObject unit, BasicAbility ability, IEnumerable<AbstractObject> targets)
            : this(unit, ability, targets.Select(o => new Target(o, o.transform.position+ Vector3.up * o.basicData.height/2)).ToArray())
        {
        }

        private AbilityAction(UnitObject unit, BasicAbility ability, Target[] targets)
        {
            _unit = unit;
            _ability = ability;
            _targets = targets;
        }

        public override bool IsInRange()
        {
            return _targets.IsNullOrEmpty() || _targets.All(target =>
                target.targetObject
                    ? RangeUtil.IsInRange(_unit, target.targetObject, _ability.range)
                    : RangeUtil.IsInRange(_unit, target.targetPosition, _ability.range));
        }

        public override bool CanBeStopped() => _abilityEvent is null || _abilityEvent.prevented;

        public override void CanDoIt()
        {
            Check(
                state == ActionState.Ready,
                UnitActionErrorType.ActionStateIsNotReady);
            if (_unit.basicState.turnManager)
            {
                Check(
                    _unit.unitState.isSelfTurn,
                    UnitActionErrorType.UnitSelfTurnNotActive);
                Check(
                    _unit.unitData.currentResources.actionPoints >= _ability.actionPointCost,
                    UnitActionErrorType.UnitNotEnoughActionPoints);
                Check(
                    _unit.unitData.currentResources.movePoints >= _ability.movePointCost,
                    UnitActionErrorType.UnitNotEnoughMovePoints);
            }

            Check(
                _ability is not BasicAttackAbility || _unit.unitData.canAttack,
                UnitActionErrorType.ThisUnitCantAttack);
            Check(
                _unit.unitState.isControllable,
                UnitActionErrorType.UnitStateIsNotControllable);
            Check(
                IsInRange(),
                UnitActionErrorType.NotInRange);
            Check(
                _ability.requirements.All(requirement => requirement.Check(_unit)),
                UnitActionErrorType.RequirementsNotMeetToUseThatAbility);
            Check(
                _ability.projectileSteps.First().validTargets.All(validTarget =>
                    _targets.All(target => validTarget.IsValid(_unit, target.targetPosition, target.targetObject))),
                UnitActionErrorType.WrongTargetsSelectedToUseThatAbility);
            Check(
                _ability.aimingType is AbilityAimingType.Blank || _ability.castUsages == _targets.Length,
                UnitActionErrorType.WrongNumberOfTargetsToUseThatAbility);
        }

        protected override void OnDoIt()
        {
            if (_unit.basicState.turnManager)
            {
                _unit.unitData.UseResource(UnitResourceType.ActionPoints, _ability.actionPointCost);
                _unit.unitData.UseResource(UnitResourceType.MovePoints, _ability.movePointCost);
            }

            _abilityEvent = new AbilityCastEvent
            {
                caster = _unit,
                ability = _ability
            };

            _unit.unitEvents.onStartAbilityBefore.Invoke(_abilityEvent);

            Check(!_abilityEvent.prevented, UnitActionErrorType.AbilityIsPrevented);

            _unit.unitEvents.onStartAbilityAfter.Invoke(_abilityEvent);

            if (_ability.castingType == AbilityCastingType.Instant)
            {
                DoAbility(null);
                EndIt();
                return;
            }

            var animationGenericEvent = new AnimationGenericEvent
            {
                animationType = AnimationType.Ability,
                unit = _unit,
                ability = _ability
            };

            if (_targets.Length > 0) new RotateAction(_unit, _targets[0].targetPosition).DoIt();
            
            if (_ability.castingType is AbilityCastingType.Channel && _ability.castUsages > 0)
            {
                // When channeling - ability ends when all projectiles end
                _unit.unitEvents.onAnimation[AnimationType.Ability].onPerform.AddListener(DoAbility);
                _unit.unitAnimator.SetLoopAnimation(AnimationType.Ability, _ability.abilityLoopAnimationList);
                _unit.unitAnimator.StartLoopAnimation(animationGenericEvent);
            }
            else
            {
                // Otherwise - ability ends with animation
                _unit.unitEvents.onAnimation[AnimationType.Ability].onPerform.AddListener(DoAbility);
                _unit.unitEvents.onAnimation[AnimationType.Ability].onEnd.AddListener(OnAbilityEnd);
                _unit.unitAnimator.SetAnimation(AnimationType.Ability, _ability.abilityAnimationList);
                _unit.unitAnimator.StartAnimation(animationGenericEvent);
            }
        }

        private void DoAbility(AnimationGenericEvent tEvent)
        {
            BeforePerform();
            if (_ability.applyOnCasterMoment is AbilityExecutionMoment.AfterCast) ApplyEffectsToCaster();
            _targets.ForEach((target, index) =>
            {
                // Predefined object is only for visual things
                // Else - new empty projectile object
                var projectile = _ability.projectile
                    ? Object.Instantiate(_ability.projectile.gameObject).GetComponent<ProjectileObject>()
                    : new GameObject().AddComponent<ProjectileObject>();
                
                projectile.StartProjectile(index, _unit, _unit.unitData.castPosition.position, target.targetPosition, _ability.projectileSteps);
                projectile.onHit.AddListener(OnProjectileHit);
                projectile.onEnd.AddListener(OnProjectileEnd);

                _unit.unitEvents.onAbilityProjectileStart.Invoke(new AbilityProjectileEvent
                {
                    caster = _unit,
                    ability = _ability,
                    projectileIndex = index,
                    projectile = projectile
                });
            });
            AfterPerform();
        }

        private void OnProjectileHit(ProjectileObject projectile, AbstractObject hitObject)
        {
            var abilityHitEvent = new AbilityHitEvent
            {
                caster = _unit,
                target = hitObject,
                ability = _ability
            };

            hitObject.basicEvents.onAbilityHitBefore.Invoke(abilityHitEvent);
            _unit.unitEvents.onAbilityHitBefore.Invoke(abilityHitEvent);

            // We cant run Check here, because this OnHit can be one of many - just do nothing more for this projectile
            if (abilityHitEvent.prevented) return;

            hitObject.basicEvents.onAbilityHitAfter.Invoke(abilityHitEvent);
            _unit.unitEvents.onAbilityHitAfter.Invoke(abilityHitEvent);
            
            ApplyEffectsToTarget(hitObject);
        }

        private void OnProjectileEnd(ProjectileObject projectile)
        {
            _projectilesEnded++;
            
            _unit.unitEvents.onAbilityProjectileEnd.Invoke(new AbilityProjectileEvent
            {
                caster = _unit,
                ability = _ability,
                projectileIndex = projectile.index,
                projectile = projectile
            });

            if (_ability.castingType is AbilityCastingType.Channel && _projectilesEnded == _ability.castUsages)
            {
                _unit.unitAnimator.EndLoopAnimation();
                EndIt();
            }
        }

        private void ApplyEffectsToTarget(AbstractObject target)
        {
            if (_ability.applyOnTarget.Count <= 0) return;
            
            // Because ability have defined only effects we need create dummy status
            // IMPORTANT - It is hidden and instantly starts and then ends!
            var targetStatus = ScriptableObject.CreateInstance<BasicStatus>();
            targetStatus.effects = _ability.applyOnTarget;
            targetStatus.hidden = true;
            target.statusManager.AddStatus(targetStatus, _unit, false);
        }

        private void ApplyEffectsToCaster()
        {
            if (_ability.applyOnCaster.Count <= 0) return;
            
            // Because ability have defined only effects we need create dummy status
            // IMPORTANT - It is hidden and instantly starts and then ends!
            var casterStatus = ScriptableObject.CreateInstance<BasicStatus>();
            casterStatus.effects = _ability.applyOnCaster;
            casterStatus.hidden = true;
            _unit.statusManager.AddStatus(casterStatus, _unit, false);
        }

        private void OnAbilityEnd(AnimationGenericEvent tEvent)
        {
            EndIt();
        }

        protected override void OnCancelIt()
        {
            _unit.unitAnimator.EndLoopAnimation();
            _unit.unitEvents.onAnimation[AnimationType.Ability].onPerform.RemoveListener(DoAbility);
            _unit.unitEvents.onAnimation[AnimationType.Ability].onEnd.RemoveListener(OnAbilityEnd);
            _unit.unitEvents.onEndAbility.Invoke(_abilityEvent);
        }

        protected override void OnEndIt()
        {
            _unit.unitAnimator.EndLoopAnimation();
            if (_ability.applyOnCasterMoment is AbilityExecutionMoment.AfterEnd) ApplyEffectsToCaster();
            _unit.unitEvents.onAnimation[AnimationType.Ability].onPerform.RemoveListener(DoAbility);
            _unit.unitEvents.onAnimation[AnimationType.Ability].onEnd.RemoveListener(OnAbilityEnd);
            _unit.unitEvents.onEndAbility.Invoke(_abilityEvent);
        }
    }
}