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

namespace Assets.Generators
{
    public class PoissonDiskSampler
    {
        public System.Random Generator { get; set; } = new System.Random();
        
        private int k;
        private float r;

        private float Random(float max, float min = 0)
        {
            return (float)Generator.NextDouble() * (max - min) + min;
        }
        
        public PoissonDiskSampler(float r, int k = 30)
        {
            this.k = k;
            this.r = r;
        }

        public IEnumerable<Vector3> Generate(float width, float height)
        {
            var size = r / Mathf.Sqrt(2);

            var grid = new Dictionary<(int, int), Vector3>();
            var active = new List<Vector3>();

            var initial = new Vector3(Random(width), Random(height));

            void AddToGrid(Vector3 point)
            {
                grid.Add(
                    (Mathf.FloorToInt(point.x / size), Mathf.FloorToInt(point.y / size)),
                    point
                );

                active.Add(point);
            }

            bool IsPointOk(Vector3 point)
            {
                if (point.x < 0 || point.y < 0 || point.x > width || point.y > height)
                {
                    return false;
                }

                var x = Mathf.FloorToInt(point.x / size);
                var y = Mathf.FloorToInt(point.y / size);

                var neighbours = (new List<(int, int)>() {
                    (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),
                    (x - 1, y),     (x, y),     (x + 1, y),
                    (x - 1, y - 1), (x, y - 1), (x + 1, y - 1)
                });

                return neighbours
                    .Where(p => grid.ContainsKey(p))
                    .All(p => Vector3.Distance(point, grid[p]) > r);
            }

            Vector3 RandomPoint(Vector3 origin)
            {
                var angle = Random(Mathf.PI * 2);
                var length = Random(2 * r, r);

                return origin + new Vector3(Mathf.Sin(angle), Mathf.Cos(angle)) * length;
            }

            AddToGrid(initial);
            yield return initial;

            int watchdog = 3000000;

            while (active.Count > 0 && watchdog-- > 0)
            {
                var current = Mathf.FloorToInt(Random(active.Count));
                var point = active[current];

                var i = 0;
                for (; i < k; i++)
                {
                    var candidate = RandomPoint(point);

                    if (IsPointOk(candidate))
                    {
                        AddToGrid(candidate);
                        yield return candidate;
                        break;
                    }
                }

                if (i >= k)
                {
                    active.RemoveAt(current);
                }
            }
        }
    }
}