namespace TraceCad.Core.Geometry;

public static class GeometryHelpers
{
    public static double NormalizeAngleDeg(double angleDeg)
    {
        var normalized = angleDeg % 360.0;
        return normalized < 0.0 ? normalized + 360.0 : normalized;
    }

    public static double AngleDeg(Point2 center, Point2 point)
    {
        return NormalizeAngleDeg(Math.Atan2(point.Y - center.Y, point.X - center.X) * 180.0 / Math.PI);
    }

    public static bool TryCreateCircleFromThreePoints(
        Point2 first,
        Point2 second,
        Point2 third,
        out CircleDefinition circle,
        double epsilon = 1e-8)
    {
        var d = 2.0 * (
            first.X * (second.Y - third.Y) +
            second.X * (third.Y - first.Y) +
            third.X * (first.Y - second.Y));

        if (Math.Abs(d) < epsilon)
        {
            circle = default;
            return false;
        }

        var firstSq = (first.X * first.X) + (first.Y * first.Y);
        var secondSq = (second.X * second.X) + (second.Y * second.Y);
        var thirdSq = (third.X * third.X) + (third.Y * third.Y);

        var center = new Point2(
            ((firstSq * (second.Y - third.Y)) +
             (secondSq * (third.Y - first.Y)) +
             (thirdSq * (first.Y - second.Y))) / d,
            ((firstSq * (third.X - second.X)) +
             (secondSq * (first.X - third.X)) +
             (thirdSq * (second.X - first.X))) / d);

        var radius = center.DistanceTo(first);
        if (radius < epsilon)
        {
            circle = default;
            return false;
        }

        circle = new CircleDefinition(center, radius);
        return true;
    }

    public static bool TryCreateArcFromThreePoints(
        Point2 start,
        Point2 pointOnArc,
        Point2 end,
        out ArcDefinition arc,
        double epsilon = 1e-8)
    {
        if (!TryCreateCircleFromThreePoints(start, pointOnArc, end, out var circle, epsilon))
        {
            arc = default;
            return false;
        }

        var startAngle = AngleDeg(circle.Center, start);
        var midAngle = AngleDeg(circle.Center, pointOnArc);
        var endAngle = AngleDeg(circle.Center, end);
        var isClockwise = !AngleIsOnSweep(startAngle, endAngle, midAngle, isClockwise: false);

        arc = new ArcDefinition(circle.Center, circle.Radius, startAngle, endAngle, isClockwise);
        return true;
    }

    public static bool TryCreateArcFromStartTangentAndEndPoint(
        Point2 start,
        Vector2 startTangent,
        Point2 end,
        Point2? sidePoint,
        out ArcDefinition arc,
        double epsilon = 1e-8)
    {
        var tangent = startTangent.Normalized();
        var chord = end - start;
        if (tangent.Length <= epsilon || chord.Length <= epsilon)
        {
            arc = default;
            return false;
        }

        var normals = new[]
        {
            new Vector2(-tangent.Y, tangent.X),
            new Vector2(tangent.Y, -tangent.X)
        };
        CandidateArc? best = null;

        foreach (var normal in normals)
        {
            var denominator = 2.0 * chord.Dot(normal);
            if (Math.Abs(denominator) <= epsilon)
            {
                continue;
            }

            var signedRadius = chord.Dot(chord) / denominator;
            var center = start + (normal * signedRadius);
            var radius = Math.Abs(signedRadius);
            if (radius <= epsilon)
            {
                continue;
            }

            var startAngle = AngleDeg(center, start);
            var endAngle = AngleDeg(center, end);
            var clockwiseTangent = TangentAtAngle(startAngle, isClockwise: true);
            var counterClockwiseTangent = TangentAtAngle(startAngle, isClockwise: false);
            var isClockwise = clockwiseTangent.Dot(tangent) > counterClockwiseTangent.Dot(tangent);
            var candidate = new ArcDefinition(center, radius, startAngle, endAngle, isClockwise);
            var score = ScoreArcCandidate(candidate, sidePoint);

            if (best is null || score < best.Score)
            {
                best = new CandidateArc(candidate, score);
            }
        }

        if (best is null)
        {
            arc = default;
            return false;
        }

        arc = best.Arc;
        return true;
    }

    public static bool AngleIsOnSweep(double startDeg, double endDeg, double candidateDeg, bool isClockwise)
    {
        startDeg = NormalizeAngleDeg(startDeg);
        endDeg = NormalizeAngleDeg(endDeg);
        candidateDeg = NormalizeAngleDeg(candidateDeg);

        if (!isClockwise)
        {
            var sweep = CounterClockwiseSweepDeg(startDeg, endDeg);
            var candidateSweep = CounterClockwiseSweepDeg(startDeg, candidateDeg);
            return candidateSweep <= sweep + GeometryConstants.Epsilon;
        }

        var clockwiseSweep = ClockwiseSweepDeg(startDeg, endDeg);
        var candidateClockwiseSweep = ClockwiseSweepDeg(startDeg, candidateDeg);
        return candidateClockwiseSweep <= clockwiseSweep + GeometryConstants.Epsilon;
    }

    public static double CounterClockwiseSweepDeg(double startDeg, double endDeg)
    {
        return NormalizeAngleDeg(endDeg - startDeg);
    }

    public static double ClockwiseSweepDeg(double startDeg, double endDeg)
    {
        return NormalizeAngleDeg(startDeg - endDeg);
    }

    public static Vector2 TangentAtAngle(double angleDeg, bool isClockwise)
    {
        var radians = angleDeg * Math.PI / 180.0;
        var counterClockwise = new Vector2(-Math.Sin(radians), Math.Cos(radians));
        return isClockwise
            ? new Vector2(-counterClockwise.X, -counterClockwise.Y)
            : counterClockwise;
    }

    private static double ScoreArcCandidate(ArcDefinition arc, Point2? sidePoint)
    {
        var sweep = arc.IsClockwise
            ? ClockwiseSweepDeg(arc.StartAngleDeg, arc.EndAngleDeg)
            : CounterClockwiseSweepDeg(arc.StartAngleDeg, arc.EndAngleDeg);

        if (sidePoint is null)
        {
            return sweep;
        }

        var sideAngle = AngleDeg(arc.Center, sidePoint.Value);
        var sideDistance = Math.Abs(arc.Center.DistanceTo(sidePoint.Value) - arc.Radius);
        var isOnSweep = AngleIsOnSweep(arc.StartAngleDeg, arc.EndAngleDeg, sideAngle, arc.IsClockwise);
        return (isOnSweep ? 0.0 : 1000000.0) + sideDistance;
    }

    private sealed record CandidateArc(ArcDefinition Arc, double Score);
}
