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

namespace Assets.PointDistribution
{
    public class PoissonDiskSampler : IPointDistribution
    {
        public System.Random Generator { get; set; } = new System.Random();
        
        private int k;
        private double r;

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

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

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

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

            void AddToGrid(Point point)
            {
                grid.Add(
                    ((int)Math.Floor(point.x / size), (int)Math.Floor(point.y / size)),
                    point
                );

                active.Add(point);
            }

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

                var x = (int)Math.Floor(point.x / size);
                var y = (int)Math.Floor(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 => Point.Dist(point, grid[p]) > r);
            }

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

                return origin + new Point(Math.Sin(angle), Math.Cos(angle)) * length;
            }

            AddToGrid(initial);
            yield return initial;

            int watchdog = 3000000;

            while (active.Count > 0 && watchdog-- > 0)
            {
                var current = (int)Math.Floor(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);
            }
        }
    }
}