using System;
using System.Collections.Generic;
using System.Linq;
using UniRx;
using UnityEngine;
using UnityEngine.Tilemaps;

namespace Hazards
{
    public class IgnitableManager : MonoBehaviour
    {
        private List<GameObject> ignitableAreaObjects;
        private List<TilemapRenderer> tilemaps = new();
        private List<Bounds> boundsList = new();
        private List<Vector3> circleCenterPoints = new();
        private List<CircleCollider2D> circleColliders = new();
        private List<Ignitable> ignitables = new();
        
        [SerializeField] private float ignitableRadius;
        [SerializeField] private float radiusFactorForOverlap = 1.1f;
        [SerializeField] private GameObject ignitablePrefab;

        public static IgnitableManager Instance { get; private set; }

        private void Awake()
        {
            if (Instance == null)
            {
                Instance = this;
            }
            else
            {
                Destroy(gameObject);
                Destroy(this);
            }
            
            GetAllIgnitableAreaObjects();
            GenerateCentersByCirclePacking();
            
            // in AddNeighbours we do a physics query, so we need to sync the transforms, otherwise the transforms do not update soon enough
            Physics2D.SyncTransforms(); 
            
            AddNeighbours();

        }

        private void Start()
        {
            FireLoopSfxSubscription();
        }

        public void FireLoopSfxSubscription()
        {
            var anyIgnited = ignitables
                .Select(i => i.isIgnited)
                .CombineLatest()
                .Select(values => values.Any(v => v))
                .DistinctUntilChanged();

            // Start loop with 1 second delay when any ignites
            anyIgnited
                .Where(x => x)
                .Delay(TimeSpan.FromSeconds(0.2f))
                .Subscribe(_ => AudioManager.Instance.fireSfxSource.Play())
                .AddTo(this);

            // Stop only when ALL are false
            anyIgnited
                .Where(x => !x)
                .Subscribe(_ => AudioManager.Instance.fireSfxSource.Stop())
                .AddTo(this);
        }
        
        private void GetAllIgnitableAreaObjects()
        {
            ignitableAreaObjects = GameObject.FindGameObjectsWithTag("IgnitableArea").ToList();
        }
        
        void OnDrawGizmos()
        {
            Gizmos.color = Color.cyan;
            foreach (var bounds in boundsList)
            {
                Gizmos.DrawWireCube(bounds.center, bounds.size);   
            }
            
            Gizmos.color = Color.red;

            foreach (var centerPoint in circleCenterPoints)
            {
                Gizmos.DrawWireSphere(centerPoint, ignitableRadius);
            }
            
        }
        
        /// <summary>
        /// Following the idea of circle packing for larger objects, that are supposed to be ignitable
        /// mentioned here: https://en.wikipedia.org/wiki/Circle_packing
        /// Using the hex version.
        /// </summary>
        public void GenerateCentersByCirclePacking()
        {
            // first add all the bounds to the list, from the TilemapRenderer objects
            foreach (var ignitableObject in ignitableAreaObjects)
            {
                var tilemap = ignitableObject.GetComponent<TilemapRenderer>();
                tilemaps.Add(tilemap);
                boundsList.Add(tilemap.bounds);
            }
            
            float dx = 2f * ignitableRadius;
            float dy = Mathf.Sqrt(3f) * ignitableRadius; 

            foreach (var bounds in boundsList)
            {
                Vector3 min = bounds.min;
                Vector3 max = bounds.max; 

                    float xMin = min.x + ignitableRadius;
                    float xMax = max.x - ignitableRadius;
                    float yMin = min.y + ignitableRadius;
                    float yMax = max.y - ignitableRadius;

                    if (xMin > xMax || yMin > yMax) continue;

                    int row = 0;
                    // due to precision we add 1e-6f to the for-loop bounds
                    for (float y = yMin; y <= yMax + 1e-6f; y += dy, row++)
                    {
                        float xOffset = (row & 1) == 1 ? ignitableRadius : 0f;
                        for (float x = xMin + xOffset; x <= xMax + 1e-6f; x += dx)
                        {
                            Vector3 centerPos = new Vector3(x, y, bounds.center.z);
                            circleCenterPoints.Add(centerPos);
                            GameObject ignitableObject = Instantiate(ignitablePrefab);
                            
                            CircleCollider2D circleCollider2D = ignitableObject.GetComponent<CircleCollider2D>();
                            Ignitable ignitable = ignitableObject.GetComponent<Ignitable>();
                            
                            circleColliders.Add(circleCollider2D);
                            ignitables.Add(ignitable);
                            
                            circleCollider2D.radius = ignitableRadius * radiusFactorForOverlap;
                            ignitableObject.transform.position = centerPos;
                        }
                    }
            }
        }


        private void AddNeighbours()
        {
            var filter = new ContactFilter2D();
            filter.useTriggers = true;
            filter.SetLayerMask(~0); // uses all layers, however may be changed if needed
            
            for (int i = 0; i < ignitables.Count; i++)
            {
                Ignitable ignitable = ignitables[i];
                CircleCollider2D circle = circleColliders[i];

                Collider2D[] hits = new Collider2D[4];
                
                int count = circle.Overlap(filter, hits); 

                for (int h = 0; h < count; h++)
                {
                    Collider2D hit = hits[h];
                    if (!hit) continue;

                    // Tag check (fast + validates tag exists)
                    if (!hit.CompareTag("Ignitable")) continue; 

                    // Get the Ignitable component from the overlapped object
                    var neighbour = hit.GetComponent<Ignitable>();
                    if (!neighbour) continue;

                    // Avoid adding self
                    if (neighbour == ignitable) continue;

                    ignitable.AddIgnitableNeighbour(neighbour);
                }
            }
        }
        
    }
}
