using System.Collections.Generic;
using System.Linq;
using Assets.Common;
using Assets.PointDistribution;
using Assets.Voronoi;

namespace Assets.GridGenerator
{
    public class VoronoiGridGenerator : IGridGenerator
    {
        public const string VoronoiSiteProperty = "VoronoiSite";

        private readonly int _seed;
        private readonly IPointDistribution _sampler;

        public VoronoiGridGenerator(int seed, IPointDistribution sampler)
        {
            _seed = seed;
            _sampler = sampler;
        }

        public Map CreateGrid(double width, double height)
        {
            Map result = new Map(_seed);

            var points = GeneratePoints(width, height);
            var generator = new VoronoiGenerator(points);
            
            generator.Generate();
            
            bool IsOutside(Point point) => !(point.x > 0 && point.x < width && point.y > 0 && point.y < height);

            result.Boundaries = generator.Voronoi.Clone() as Graph<Point>;
            result.Sites = generator.Delaunay.Morph(s =>
            {
                var site = new MapSite(s.Point, ClockwiseVertices(generator.Voronoi, s.Vertices));
                
                site.Metadata.SetProperty(VoronoiSiteProperty, s);

                if (site.Boundary.Select(n => result.Boundaries[n]).Any(IsOutside))
                    site.Tags.Add(CommonTags.Edge);

                return site;
            });

            foreach (var site in result.Sites.Vertices.Take(4))
                site.Tags.Add(CommonTags.Outer);

            FixTooLongEdges(result, width, height);
            
            return result;
        }

        private void FixTooLongEdges(Map map, double width, double height)
        {
            List<(int, int)> remove = new List<(int, int)>();

            bool IsValid(Point point) => point.x > 0 && point.x < width && point.y > 0 && point.y < height;

            foreach (var (a, b) in map.Boundaries.Edges)
            {
                var pointA = map.Boundaries[a];
                var pointB = map.Boundaries[b];
                
                if (!IsValid(pointA) || !IsValid(pointB))
                    remove.Add((a, b));
            }

            foreach (var (a, b) in remove)
                map.Boundaries.DeleteEdge(a, b);
        }

        private List<Point> GeneratePoints(double width, double height)
        {
            var points = new List<Point>
            {
                new Point(-width / 8, height / 2 + 0.1f),
                new Point(width * 1.125f, height / 2 - 0.1f),
                new Point(width / 2 - 0.1f, -height / 8),
                new Point(width / 2 + 0.1f, height * 1.125f)
            };

            points.AddRange(_sampler.Generate(width, height));
            
            return points;
        }

        private IEnumerable<int> ClockwiseVertices(Graph<Point> graph, IEnumerable<int> vertices)
        {
            var enumerated = vertices as int[] ?? vertices.ToArray();
            
            var center = PointUtils.Mean(enumerated.Select(n => graph[n]));
            var reference = center + new Point(0, -100);
            
            return enumerated
                .Select(n => (Vertex: n, Point: graph[n]))
                .Select(x => (Vertex: x.Vertex, Angle: PointUtils.AngleBetween(center, reference, x.Point)))
                .OrderBy(x => x.Angle)
                .Select(x => x.Vertex);
        }
    }
}