using TraceCad.Core.Model;

namespace TraceCad.Core.Geometry;

public static class EntityEditing
{
    public static bool TryTrimOrExtendEndpoint(
        Entity subject,
        Point2 subjectPick,
        Entity boundary,
        Point2 boundaryPick,
        out Entity replacement) =>
        TryAdjustEndpoint(subject, subjectPick, boundary, boundaryPick, EndpointEditMode.TrimOrExtend, out replacement);

    public static bool TryTrimEndpoint(
        Entity subject,
        Point2 subjectPick,
        Entity boundary,
        Point2 boundaryPick,
        out Entity replacement) =>
        TryAdjustEndpoint(subject, subjectPick, boundary, boundaryPick, EndpointEditMode.Trim, out replacement);

    public static bool TryExtendEndpoint(
        Entity subject,
        Point2 subjectPick,
        Entity boundary,
        Point2 boundaryPick,
        out Entity replacement) =>
        TryAdjustEndpoint(subject, subjectPick, boundary, boundaryPick, EndpointEditMode.Extend, out replacement);

    private static bool TryAdjustEndpoint(
        Entity subject,
        Point2 subjectPick,
        Entity boundary,
        Point2 boundaryPick,
        EndpointEditMode mode,
        out Entity replacement)
    {
        replacement = subject;

        if (subject.Id == boundary.Id || subject is CircleEntity)
        {
            return false;
        }

        var intersections = Intersections(subject, boundary)
            .Where(point => IsUsableBoundaryIntersection(boundary, point))
            .OrderBy(point => point.DistanceTo(boundaryPick))
            .ThenBy(point => point.DistanceTo(subjectPick))
            .ToList();
        if (intersections.Count == 0)
        {
            return false;
        }

        return subject switch
        {
            LineEntity line => TryReplaceLineEndpoint(line, subjectPick, intersections, mode, out replacement),
            ArcEntity arc => TryReplaceArcEndpoint(arc, subjectPick, intersections, mode, out replacement),
            _ => false
        };
    }

    private static bool TryReplaceLineEndpoint(
        LineEntity line,
        Point2 subjectPick,
        IReadOnlyList<Point2> intersections,
        EndpointEditMode mode,
        out Entity replacement)
    {
        var replaceStart = subjectPick.DistanceTo(line.Start) <= subjectPick.DistanceTo(line.End);
        foreach (var intersection in intersections)
        {
            if (!LineEndpointEditMatchesMode(line, replaceStart, intersection, mode))
            {
                continue;
            }

            var updated = replaceStart
                ? line with { Start = intersection }
                : line with { End = intersection };

            replacement = updated;
            return updated.Length > GeometryConstants.Epsilon;
        }

        replacement = line;
        return false;
    }

    private static bool TryReplaceArcEndpoint(
        ArcEntity arc,
        Point2 subjectPick,
        IReadOnlyList<Point2> intersections,
        EndpointEditMode mode,
        out Entity replacement)
    {
        var replaceStart = subjectPick.DistanceTo(arc.StartPoint) <= subjectPick.DistanceTo(arc.EndPoint);
        foreach (var intersection in intersections)
        {
            var angle = GeometryHelpers.AngleDeg(arc.Center, intersection);
            var updated = replaceStart
                ? arc with { StartAngleDeg = angle }
                : arc with { EndAngleDeg = angle };

            if (!ArcEndpointEditMatchesMode(arc, updated, mode))
            {
                continue;
            }

            replacement = updated;
            return ArcSweep(updated) > GeometryConstants.Epsilon;
        }

        replacement = arc;
        return false;
    }

    private static IEnumerable<Point2> Intersections(Entity subject, Entity boundary)
    {
        return (subject, boundary) switch
        {
            (LineEntity a, LineEntity b) => LineLineIntersections(a, b, subjectInfinite: true, boundaryInfinite: true),
            (LineEntity line, CircleEntity circle) => LineCircleIntersections(line, circle, subjectInfinite: true),
            (LineEntity line, ArcEntity arc) => LineCircleIntersections(line, ToCircle(arc), subjectInfinite: true),
            (ArcEntity arc, LineEntity line) => LineCircleIntersections(line, ToCircle(arc), subjectInfinite: true),
            (ArcEntity a, CircleEntity b) => CircleCircleIntersections(ToCircle(a), b),
            (ArcEntity a, ArcEntity b) => CircleCircleIntersections(ToCircle(a), ToCircle(b)),
            _ => Enumerable.Empty<Point2>()
        };
    }

    private static IEnumerable<Point2> LineLineIntersections(
        LineEntity subject,
        LineEntity boundary,
        bool subjectInfinite,
        bool boundaryInfinite)
    {
        var p = subject.Start;
        var r = subject.End - subject.Start;
        var q = boundary.Start;
        var s = boundary.End - boundary.Start;
        var denominator = r.Cross(s);
        if (Math.Abs(denominator) <= GeometryConstants.Epsilon)
        {
            yield break;
        }

        var qmp = q - p;
        var t = qmp.Cross(s) / denominator;
        var u = qmp.Cross(r) / denominator;
        if ((!subjectInfinite && !IsUnitInterval(t)) || (!boundaryInfinite && !IsUnitInterval(u)))
        {
            yield break;
        }

        yield return p + (r * t);
    }

    private static IEnumerable<Point2> LineCircleIntersections(
        LineEntity line,
        CircleEntity circle,
        bool subjectInfinite)
    {
        var direction = line.End - line.Start;
        var lengthSquared = direction.Dot(direction);
        if (lengthSquared <= GeometryConstants.Epsilon)
        {
            yield break;
        }

        var fromCenter = line.Start - circle.Center;
        var a = lengthSquared;
        var b = 2.0 * fromCenter.Dot(direction);
        var c = fromCenter.Dot(fromCenter) - (circle.Radius * circle.Radius);
        var discriminant = (b * b) - (4.0 * a * c);
        if (discriminant < -GeometryConstants.Epsilon)
        {
            yield break;
        }

        if (Math.Abs(discriminant) <= GeometryConstants.Epsilon)
        {
            var t = -b / (2.0 * a);
            if (subjectInfinite || IsUnitInterval(t))
            {
                yield return line.Start + (direction * t);
            }

            yield break;
        }

        var sqrt = Math.Sqrt(discriminant);
        foreach (var t in new[] { (-b - sqrt) / (2.0 * a), (-b + sqrt) / (2.0 * a) })
        {
            if (subjectInfinite || IsUnitInterval(t))
            {
                yield return line.Start + (direction * t);
            }
        }
    }

    private static IEnumerable<Point2> CircleCircleIntersections(CircleEntity first, CircleEntity second)
    {
        var distance = first.Center.DistanceTo(second.Center);
        if (distance <= GeometryConstants.Epsilon ||
            distance > first.Radius + second.Radius + GeometryConstants.Epsilon ||
            distance < Math.Abs(first.Radius - second.Radius) - GeometryConstants.Epsilon)
        {
            yield break;
        }

        var a = ((first.Radius * first.Radius) - (second.Radius * second.Radius) + (distance * distance)) / (2.0 * distance);
        var hSquared = (first.Radius * first.Radius) - (a * a);
        if (hSquared < -GeometryConstants.Epsilon)
        {
            yield break;
        }

        var direction = (second.Center - first.Center).Normalized();
        var basePoint = first.Center + (direction * a);
        if (Math.Abs(hSquared) <= GeometryConstants.Epsilon)
        {
            yield return basePoint;
            yield break;
        }

        var h = Math.Sqrt(hSquared);
        var perpendicular = new Vector2(-direction.Y, direction.X);
        yield return basePoint + (perpendicular * h);
        yield return basePoint - (perpendicular * h);
    }

    private static bool IsUsableBoundaryIntersection(Entity boundary, Point2 point)
    {
        return boundary switch
        {
            LineEntity => true,
            CircleEntity circle => Math.Abs(point.DistanceTo(circle.Center) - circle.Radius) <= 0.01,
            ArcEntity arc => Math.Abs(point.DistanceTo(arc.Center) - arc.Radius) <= 0.01 &&
                             GeometryHelpers.AngleIsOnSweep(
                                 arc.StartAngleDeg,
                                 arc.EndAngleDeg,
                                 GeometryHelpers.AngleDeg(arc.Center, point),
                                 arc.IsClockwise),
            _ => false
        };
    }

    private static CircleEntity ToCircle(ArcEntity arc) =>
        new(arc.Id, arc.Layer, arc.Center, arc.Radius);

    private static bool LineEndpointEditMatchesMode(
        LineEntity line,
        bool replaceStart,
        Point2 intersection,
        EndpointEditMode mode)
    {
        var direction = line.End - line.Start;
        var lengthSquared = direction.Dot(direction);
        if (lengthSquared <= GeometryConstants.Epsilon)
        {
            return false;
        }

        var t = (intersection - line.Start).Dot(direction) / lengthSquared;
        var trims = t > GeometryConstants.Epsilon && t < 1.0 - GeometryConstants.Epsilon;
        var extends = replaceStart
            ? t < -GeometryConstants.Epsilon
            : t > 1.0 + GeometryConstants.Epsilon;

        return mode switch
        {
            EndpointEditMode.Trim => trims,
            EndpointEditMode.Extend => extends,
            _ => trims || extends
        };
    }

    private static bool ArcEndpointEditMatchesMode(
        ArcEntity original,
        ArcEntity updated,
        EndpointEditMode mode)
    {
        var originalSweep = ArcSweep(original);
        var updatedSweep = ArcSweep(updated);
        var trims = updatedSweep > GeometryConstants.Epsilon &&
                    updatedSweep < originalSweep - GeometryConstants.Epsilon;
        var extends = updatedSweep > originalSweep + GeometryConstants.Epsilon;

        return mode switch
        {
            EndpointEditMode.Trim => trims,
            EndpointEditMode.Extend => extends,
            _ => trims || extends
        };
    }

    private static double ArcSweep(ArcEntity arc) =>
        arc.IsClockwise
            ? GeometryHelpers.ClockwiseSweepDeg(arc.StartAngleDeg, arc.EndAngleDeg)
            : GeometryHelpers.CounterClockwiseSweepDeg(arc.StartAngleDeg, arc.EndAngleDeg);

    private static bool IsUnitInterval(double value) =>
        value >= -GeometryConstants.Epsilon && value <= 1.0 + GeometryConstants.Epsilon;

    private static double DistanceToSegment(Point2 point, Point2 start, Point2 end)
    {
        var segment = end - start;
        var lengthSquared = segment.Dot(segment);
        if (lengthSquared <= GeometryConstants.Epsilon)
        {
            return point.DistanceTo(start);
        }

        var t = Math.Clamp((point - start).Dot(segment) / lengthSquared, 0.0, 1.0);
        return point.DistanceTo(start + (segment * t));
    }
}
