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

namespace Assets.AnnotationPass
{
    public class CityFieldsPass : IAnnotationPass
    {
        private Random _random;
        
        private Graph<Point> _voronoiGraph;

        public double MinimumAngle { get; set; }
        public double MaxNudgeDistance { get; set; } = .25;
        public double MinNudgeDistance { get; set; } = .75;

        public double MinimumRoadLength { get; set; } = 2;

        public CityFieldsPass(Random random)
        {
            _random = random;
        }

        private bool IsPointInsideCity(City city, Point point)
        {
            return city.Sites.Any(s => PointUtils.IsPointInside(point, s.Boundary.Select(i => _voronoiGraph[i]).ToArray()));
        }

        private void AnnotateCity(City city)
        {
            CreateRoadGraph(city, _voronoiGraph.Vertices);
            RemoveIncorrectEdges(city);
            ConnectPointsIntoRoads(city);
            FixRoadsDensity(city, MinimumAngle);
            CreateFieldBoundaries(city);
            CreateCityFields(city);
            RemoveTooShortRoads(city);
            NudgeCityFields(city);
        }

        private void RemoveTooShortRoads(City city)
        {
            foreach (var field in city.Fields)
            {
                field.Boundary = field.Boundary.Aggregate(new List<Point>(), (boundary, point) =>
                {
                    if (boundary.Count == 0)
                    {
                        boundary.Add(point);
                    }
                    else
                    {
                        var last = boundary[boundary.Count - 1];

                        if (Point.Dist(last, point) > MinimumRoadLength)
                            boundary.Add(point);
                        else
                            boundary[boundary.Count - 1] = (point + last) / 2;
                    }

                    return boundary;
                });

                var f = field.Boundary.First();
                var l = field.Boundary.Last();
                
                if (Point.Dist(l, f) < MinimumRoadLength)
                {
                    field.Boundary[0] = (f + l) / 2;
                    field.Boundary.Remove(l);
                }
            }

            city.Fields = city.Fields.Where(field => field.Boundary.Count > 2).ToList();
        }

        private void NudgeCityFields(City city)
        {
            foreach (var field in city.Fields)
            {
                var nudge = _random.NextDouble() * MaxNudgeDistance + MinNudgeDistance;
                field.Boundary = field.Boundary.Zip(PointUtils.CalculateNormals(field.Boundary), (a, b) => (Point: a, Normal: b)).Select(tuple => tuple.Point - tuple.Normal * nudge).ToList();
            }
        }

        private void CreateCityFields(City city)
        {
            var edges = city.FieldBoundaries.Edges.Distinct().ToList();
            edges = edges.Concat(edges.Select(tuple => (tuple.Item2, tuple.Item1))).ToList();

            int GetNext(int current, int previous)
            {
                var center    = city.FieldBoundaries[current];
                var reference = center + new Point(0, 10);

                var neighbours = city.FieldBoundaries
                    .Neighbours(current)
                    .Select(n => (Vertex: n, Angle: PointUtils.AngleBetween(center, reference, city.FieldBoundaries[n])))
                    .OrderBy(tuple => tuple.Angle)
                    .Select(tuple => tuple.Vertex)
                    .ToList();

                return neighbours[(neighbours.FindIndex(n => n == previous) + 1) % neighbours.Count];
            }

            CityField CreateField(int start, int next)
            {
                CityField field = new CityField();

                int watchdog = 100;
                int current  = start;
 
                do
                {
                    field.Boundary.Add(city.FieldBoundaries[current].Clone() as Point);
                    edges.Remove((current, next));
                    (current, next) = (next, GetNext(next, current));
                } while (current != start && watchdog-- > 0);

                return field;
            }

            while (edges.Count > 0)
            {
                var (a, b) = edges[0];
                edges.RemoveAt(0);
                
                city.Fields.Add(CreateField(a, b));
            }

            // remove outside field
            city.Fields.RemoveAll(field => !PointUtils.IsClockwise(field.Boundary));
        }

        public void Annotate(Map map)
        {
            _voronoiGraph = map.Boundaries;
            
            var cities = map.Metadata.GetProperty<List<City>>(LocateCitiesPass.CitiesProperty);

            foreach (var city in cities)
                AnnotateCity(city);
        }

        private void RemoveIncorrectEdges(City city)
        {
            var edges = city.Roads.Edges.ToList();
            foreach (var (a, b) in edges)
            {
                var (va, vb) = (city.Roads[a], city.Roads[b]);
                var center = (va + vb) / 2;

                if (!IsPointInsideCity(city, center))
                    city.Roads.DeleteEdge(a, b);
            }
        }

        private void CreateRoadGraph(City city, IList<Point> points)
        {
            var pointsForRoads = city.Sites.SelectMany(site => site.Boundary.Select(i => (i, points[i])).Append((-1, site.Center))).Distinct().ToList();
            var mapping = pointsForRoads.Select((x, i) => x.Item1 == -1 ? (-1, -1) : (i, x.Item1)).Where(x => x.Item1 != -1).ToDictionary(x => x.Item2, x => x.Item1);

            VoronoiGenerator generator = new VoronoiGenerator(pointsForRoads.Select(x => x.Item2));
            generator.Generate();

            city.Roads = generator.Delaunay.Morph(s => s.Point);

            city.FieldBoundaries = new Graph<Point> { Vertices = city.Roads.Vertices, Edges = city.Edges.Select(e => (mapping[e.Item1], mapping[e.Item2])) };
        }

        private void ConnectPointsIntoRoads(City city)
        {
            var original = city.Roads.Morph(s => s, e => Point.Dist(city.Roads[e.Item1], city.Roads[e.Item2]));

            //1.Sort all the edges in non - decreasing order of their weight.
            //2.Pick the smallest edge.Check if it forms a cycle with the spanning tree formed so far. If cycle is not formed, include this edge.Else, discard it.
            //3.Repeat step#2 until there are (V-1) edges in the spanning tree.

            var edges = original.Edges.OrderByDescending(e => original.GetEdgeData(e));

            Graph<Point> roads = new Graph<Point>() { Vertices = city.Roads.Vertices };

            foreach (var edge in original.Edges)
            {
                roads.AddEdge(edge.Item1, edge.Item2);

                if (Graph<Point>.HasCycle(roads))
                    roads.DeleteEdge(edge.Item1, edge.Item2);
                if (roads.Edges.Count() == roads.Vertices.Count() - 1)
                    break;
            }

            city.Roads = roads;
        }

        private void CreateFieldBoundaries(City city)
        {
            foreach (var (a, b) in city.Roads.Edges)
                city.FieldBoundaries.AddEdge(a, b);

            var deadEnds = city.FieldBoundaries.Vertices.Select((_, i) => i).Where(i => city.FieldBoundaries.Neighbours(i).Count() == 1);

            foreach (var deadEnd in deadEnds)
            {
                try
                {
                    var neighbour = city.FieldBoundaries.Neighbours(deadEnd).First();
                    var closest = city.FieldBoundaries.Vertices
                        .Select((_, i) => i)
                        .OrderBy(i => Point.Dist(city.FieldBoundaries[i], city.FieldBoundaries[deadEnd]))
                        .Skip(1)
                        .First(c => 
                            c != neighbour && 
                            PointUtils.AngleBetween(city.FieldBoundaries[deadEnd],city.FieldBoundaries[c], city.FieldBoundaries[neighbour]) > MinimumAngle
                        );

                    city.FieldBoundaries.AddEdge(deadEnd, closest);
                    city.Roads.AddEdge(deadEnd, closest);
                }
                catch (InvalidOperationException e)
                {
                }
            }
        }


        private void FixRoadsDensity(City city, double angle = 0.25f)
        {
            // dla każdego punktu w grafie dróg
            // chyba że ma 2 to pomiń?
            // wyznacz punkt 0
            // oblicz odległości kątowe do wszystkich punktów
            // posortuj je
            // między wszystkimiu parami oblicz kąt
            // potem według jakiegoś parametru usuń te, które są za bliskie


            for (int i = 0; i < city.Roads.Vertices.Count(); i++)
            {
                var p = city.Roads[i];
                if (city.Roads.Neighbours(i).Count() <= 2)
                    continue;

                var reference = new Point(p.x, p.y + 30);

                var orderedNeighours = city.Roads.Neighbours(i)
                    .Select(n => (Vertex: n, Point: city.Roads[n]))
                    .Select(x => (Vertex: x.Vertex, Angle: PointUtils.AngleBetween(p, reference, x.Point)))
                    .OrderBy(x => x.Angle)
                    .Select(x => x.Vertex);

                foreach (var (a, b) in orderedNeighours.RotateRight(1).Zip(orderedNeighours, (a, b) => (a, b)))
                {
                    if( PointUtils.AngleBetween(p, city.Roads[a], city.Roads[b]) < angle)
                    {
                        city.Roads.DeleteEdge(i, a);
                    }
                }
            }
        }
    }
}