using System.Collections.Generic;
using System.Linq;
using Assets.Common;
using Assets.Map;
using Priority_Queue;

namespace Assets.Voronoi
{
    public interface IEvent 
    {
        double Priority { get; }
        bool IsValid { get; }
    }

    public class SiteEvent : IEvent 
    {
        public Site Site { get; internal set; }
        
        public double Priority => Site.Point.y;
        public bool IsValid => true;
    }

    public class EdgeEvent : IEvent
    {
        public RedBlackNode<Parabola> node;
        public Point vertex;
        public bool IsValid { get; set; } = true;

        public double Priority => vertex.y + Point.Dist(vertex, node.Value.Site.Point);
    }

    public class Site
    {
        public Point Point { get; internal set; }
        public int Index { get; internal set; }
        public List<int> Vertices { get; internal set; } = new List<int>();
        public List<(int, int)> Edges { get; internal set; } = new List<(int, int)>();
        
        public Site(Point point, int index)
        {
            Point = point;
            Index = index;
        }
    }
    
    public class VoronoiGenerator
    {
        public IList<Site> Sites { get; }

        public IPriorityQueue<IEvent, double> Queue { get; internal set; }

        public Graph<Point> Voronoi;
        public Graph<Site> Delaunay;

        public List<HalfEdge> HalfEdges;
        
        public BeachLine Line { get; private set; }
        
        public bool Done => Queue.Count == 0;
        
        public VoronoiGenerator(IList<Point> sites)
        {
            int i = 0;
            Sites = sites.Select(x => new Site(x, i++)).ToList();
            
            Reset();
        }
        
        private void Reset()
        {
            Queue = new SimplePriorityQueue<IEvent, double>();
            Line = new BeachLine();
            
            Voronoi   = new Graph<Point>();
            Delaunay  = new Graph<Site>();
            HalfEdges = new List<HalfEdge>();
            
            Delaunay.Vertices.AddRange(Sites);

            foreach (var site in Sites)
            {
                var @event = new SiteEvent { Site = site };
                Queue.Enqueue(@event, @event.Priority);
            }
        }

        public void Step()
        {
            while (Queue.Count > 0)
            {
                var ev = Queue.Dequeue();
                if (!ev.IsValid) continue;

                Line.Directrix = ev.Priority;
                
                switch (ev)
                {
                    case SiteEvent site: 
                        HandleSiteEvent(site);
                        return;
                    
                    case EdgeEvent edge: 
                        HandleEdgeEvent(edge);
                        return;
                }
            }
        }

        public void Generate()
        {
            Reset();
            
            while (Queue.Count > 0)
                Step();
        }

        private void Enqueue(EdgeEvent @event)
        {
            if (@event.Priority > Line.Directrix)
                Queue.Enqueue(@event, @event.Priority);
        }

        private void HandleEdgeEvent(EdgeEvent @event)
        {
            var node = @event.node;
            
            var previous = node.Previous;
            var next     = node.Next;
            
            if (previous?.Value.Event != null) previous.Value.Event.IsValid = false;
            if (next?.Value.Event != null) next.Value.Event.IsValid = false;

            Voronoi.Vertices.Add(@event.vertex);

            var index = Voronoi.Vertices.Count - 1;

            node.Value.LeftEdge.End = @event.vertex;
            node.Value.LeftEdge.EndVertex = index;
            
            node.Value.RightEdge.End = @event.vertex;
            node.Value.RightEdge.EndVertex = index;
            
            HalfEdges.Add(node.Value.LeftEdge);
            HalfEdges.Add(node.Value.RightEdge);

            node.Value.Site.Vertices.Add(index);
            previous?.Value.Site.Vertices.Add(index);
            next?.Value.Site.Vertices.Insert(0, index);

            if (node.Value.LeftEdge.IsComplete)
            {
                var edge = node.Value.LeftEdge.Edge;

                node.Value.Site.Edges.Insert(0, FixEdgeDirection(node.Value, edge));
                previous?.Value.Site.Edges.Add(FixEdgeDirection(previous.Value, edge));
                
                Voronoi.AddEdge(edge.Item1, edge.Item2);
            }

            if (node.Value.RightEdge.IsComplete)
            {
                var edge = node.Value.RightEdge.Edge;

                node.Value.Site.Edges.Add(FixEdgeDirection(node.Value, edge));
                next?.Value.Site.Edges.Insert(0, FixEdgeDirection(next.Value, edge));
                
                Voronoi.AddEdge(edge.Item1, edge.Item2);
            }
            
            var newEdge = new HalfEdge() { Start = @event.vertex, StartVertex = index };
            
            node.Previous.Value.RightEdge = newEdge;
            node.Next.Value.LeftEdge = newEdge;

            Line.RemoveParabola(node);
            
            if (Line.CheckCircleEvent(previous) is EdgeEvent p) 
                Enqueue(p);
            
            if (Line.CheckCircleEvent(next) is EdgeEvent n) 
                Enqueue(n);

            if (previous != null && next != null)
                Delaunay.AddEdge(next.Value.Site.Index, previous.Value.Site.Index);
        }

        private (int, int) FixEdgeDirection(Parabola parabola, (int, int) edge)
        {
            var center = parabola.Site.Point;
            
            var a = Voronoi.Vertices[edge.Item1];
            var b = Voronoi.Vertices[edge.Item2];

            var xa = a.x - center.x;
            var xb = b.x - center.x;
            var ya = a.y - center.y;
            var yb = b.y - center.y;

            return xa * yb - xb * ya < 0 ? (edge.Item2, edge.Item1) : edge;
        }

        private void HandleSiteEvent(SiteEvent @event)
        {
            var site = @event.Site;
            var start = new Point(site.Point.x, Line.Eval(site.Point.x));

            var above = Line.FindParabola(site.Point.x);

            if (above != null)
            {
                var left  = above.Value.LeftEdge;
                var right = above.Value.RightEdge;
                
                var node = Line.AddParabola(@event.Site, above);
                
                var newLeft  = new HalfEdge() { Start = start };
                var newRight = new HalfEdge() { Start = start, Twin = newLeft }; 

                node.Previous.Value.LeftEdge = left;
                node.Previous.Value.RightEdge = newLeft;

                node.Value.LeftEdge = newLeft;
                node.Value.RightEdge = newRight;

                node.Next.Value.LeftEdge = newRight;
                node.Next.Value.RightEdge = right;

                if (Line.CheckCircleEvent(node.Previous) is EdgeEvent p) 
                    Enqueue(p);
                
                if (Line.CheckCircleEvent(node.Next) is EdgeEvent n) 
                    Enqueue(n);

                if (node.Previous != null)
                    Delaunay.AddEdge(@event.Site.Index, node.Previous.Value.Site.Index);
            }
            else
            {
                Line.AddParabola(@event.Site);
            }
        }
    }
}