using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.Jobs;
using Utils;
using Weapons.Scripts;
using Weapons.Spawning;
using Random = UnityEngine.Random;

[CreateAssetMenu]
public class Weapon : ScriptableObject
{
    [BurstCompile]
    private struct BulletMoveJob : IJobParallelForTransform
    {
        public float DeltaTime;
        public NativeArray<Bullet> Bullets;

        public void Execute(int index, TransformAccess transform)
        {
            if (Bullets[index].IsAlive)
                transform.position += Bullets[index].Direction * Bullets[index].Speed * DeltaTime;
        }
    }

    private struct Bullet
    {
        public Vector3 Direction;
        public Vector3 PrevPos;
        public float Speed;
        public float Lifetime;
        public bool IsAlive;
        public int Idx;
    }

    public event Action<Vector3> BulletCollision;

    [SerializeField] private float fireRate;
    [SerializeField] private ParticleSystem.MinMaxCurve bulletSpeed;
    [SerializeField] private ParticleSystem.MinMaxCurve bulletLifetime;
    [SerializeField] private ParticleSystem.MinMaxCurve bulletSize;
    [SerializeField] private float damage;
    [SerializeField] private LayerMask collidesWith;
    [SerializeField] private float accuracy;
    [SerializeField] private SpawnZone zone;
    [SerializeField] private bool manualFire;
    [SerializeField] private BaseBulletBehaviour behaviour;
    
    private List<Bullet> _bullets;
    private List<Transform> _bulletTransforms;
    private static Collider[] _results = new Collider[32];

    private ObjectPool<Transform> _pool;
    
    private BulletMoveJob _job;
    private JobHandle _jobHandle;

    private Bullet _currentBullet;
    private Transform _currentTransform;
    private TransformAccessArray _accessArray;

    private float _currentCooldown;
    
    public void Init()
    {
        _job = new BulletMoveJob();
        _bullets = new List<Bullet>();
        _bulletTransforms = new List<Transform>();
        _currentCooldown = fireRate;
    }

    public void Update()
    {
        _currentCooldown += Time.deltaTime;

        for (int i = 0; i < _bulletTransforms.Count; i++)
        {
            _currentBullet = _bullets[i];
            _currentBullet.PrevPos = _bulletTransforms[i].position;
            _bullets[i] = _currentBullet;
        }
        
        _job.DeltaTime = Time.deltaTime;
        _job.Bullets = _bullets.ToNativeArray(Allocator.Persistent);

        _accessArray = new TransformAccessArray(_bulletTransforms.ToArray());
        _jobHandle = _job.Schedule(_accessArray);
    }

    public void LateUpdate()
    {
        _jobHandle.Complete();
        _job.Bullets.Dispose();
        _accessArray.Dispose();
        // remove all the inactive bullets from our internal list so we don't accidentally update them multiple times
        for (var i = _bullets.Count - 1; i >= 0; i--)
        {
            _currentBullet = _bullets[i];
            _currentBullet.Lifetime -= Time.deltaTime;
            if (_currentBullet.Lifetime <= 0)
                _currentBullet.IsAlive = false;

            
            _bullets[i] = _currentBullet;
            
            if (_bullets[i].IsAlive && _bulletTransforms[i].gameObject.activeSelf) continue;
            
            _bulletTransforms[i].gameObject.SetActive(false);
            _pool.ReturnObject(_bulletTransforms[i], _bullets[i].Idx);
            _bullets.RemoveAt(i);
            _bulletTransforms.RemoveAt(i);
        }
    }

    public void FixedUpdate()
    {
        // loop through the bullets. Backwards, because that is marginally quicker
        for (var i = _bullets.Count - 1; i >= 0; i--)
        {
            _currentBullet = _bullets[i];
            _currentTransform = _bulletTransforms[i];
            
            // if the bullet is inactive, continue. We don't care about it
            if (!_currentTransform.gameObject.activeSelf || !_currentBullet.IsAlive) continue;

            // if it has hit something
            if (CheckCollision(_currentTransform, _currentBullet.PrevPos, _currentTransform.localScale.x, out var numHits))
            {
                // send the event
                BulletCollision?.Invoke(_currentTransform.position);
                
                // damage any damageable entities it has hit
                for (int j = 0; j < numHits; j++)
                {
                    var healthObject = _results[j].GetComponent<IDamageable>();
                    healthObject?.Hit(damage);
                }
                
                // deactivate the bullet
                _currentBullet.IsAlive = false;
                _currentTransform.gameObject.SetActive(false);
                _pool.ReturnObject(_currentTransform, _currentBullet.Idx);
                _bullets.RemoveAt(i);
                _bulletTransforms.RemoveAt(i);
            }
            
            // apply the changes we made
            _bullets[i] = _currentBullet;
        }
    }

    private bool CheckCollision(Transform instance, Vector3 prevPos, float size, out int numHits)
    {
        numHits = 0;
        
        // if it's inactive, we haven't hit anything
        if (!instance.gameObject.activeSelf) return false;
        
        // use the non allocating version so we don't have to allocate memory for every bullet
        numHits = Physics.OverlapCapsuleNonAlloc(prevPos, instance.position, size/2f, _results, collidesWith, QueryTriggerInteraction.Collide);

        return numHits > 0;
    }
    
    public bool Fire(ObjectPool<Transform> pool, Transform position)
    {
        if (_pool == null)
            _pool = pool;
        
        if (!manualFire)
        {
            if (_currentCooldown < fireRate) return false;
            _currentCooldown = 0;
        }
        
        SpawnBullets(position);
        return true;
    }
    
    /// <summary>
    /// Spawn the bullets.
    /// </summary>
    private void SpawnBullets(Transform transform)
    {
        // alter the direction based on accuracy
        var direction = Quaternion.Euler(0, Random.Range(-accuracy, accuracy), 0) * transform.forward;

        // for every bullet
        zone.GetPoint(transform, (point, dir) =>
        {
            // get the object in the pool
            var (bullet, idx) = _pool.GetObject();
            // if the pool returns no bullet, continue (it probably hasn't initialised yet)
            if (bullet == null) return;

            // enable the bullet
            bullet.gameObject.SetActive(true);

            var newPos = point;
            bullet.position = transform.position + newPos;
            
            // point the bullet in the right direction
            bullet.forward = dir;
            bullet.transform.localScale = Vector3.one * bulletSize.EvaluateMinMaxCurve();
            behaviour.DoBehaviour(bullet, bulletSize.EvaluateMinMaxCurve(), bullet.position);
            if (zone.SpawnDir != SpawnDir.Spherised)
            {
                var y = bullet.eulerAngles.y;
                bullet.forward = Quaternion.Euler(0, y, 0) * direction;
            }

            if (zone.SpawnDir == SpawnDir.None)
                bullet.forward = direction;
            // add the bullet to the list we're returning
            
            _bulletTransforms.Add(bullet);
            _bullets.Add(new Bullet
            {
                Direction = bullet.forward,
                PrevPos = bullet.position,
                IsAlive = true,
                Speed = bulletSpeed.EvaluateMinMaxCurve(),
                Lifetime = bulletLifetime.EvaluateMinMaxCurve(),
                Idx = idx
            });
        });
    }

    /// <summary>
    /// Draw gizmos for the weapon
    /// </summary>
    public void DrawGizmos(Transform transform)
    {
        #if UNITY_EDITOR
        var color = Color.cyan;
        color.a = Selection.activeObject == this ? 1 : 0.05f;
       zone.DrawGizmos(color, transform);

       Gizmos.color = Color.white;
       if (_bullets == null) return;
       for (int i = 0; i < _bulletTransforms.Count; i++)
       {
           var b = _bulletTransforms[i];
           if (b == null) continue;
           Gizmos.DrawSphere(b.position, b.localScale.x/2f * bulletSize.EvaluateMinMaxCurve());
           Gizmos.DrawSphere(_bullets[i].PrevPos, b.localScale.x/2f * bulletSize.EvaluateMinMaxCurve());
           Gizmos.DrawLine(b.position, _bullets[i].PrevPos);
           
       }
       #endif
    }
}
