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

namespace Assets.AnnotationPass
{
    public class LandmassPass : IAnnotationPass
    {
        public const string MapLocationsProperty = "Locations";
        public const string SiteLocationProperty = "Location";
        
        private IList<Point> _points;
        private Graph<MapSite> _sites;
        
        private readonly Graph<Location> _locations;
        
        private readonly List<(int, Location)> _queue = new List<(int, Location)>();
        
        private Random _random;

        public LandmassPass(IEnumerable<Location> locations, Random random = null)
        {
            _locations = new Graph<Location> { Vertices = new List<Location>(locations) };
            _random = random ?? new Random();
        }

        private void Process(int vertex, Location location)
        {
            SetLocation(_sites.Vertices[vertex], location);

            foreach (var neighbour in _sites.Neighbours(vertex))
            {
                var site = _sites[neighbour];

                if (!site.HasProperty(SiteLocationProperty))
                {
                    if (location == _locations[0] || !site.Tags.Contains(CommonTags.Edge))
                        _queue.Add((neighbour, location));
                }
                else if (location != site.GetProperty<Location>(SiteLocationProperty))
                    _locations.AddEdge(
                        _locations.Vertices.IndexOf(location),
                        _locations.Vertices.IndexOf(site.Metadata.GetProperty<Location>(SiteLocationProperty))
                    );
            }
        }

        private void SetLocation(MapSite site, Location location)
        {
            location.AddSite(site);
            
            if (location != _locations[0])
                site.Tags.Add(CommonTags.Land);

            site.SetProperty(SiteLocationProperty, location);
            site.SetProperty(CommonSiteProperties.Height, (double)location.Type.height);
        }

        private (int, Location) Dequeue()
        {
            while (_queue.Count > 0)
            {
                var index = _random.Next(_queue.Count);
                var pair  = _queue[index];

                _queue.RemoveAt(index);

                if (!_sites[pair.Item1].HasProperty(SiteLocationProperty)) return pair;
            }
            
            throw new Exception("No more elements.");
        }
        
        private void Step()
        {
            try
            {
                var (vertex, location) = Dequeue();

                Process(vertex, location);
            }
            catch (Exception ex)
            {
                foreach (var location in _locations.Vertices.Skip(1))
                    FixLocation(location);
            }
        }

        private void FixLocation(Location location)
        {
            var center = location.Center;
            var left   = location.BoundaryEdges;
            
            location.BoundaryEdges = new List<(int, int)> { EnsureClockwise(center, left[0]) };
            (int, int) last = location.BoundaryEdges[0];
            left.RemoveAt(0);

            while (left.Count > 0)
            {
                var index = left.FindIndex(x => x.Item1 == last.Item2 || x.Item2 == last.Item2);

                if (index == -1) break;
                
                var item = left[index];
                item = item.Item1 == last.Item2 ? item : (item.Item2, item.Item1);
                
                left.RemoveAt(index);
                
                location.BoundaryEdges.Add(item);

                last = item;
            }
        }

        private (int, int) EnsureClockwise(Point center, (int, int) edge)
        {
            return PointUtils.IsClockwise(center, _points[edge.Item1], _points[edge.Item2]) ? (edge.Item2, edge.Item1) : edge;
        }
        
        public void Annotate(Map map)
        {
            _random = new Random(map.Seed);
            _sites = map.Sites;
            _points = map.Boundaries.Vertices;

            map.SetProperty(MapLocationsProperty, _locations);

            var ocean = _locations[0];
            var possible = new Queue<int>(map.Sites
                .Vertices
                .Select((site, i) => (Vertex: i, Site: site))
                .Where(tuple => !tuple.Site.Tags.Contains(CommonTags.Edge))
                .Select(tuple => tuple.Vertex)
                .OrderBy(tuple => _random.Next()));

            var initial =
                from location in _locations.Vertices.Skip(1)
                select (possible.Dequeue(), location);

            initial = initial.Concat(
                map.Sites.Vertices
                    .Select((s, i) => (Site: s, Vertex: i))
                    .Where(t => t.Site.Tags.Contains(CommonTags.Outer))
                    .Select(t => (t.Vertex, ocean))
            );

            foreach (var (vertex, location) in initial)
                Process(vertex, location);
            
            while (_queue.Count != 0)
                Step();
        }
    }
}