using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Assets.Common;
using static System.Math;

namespace Assets.Voronoi
{

    public class HalfEdge
    {
        public Point Start = null;
        public Point End = null;

        public int StartVertex = -1;
        public int EndVertex = -1;

        private HalfEdge _twin;

        public HalfEdge Twin
        {
            get => _twin;
            set
            {
                _twin = value;
                value._twin = this;
            }
        }

        public bool IsComplete => (StartVertex != -1 && EndVertex != -1) || (_twin != null && EndVertex != -1 && _twin.EndVertex != -1);
        public (int, int) Edge => _twin != null ? (this.EndVertex, _twin.EndVertex) : (StartVertex, EndVertex);
    }
    
    public class Parabola
    {
        public Parabola(Site site)
        {
            Site = site;
        }

        public Site Site { get; internal set; }
        public EdgeEvent Event { get; internal set; }
        
        public HalfEdge LeftEdge { get; set; }
        public HalfEdge RightEdge { get; set; }

        public override string ToString()
        {
            return $"({Site.Point.x}, {Site.Point.y})";
        }
    }
    
    public class BeachLine
    {
        private IDictionary<RedBlackNode<Point>, EdgeEvent> _events = new Dictionary<RedBlackNode<Point>, EdgeEvent>();
        private RedBlackTree<Parabola> _tree = new RedBlackTree<Parabola>();
        public double Directrix { get; set; } = 0.0f;

        public IEnumerable<Parabola> Parabolas
        {
            get
            {
                if (!_tree.IsEmpty)
                {
                    var current = _tree.Root;

                    while (current.Left != null)
                        current = current.Left;

                    while (current != null)
                    {
                        yield return current.Value;
                        current = current.Next;
                    }
                }
            }
        }

        public RedBlackNode<Parabola> AddParabola(Site site, RedBlackNode<Parabola> node = null)
        {
            if (node == null)
            {
                node = new RedBlackNode<Parabola>(new Parabola(site));
                
                _tree.Root = node;
            }
            else
            {
                if (node.Value.Event != null) node.Value.Event.IsValid = false;

                _tree.InsertBefore(node, new Parabola(node.Value.Site));
                _tree.InsertAfter(node, new Parabola(node.Value.Site));

                node.Value = new Parabola(site);
            }

            return node;
        }

        public void RemoveParabola(RedBlackNode<Parabola> parabola)
        {
            _tree.Remove(parabola);
        }

        public RedBlackNode<Parabola> FindParabola(double x)
        {
            if (_tree.IsEmpty) 
                return null;
            
            var current = _tree.Root;

            while (current != null)
            {
                if (x < Left(current))
                {
                    current = current.Left;
                }
                else if (x > Right(current))
                {
                    current = current.Right;
                }
                else
                {
                    return current;
                }
            }

            throw new Exception("WTF?");
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public double Left(RedBlackNode<Parabola> node)
        {
            return node.Previous == null 
                ? double.NegativeInfinity 
                : IntersectParabola(node.Previous.Value.Site.Point, node.Value.Site.Point);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public double Right(RedBlackNode<Parabola> node)
        {
            return node.Next == null 
                ? double.PositiveInfinity 
                : IntersectParabola(node.Value.Site.Point, node.Next.Value.Site.Point);
        }

        public double EvalParabola(Point f, double x)
        {
            var nominator = Pow(x - f.x, 2) + f.y * f.y - Directrix * Directrix;
            return nominator / (2 * (f.y - Directrix));
        }

        public double Eval(double x)
        {
            var parabola = FindParabola(x);

            if (parabola is null) return Directrix;

            return EvalParabola(parabola.Value.Site.Point, x);
        }

        private Point Circle(RedBlackNode<Parabola> node)
        {
            if (node.Previous == null || node.Next == null)
                return null;

            var a = node.Previous.Value.Site.Point;
            var b = node.Value.Site.Point;
            var c = node.Next.Value.Site.Point;

            if ((a.x - b.x) * (c.y - b.y) - (c.x - b.x) * (a.y - b.y) > 0.000001)
                return null;
            
            var A = a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y;
            var B = (a.x * a.x + a.y * a.y) * (c.y - b.y)
                    + (b.x * b.x + b.y * b.y) * (a.y - c.y)
                    + (c.x * c.x + c.y * c.y) * (b.y - a.y);
            var C = (a.x * a.x + a.y * a.y) * (b.x - c.x)
                    + (b.x * b.x + b.y * b.y) * (c.x - a.x)
                    + (c.x * c.x + c.y * c.y) * (a.x - b.x);

            if (Math.Abs(A) < 0.1)
                return null;

            var x = -B / (2 * A);
            var y = -C / (2 * A);

            return new Point(x, y);
        }

        public EdgeEvent CheckCircleEvent(RedBlackNode<Parabola> node)
        {
            if (node == null)
                return null;
            
            var circle = Circle(node);

            if (circle == null) 
                return null;
            
            return node.Value.Event = new EdgeEvent() { vertex = circle, node = node };
        }

        private double IntersectParabola(Point q, Point p)
        {
            if (Abs(p.y - Directrix) < 0.0001) return p.x;
            if (Abs(q.y - Directrix) < 0.0001) return q.x;

            if (Abs(p.y - q.y) < 0.0001) return (q.x + p.x) / 2;
            
            var alpha = 1 / (2 * (q.y - Directrix));
            var beta = 1 / (2 * (p.y - Directrix));

            double a = alpha - beta;
            double b = -2 * alpha * q.x + 2 * beta * p.x;
            double c = alpha * (Pow(q.x, 2) + Pow(q.y, 2) - Pow(Directrix, 2))
                    - beta * (Pow(p.x, 2) + Pow(p.y, 2) - Pow(Directrix, 2));

            double d = Sqrt(b * b - 4.0 * a * c);
            
            return (-b - d) / (2 * a);
        }
    }
}