using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sirenix.Utilities;
using UnityEngine;
using UnityEngine.Events;
using VOID.Generic.Objects.Abstract;
using VOID.Generic.Objects.Unit;
using VOID.Generic.Trajectory;

namespace VOID.Generic.Projectile
{
    /// <summary>
    /// Moves projectile on defined <see cref="ProjectileStep"/>
    /// </summary>
    [DefaultExecutionOrder(-1)]
    public class ProjectileObject : MonoBehaviour
    {
        // Projectile Configuration
        public int index { private set; get; }
        public UnitObject caster { private set; get; }
        public ProjectileStep[] steps { private set; get; }

        // Events
        public UnityEvent<ProjectileObject> onStart = new();
        public UnityEvent<ProjectileObject, AbstractObject> onHit = new();
        public UnityEvent<ProjectileObject> onNextStep = new();
        public UnityEvent<ProjectileObject> onEnd = new();

        // Step info
        private ProjectileStep _currentStep;
        private int _currentStepIndex = -1;
        private float _currentStepDistance;
        private AbstractTrajectory _currentTrajectory;
        private Vector3 _startPosition;
        private Vector3 _targetPosition;
        private Vector3 _lastHitPosition;

        // Projectile and its AOE game objects
        private GameObject _gameObject;
        private GameObject _aoeGameObject;

        // Storing collisions 
        private readonly Dictionary<AbstractObject, int> _aoeCollisions = new();

        private void Awake()
        {
            gameObject.SetActive(false);
        }

        private void FixedUpdate()
        {
            if (MoveProjectile() || CheckCollision()) StartCoroutine(NextStep());
        }
        
        /// <summary>
        /// Moves projectile by one step in trajectory.
        /// </summary>
        /// <returns>TRUE if current step reached end</returns>
        private bool MoveProjectile()
        {
            _currentStepDistance += Time.fixedDeltaTime * _currentStep.speed;
            var newProgress = _currentTrajectory.GetProgressForDistance(_currentStepDistance);
            var newPosition = _currentTrajectory.GetPositionByProgress(newProgress);
            
            // Update rotation
            _gameObject.transform.rotation = Quaternion.LookRotation(transform.position - newPosition);
            
            // Update position
            _aoeGameObject.transform.position = transform.position = newPosition;

            return Mathf.Approximately(newProgress, 1f);
        }

        /// <summary>
        /// Checks if projectile should end early because it hit something.
        /// </summary>
        /// <returns>TRUE if collision ends this projectile step</returns>
        private bool CheckCollision()
        {
            int layerMask;
            switch (_currentStep.collisionType)
            {
                case ProjectileCollisionType.StaticOrDynamicCollision:
                    layerMask = LayerManager.Static | LayerManager.StaticHideable | LayerManager.Dynamic;
                    break;
                case ProjectileCollisionType.StaticCollision:
                    layerMask = LayerManager.Static | LayerManager.StaticHideable;
                    break;
                default:
                    return false;
            }

            // Just to be sure - ignore caster :D
            var colliders = new Collider[2];
            Physics.OverlapSphereNonAlloc(transform.position, _currentStep.radius, colliders, layerMask);
            return colliders.Any(collider => collider && collider.GetComponentInParent<UnitObject>() != caster);
        }

        /// <summary>
        /// Starts next step of projectile. If no more steps available then whole projectile is destroyed.
        /// </summary>
        private IEnumerator NextStep()
        {
            // IMPORTANT
            // 1. Collisions calculate after fixed update, we need to wait 1 (or 2?) fixed updates
            // to be sure that we dont ignore collision that will occour later this frame in projectile's AOE
            // 2. Also we want to stop FixedUpdate from running when this happens otherwise this part will be executed twice 
            enabled = false;
            yield return new WaitForFixedUpdate();
            enabled = true;

            if (_currentStep.hitType == ProjectileHitType.OnEnd)
                _aoeCollisions.Keys.ForEach(hitGameObject => onHit.Invoke(this, hitGameObject));

            if (_currentStepIndex >= steps.Length - 1)
            {
                EndProjectile();
                yield break;
            }

            _lastHitPosition = gameObject.transform.position;
            onNextStep.Invoke(this);
            PrepareNextStep();
        }

        /// <summary>
        /// 1. Clear temporary data<br/>
        /// 2. Initialize next Trajectory<br/>
        /// 3. Initialize next Area Of Effect
        /// </summary>
        private void PrepareNextStep()
        {
            // Clear temp data about previous step
            _currentStepDistance = 0f;
            _currentStep = steps[++_currentStepIndex];
            _aoeCollisions.Clear();
            _currentTrajectory = _currentStep.trajectory.Clone();
            
            // Update AOE GameObject's rotation and collider by data in AreaOfEffect
            // This process can be done once when starting new step, because AOE dont change during flight
            if (_aoeGameObject.TryGetComponent(out Collider aoeCollider)) Destroy(aoeCollider);
            _currentStep.areaOfEffect.AddCollider(_aoeGameObject);
            _aoeGameObject.transform.rotation = _currentStep.areaOfEffect.GetRotation(_startPosition, _targetPosition);
            
            // Prepare trajectory
            _currentTrajectory.Init(FindStartPositionForStep(), _targetPosition);
        }

        /// <summary>
        /// Returns start position for current step by defined <see cref="ProjectileStepStartFrom"/>.
        /// </summary>
        private Vector3 FindStartPositionForStep()
        {
            return _currentStep.startFrom switch
            {
                ProjectileStepStartFrom.FromStart => _startPosition,
                ProjectileStepStartFrom.FromCaster => caster.transform.position,
                ProjectileStepStartFrom.FromHit => _lastHitPosition == default ? _lastHitPosition : _startPosition,
                _ => throw new ArgumentOutOfRangeException(
                    nameof(_currentStep.startFrom), _currentStep.startFrom, null)
            };
        }

        /// <summary>
        /// Initializes and starts whole projectile.
        /// </summary>
        /// <param name="index">Index of this projectile from source ability</param>
        /// <param name="caster">UnitObject that fired this projectile</param>
        /// <param name="startPosition">Projectile starting position</param>
        /// <param name="targetPosition">Projectile target position</param>
        /// <param name="steps">Definition HOW projectile should move and hit</param>
        public void StartProjectile(
            int index,
            UnitObject caster,
            Vector3 startPosition,
            Vector3 targetPosition,
            ProjectileStep[] steps
        )
        {
            this.index = index;
            this.caster = caster;
            this.steps = steps;
            _startPosition = startPosition;
            _targetPosition = targetPosition;
            
            // Projectile GameObject
            _gameObject = gameObject;
            _gameObject.layer = LayerManager.TriggerIndex;
            _gameObject.transform.position = _startPosition;
            _gameObject.SetActive(true);

            // Projectile AOE GameObject
            _aoeGameObject = new GameObject("Projectile AOE collider");
            _aoeGameObject.layer = LayerManager.TriggerIndex;
            _aoeGameObject.transform.position = gameObject.transform.position;
            
            // Projectile AOE controlling component
            var projectileAoe = _aoeGameObject.AddComponent<ProjectileAoe>();
            projectileAoe.OnAoeEnter += OnAoeEnter;
            projectileAoe.OnAoeExit += OnAoeExit;
            
            onStart.Invoke(this);

            PrepareNextStep();
        }

        /// <summary>
        /// Ends and destroy projectile.
        /// </summary>
        private void EndProjectile()
        {
            onEnd.Invoke(this);
            
            // Destroying only projectile's AOE and script, not whole projectile yet!
            Destroy(this);
            Destroy(_aoeGameObject);
            
            // Wait a little bit for other things like VFX to finish then we can safely destroy whole projectile
            Destroy(_gameObject, 3);
        }

        /// <summary>
        /// Checking if given target should be hit and executed OnHit.
        /// </summary>
        private bool IsValidHit(UnitObject caster, AbstractObject target)
        {
            return _currentStep.validHits.Count == 0 ||
                   _currentStep.validHits.Any(validator => validator.IsValid(caster, target));
        }

        /// <summary>
        /// Collision enter detected by projectile AOE.
        /// </summary>
        private void OnAoeEnter(Collider other)
        {
            if (!LayerManager.CodeInMask(other.gameObject.layer, LayerManager.Dynamic)) return;
            
            var hitObject = other.GetComponentInParent<AbstractObject>();
            if (!hitObject) return;

            if (!IsValidHit(caster, hitObject)) return;

            _aoeCollisions.TryAdd(hitObject, 0);
            _aoeCollisions[hitObject]++;
            if (_currentStep.hitType == ProjectileHitType.Always) onHit.Invoke(this, hitObject);
        }

        /// <summary>
        /// Collision exit detected by projectile AOE.
        /// </summary>
        private void OnAoeExit(Collider other)
        {
            if (!LayerManager.CodeInMask(other.gameObject.layer, LayerManager.Dynamic)) return;
            
            var hitObject = other.GetComponentInParent<AbstractObject>();
            if (!hitObject) return;

            if (!_aoeCollisions.ContainsKey(hitObject)) return;
            
            _aoeCollisions[hitObject]--;

            if (_aoeCollisions[hitObject] == 0) _aoeCollisions.Remove(hitObject);
        }
    }
}