using TraceCad.Core.Model;

namespace TraceCad.Core.Geometry;

public static class ConstraintGeometry
{
    public static bool TryMakeParallel(
        LineEntity reference,
        LineEntity target,
        out LineEntity replacement)
    {
        var referenceDirection = (reference.End - reference.Start).Normalized();
        var targetVector = target.End - target.Start;
        var targetLength = targetVector.Length;
        if (referenceDirection.Length <= GeometryConstants.Epsilon ||
            targetLength <= GeometryConstants.Epsilon)
        {
            replacement = target;
            return false;
        }

        if (referenceDirection.Dot(targetVector.Normalized()) < 0.0)
        {
            referenceDirection = referenceDirection * -1.0;
        }

        var midpoint = new Point2(
            (target.Start.X + target.End.X) / 2.0,
            (target.Start.Y + target.End.Y) / 2.0);
        var half = referenceDirection * (targetLength / 2.0);
        replacement = target with
        {
            Start = midpoint - half,
            End = midpoint + half
        };
        return true;
    }

    public static bool TryMeasureParallelDistance(
        LineEntity first,
        LineEntity second,
        out double distanceMm,
        double parallelTolerance = 1e-4)
    {
        var firstDirection = (first.End - first.Start).Normalized();
        var secondDirection = (second.End - second.Start).Normalized();
        if (firstDirection.Length <= GeometryConstants.Epsilon ||
            secondDirection.Length <= GeometryConstants.Epsilon ||
            Math.Abs(firstDirection.Cross(secondDirection)) > parallelTolerance)
        {
            distanceMm = double.NaN;
            return false;
        }

        var normal = new Vector2(-firstDirection.Y, firstDirection.X);
        distanceMm = (second.Start - first.Start).Dot(normal);
        return true;
    }

    public static bool TrySetParallelDistance(
        LineEntity reference,
        LineEntity target,
        double signedDistanceMm,
        out LineEntity replacement)
    {
        if (!TryMakeParallel(reference, target, out var parallel))
        {
            replacement = target;
            return false;
        }

        var direction = (reference.End - reference.Start).Normalized();
        if (direction.Length <= GeometryConstants.Epsilon)
        {
            replacement = target;
            return false;
        }

        var normal = new Vector2(-direction.Y, direction.X);
        var length = parallel.Length;
        var currentMidpoint = new Point2(
            (parallel.Start.X + parallel.End.X) / 2.0,
            (parallel.Start.Y + parallel.End.Y) / 2.0);
        var along = (currentMidpoint - reference.Start).Dot(direction);
        var newMidpoint = reference.Start + (direction * along) + (normal * signedDistanceMm);
        var half = direction * (length / 2.0);
        replacement = parallel with
        {
            Start = newMidpoint - half,
            End = newMidpoint + half
        };
        return true;
    }

    public static bool TryMakeLineTangentToArc(
        LineEntity line,
        ArcEntity arc,
        out LineEntity replacement)
    {
        var length = line.Length;
        if (length <= GeometryConstants.Epsilon || arc.Radius <= GeometryConstants.Epsilon)
        {
            replacement = line;
            return false;
        }

        var candidates = new[]
        {
            new TangencyCandidate(line.Start, line.End, arc.StartPoint, arc.StartAngleDeg, LineStartIsAnchor: true),
            new TangencyCandidate(line.Start, line.End, arc.EndPoint, arc.EndAngleDeg, LineStartIsAnchor: true),
            new TangencyCandidate(line.End, line.Start, arc.StartPoint, arc.StartAngleDeg, LineStartIsAnchor: false),
            new TangencyCandidate(line.End, line.Start, arc.EndPoint, arc.EndAngleDeg, LineStartIsAnchor: false)
        };

        var best = candidates.OrderBy(candidate => candidate.LineAnchor.DistanceTo(candidate.ArcAnchor)).First();
        var tangent = GeometryHelpers.TangentAtAngle(best.ArcAngleDeg, arc.IsClockwise).Normalized();
        var oldDirection = (best.OtherLinePoint - best.LineAnchor).Normalized();
        if (tangent.Dot(oldDirection) < 0.0)
        {
            tangent = tangent * -1.0;
        }

        if (best.LineStartIsAnchor)
        {
            replacement = line with
            {
                Start = best.ArcAnchor,
                End = best.ArcAnchor + (tangent * length)
            };
        }
        else
        {
            replacement = line with
            {
                End = best.ArcAnchor,
                Start = best.ArcAnchor + (tangent * length)
            };
        }

        return true;
    }

    public static ArcEntity SetArcRadius(ArcEntity arc, double radius) =>
        arc with { Radius = Math.Max(GeometryConstants.Epsilon, radius) };

    private sealed record TangencyCandidate(
        Point2 LineAnchor,
        Point2 OtherLinePoint,
        Point2 ArcAnchor,
        double ArcAngleDeg,
        bool LineStartIsAnchor);
}
