using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;

public sealed class Army : MonoBehaviour
{
    [SerializeField]
    private VehicleBlueprint coreBlueprint = new VehicleBlueprint();
    [SerializeField]
    private HitProvider hitProvider = null;
    [SerializeField]
    private float fireRange = 3f;
    [SerializeField]
    private EffectController hitEffectController = null;
    [SerializeField]
    private EffectController deathEffectController = null;
    [SerializeField]
    private Vector3[] points = new Vector3[0];
    [SerializeField]
    private ArmyDrawer drawer = null; // set from editor

    // control collider size for utility 
    [SerializeField]
    private float minColliderRadius = 0.5f;
    [SerializeField]
    private SphereCollider controlCollider = null;

    [Header("grid params")]
    [SerializeField]
    private int count = 500;
    [SerializeField]
    private float gridScale = 0.2f;
    [SerializeField]
    private float centralEmptyRadius = 0.1f;
    [SerializeField]
    private int restorePerSecond = 2;
    [SerializeField]
    private float restoreTime = 1f;
    [SerializeField]
    private BulletEffectController bulletFireEffect = null;

    private float restoreTimer = 0;

    private bool IsAlive => activeVehicles.Count > 0 || CoreVehicle.IsAlive;
    public int Lives => activeVehicles.Count + 1;
    public int FactionId { get; private set; }

    public float FireRange => fireRange;
    public float Radius { get; private set; }

    private Vehicle CoreVehicle;

    private List<Vehicle> activeVehicles = new List<Vehicle>();
    private List<Vehicle> unitsPlacement = new List<Vehicle>();

    private List<Bullet> bullets = new List<Bullet>();
    private float angle = 0;

    private float reloadTimer = 0;

    private void Awake()
    {
        CoreVehicle = new Vehicle(coreBlueprint, 0);
        CoreVehicle.SetOffset(Vector3.zero);
    }

    public void Init(int newFactionId, Color color)
    {
        FactionId = newFactionId;
    }

    public void SetAngle(float newAngle)
    {
        angle = newAngle;
    }

    public void AddUnit(Vehicle vehicle) {
        activeVehicles.Add(vehicle);
        SetUnitPlace(vehicle);
    }

    private void SetUnitPlace(Vehicle vehicle)
    {
        for (int i = 0; i < unitsPlacement.Count; i++)
        {
            if (unitsPlacement[i] == null || !unitsPlacement[i].IsAlive)
            {
                unitsPlacement[i] = vehicle;
                vehicle.SetOffset(points[i]);
                UpdateControlRadius();
                return;
            }
        }
        vehicle.SetOffset(points[unitsPlacement.Count]);
        unitsPlacement.Add(vehicle);
        UpdateControlRadius();
    }

    private void UpdateControlRadius()
    {
        Radius = FindMinimalRadius();
        if (controlCollider != null)
            controlCollider.radius = Mathf.Max(Radius, minColliderRadius);
    }

    private float FindMinimalRadius()
    {
        for (int i = unitsPlacement.Count - 1; i > 0; i--)
        {
            if (unitsPlacement[i] != null && unitsPlacement[i].IsAlive)
                return points[unitsPlacement.Count].magnitude;
        }
        return 0;
    }

    private void OnEnable() => hitProvider.Add(this);
    private void OnDisable() => hitProvider.Remove(this);

    private void Update()
    {
        ProcessFire();
        ProcessDamage();
        ProcessRestore();

        drawer.DrawWithMain(transform.position, angle, unitsPlacement, CoreVehicle, FactionId);
        if (!IsAlive) {
            Destroy(gameObject);
        }
    }

    private void ProcessRestore()
    {
        restoreTimer += Time.deltaTime;
        if (restoreTimer > restoreTime) {
            restoreTimer -= restoreTime;
            foreach (var vehicle in activeVehicles) {
                vehicle.Restore(restorePerSecond);
            }
        }
    }

    private void ProcessFire()
    {
        reloadTimer += Time.deltaTime;
        var enemy = hitProvider.Armies
            .Where(a => a.FactionId != FactionId)
            .Where(a => a.IsAlive)
            .Where(a => Vector3.Distance(a.transform.position, transform.position) < fireRange).FirstOrDefault();
        if (enemy == null)
        {
            CoreVehicle.Reload(reloadTimer);
            foreach (var unit in activeVehicles)
                unit.Reload(reloadTimer);

            return;
        }
        var distance = Vector3.Distance(transform.position, enemy.transform.position);
        Fire(enemy, distance, CoreVehicle);
        foreach (var unit in activeVehicles)
        {
            Fire(enemy, distance, unit);
        }
    }

    private void Fire(Army enemy, float distance, Vehicle unit)
    {
        if (unit.Reload(reloadTimer))
        {
            var bullet = unit.FireBullet();
            var lifeTime = distance / bullet.Speed;
            var from = transform.position + unit.Offset;
            var to = enemy.GetRandomPoint();
            enemy.bullets.Add(new Bullet(bullet, lifeTime, from, to));
            bulletFireEffect.Fire(from, to, bullet.Speed);
        }
    }

    private Vector3 GetRandomPoint()
    {
        if (activeVehicles.Count > 0)
        {
            var unitId = Random.Range(0, activeVehicles.Count);
            return transform.position + activeVehicles[unitId].Offset;
        }
        return transform.position;
    }

    private void ProcessDamage()
    {
        foreach (var bullet in bullets)
        {
            if (bullet.ProcessHit(Time.deltaTime)) {
                HitNextUnit(bullet.Blueprint, bullet.End);
            }
        }
        bullets.RemoveAll(bullet => !bullet.IsAlive);
        activeVehicles.RemoveAll(unit => !unit.IsAlive);
    }

    private void HitNextUnit(BulletBlueprint bullet, Vector3 hitPoint)
    {
        Vehicle unit;
        if (activeVehicles.Count > 0)
        {
            var unitId = Random.Range(0, activeVehicles.Count);
            unit = activeVehicles[unitId];
        }
        else
        {
            unit = CoreVehicle;
        }
        unit.Damage(bullet);
        if (!unit.IsAlive)
        {
            deathEffectController?.Spawn(unit.Offset + transform.position);
        }
        else
        {
            hitEffectController?.Spawn(hitPoint + new Vector3(0, 0.1f));
        }        
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.DrawWireSphere(transform.position, fireRange);
            DrawPointsGizmos();
    }

    [SerializeField]
    private int showCount = 20;

    private void DrawPointsGizmos()
    {
        if (points == null)
            return;
        for (int i = 0; i < points.Length; i++)
        {
            Gizmos.color = (i < showCount) ? Color.green : Color.white;
            Gizmos.DrawWireSphere(transform.position + points[i], 0.02f);
        }
    }

    [ContextMenu("Generate diamond distribution")]
    private void GenerateDiamondDistrinbution()
    {
        points = new Vector3[count];
        var gridPoints = new List<Vector3>();
        var gridSize = Mathf.CeilToInt(Mathf.Sqrt(count) * 0.7f);
        for (int i = -gridSize; i <= gridSize; i++)
        {
            for (int j = -gridSize; j <= gridSize; j++) {
                var point = new Vector3((i + j * 0.5f) * gridScale, 0, (j * 0.894f) * gridScale);
                if (point.magnitude >= centralEmptyRadius)
                    gridPoints.Add(point);
            }
        }
        gridPoints.Sort((a, b) => {
            if (a.magnitude < b.magnitude)
                return -1;
            return 1;
        });
        for (int i = 0; i < points.Length; i++) {
            points[i] = gridPoints[i];
        }
    }

    [ContextMenu("Generate square distribution")]
    private void GenerateSquareDistrinbution()
    {
        points = new Vector3[count];
        var gridPoints = new List<Vector3>();
        var gridSize = Mathf.CeilToInt(Mathf.Sqrt(count) * 0.7f);
        for (int i = -gridSize; i <= gridSize; i++)
        {
            for (int j = -gridSize; j <= gridSize; j++)
            {
                var point = new Vector3(i * gridScale, 0, j * gridScale);
                if (point.magnitude >= centralEmptyRadius)
                    gridPoints.Add(point);
            }
        }
        gridPoints.Sort((a, b) => {
            if (a.magnitude < b.magnitude)
                return -1;
            return 1;
        });
        for (int i = 0; i < points.Length; i++)
        {
            points[i] = gridPoints[i];
        }
    }
}
