using System;
using System.Globalization;
using CrossCutting;

namespace InterpreterLibrary.Models
{
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    public sealed class LineSegment
    {
        

        private readonly Line _line;

        
        
        
        
        
        
        public LineSegment(PointF start, PointF end)
        {
            _line = Line.FromPoints(start, end);
            Start = start;
            End = end;
        }
        public LineSegment(float x1, float y1, float x2, float y2)
        {
            Start = new PointF(x1, y1);
            End = new PointF(x2, y2);
            _line = Line.FromPoints(Start, End);
        }

        
        
        
        public PointF Start { get; }

        
        
        
        public PointF End { get; }

        
        
        
        public float Length => Start.DistanceTo(End);

        
        
        
        
        
        
        public static explicit operator Line(LineSegment segment)
        {
            return segment._line;
        }

        
        
        
        
        
        
        
        
        
        public float DistanceToPoint(PointF point)
        {
            float segmentDistance;

            switch (LocateProjection(point))
            {
                case ProjectionLocation.RayA:
                    segmentDistance = point.DistanceTo(Start);
                    break;
                case ProjectionLocation.RayB:
                    segmentDistance = point.DistanceTo(End);
                    break;
                default:
                    segmentDistance = _line.DistanceToPoint(point);
                    break;
            }

            return segmentDistance;
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public PointF? GetIntersectionWith(LineSegment other)
        {
            PointF? result = null;

            if (_line.Slope == other._line.Slope || _line.IsVertical && other._line.IsVertical)
            {
                if (_line.Intercept == other._line.Intercept)
                {
                    
                    
                    
                    
                    
                    
                    

                    ProjectionLocation projC = LocateProjection(other.Start), projD = LocateProjection(other.End);

                    if (projC != ProjectionLocation.SegmentAB && projC == projD)
                        result = null;
                    else if (Start == other.Start && projD == ProjectionLocation.RayA ||
                             Start == other.End && projC == ProjectionLocation.RayA)
                        result = Start;
                    else if (End == other.Start && projD == ProjectionLocation.RayB ||
                             End == other.End && projC == ProjectionLocation.RayB)
                        result = End;
                    else
                        throw new InvalidOperationException(
                            "Overlapping segments do not have a single intersection point.");
                }
            }
            else
            {
                result = GetIntersectionWith(other._line);

                if (result.HasValue && other.LocateProjection(result.Value) != ProjectionLocation.SegmentAB)
                    result = null;
            }

            return result;
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        public PointF? GetIntersectionWith(Line other)
        {
            PointF? result;

            if (_line.Slope == other.Slope || _line.IsVertical && other.IsVertical)
            {
                if (_line.Intercept == other.Intercept)
                    throw new InvalidOperationException("Segment is a portion of the specified line.");

                
                result = null;
            }
            else
            {
                result = _line.GetIntersectionWith(other);
            }

            if (result.HasValue && LocateProjection(result.Value) != ProjectionLocation.SegmentAB)
                result = null;

            return result;
        }

        
        private ProjectionLocation LocateProjection(PointF point)
        {
            

            

            
            var abDelta = new PointF(End.X - Start.X, End.Y - Start.Y); 
            var acDelta = new PointF(point.X - Start.X, point.Y - Start.Y); 

            var numerator = acDelta.X * abDelta.X + acDelta.Y * abDelta.Y;
            var denomenator = abDelta.X * abDelta.X + abDelta.Y * abDelta.Y;

            var result = numerator < 0
                ? ProjectionLocation.RayA
                : numerator > denomenator
                    ? ProjectionLocation.RayB
                    : ProjectionLocation.SegmentAB;

            return result;
        }

        
        
        
        
        
        
        
        
        
        public static bool operator ==(LineSegment line1, LineSegment line2)
        {
            if (ReferenceEquals(line1, line2))
                return true;

            if ((object)line1 == null || (object)line2 == null)
                return false;

            return line1.Start == line2.Start && line1.End == line2.End;
        }

        
        
        
        
        
        
        
        
        
        public static bool operator !=(LineSegment line1, LineSegment line2)
        {
            return line1 != line2;
        }

        
        
        
        
        
        public override bool Equals(object obj)
        {
            return obj is LineSegment segment && this == segment;
        }

        
        
        
        
        public override int GetHashCode()
        {
            return Start.GetHashCode() + End.GetHashCode();
        }

        
        
        
        
        public override string ToString()
        {
            return string.Format(CultureInfo.InvariantCulture, "({0}) -> ({1})", Start, End);
        }

        
        
        
        private enum ProjectionLocation
        {
            RayA,
            SegmentAB,
            RayB
        }
    }
}
