using TraceCad.Core.Geometry;
using TraceCad.Core.Model;

namespace TraceCad.Core.Validation;

public static class DrawingValidator
{
    public static IReadOnlyList<DrawingValidationIssue> Validate(
        SketchDocument document,
        DrawingValidationOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(document);

        options ??= DrawingValidationOptions.Default;
        var issues = new List<DrawingValidationIssue>();

        ValidateDocument(document, issues);
        ValidateEntities(document, options, issues);
        ValidateOpenEndpoints(document, options, issues);

        return issues;
    }

    private static void ValidateDocument(SketchDocument document, List<DrawingValidationIssue> issues)
    {
        if (!string.Equals(document.Units, "mm", StringComparison.OrdinalIgnoreCase))
        {
            issues.Add(new DrawingValidationIssue(
                DrawingIssueSeverity.Error,
                "document.units",
                $"Document units must be mm, but are '{document.Units}'."));
        }

        if (document.Width <= 0.0 || document.Height <= 0.0)
        {
            issues.Add(new DrawingValidationIssue(
                DrawingIssueSeverity.Error,
                "document.size",
                "Document width and height must be positive."));
        }
    }

    private static void ValidateEntities(
        SketchDocument document,
        DrawingValidationOptions options,
        List<DrawingValidationIssue> issues)
    {
        foreach (var entity in document.Entities)
        {
            var layer = document.FindLayer(entity.Layer);
            if (layer is null)
            {
                issues.Add(new DrawingValidationIssue(
                    DrawingIssueSeverity.Warning,
                    "entity.layer.missing",
                    $"Entity is on missing layer '{entity.Layer}'.",
                    entity.Id));
            }

            var isExportable = layer?.Exportable ?? true;
            if (!isExportable)
            {
                continue;
            }

            switch (entity)
            {
                case LineEntity line:
                    ValidateLine(line, options, issues);
                    break;
                case CircleEntity circle:
                    ValidateCircle(circle, issues);
                    break;
                case ArcEntity arc:
                    ValidateArc(arc, issues);
                    break;
                default:
                    issues.Add(new DrawingValidationIssue(
                        DrawingIssueSeverity.Error,
                        "entity.unsupported",
                        $"Unsupported export entity type '{entity.GetType().Name}'.",
                        entity.Id));
                    break;
            }
        }
    }

    private static void ValidateLine(
        LineEntity line,
        DrawingValidationOptions options,
        List<DrawingValidationIssue> issues)
    {
        if (line.Length <= GeometryConstants.Epsilon)
        {
            issues.Add(new DrawingValidationIssue(
                DrawingIssueSeverity.Error,
                "line.zero_length",
                "Line has zero length.",
                line.Id));
            return;
        }

        if (line.Length < options.TinySegmentLengthMm)
        {
            issues.Add(new DrawingValidationIssue(
                DrawingIssueSeverity.Warning,
                "line.tiny",
                $"Line is shorter than {options.TinySegmentLengthMm:0.###} mm.",
                line.Id));
        }
    }

    private static void ValidateCircle(CircleEntity circle, List<DrawingValidationIssue> issues)
    {
        if (circle.Radius <= GeometryConstants.Epsilon)
        {
            issues.Add(new DrawingValidationIssue(
                DrawingIssueSeverity.Error,
                "circle.invalid_radius",
                "Circle radius must be positive.",
                circle.Id));
        }
    }

    private static void ValidateArc(ArcEntity arc, List<DrawingValidationIssue> issues)
    {
        if (arc.Radius <= GeometryConstants.Epsilon)
        {
            issues.Add(new DrawingValidationIssue(
                DrawingIssueSeverity.Error,
                "arc.invalid_radius",
                "Arc radius must be positive.",
                arc.Id));
            return;
        }

        var sweep = arc.IsClockwise
            ? GeometryHelpers.ClockwiseSweepDeg(arc.StartAngleDeg, arc.EndAngleDeg)
            : GeometryHelpers.CounterClockwiseSweepDeg(arc.StartAngleDeg, arc.EndAngleDeg);

        if (sweep <= GeometryConstants.Epsilon)
        {
            issues.Add(new DrawingValidationIssue(
                DrawingIssueSeverity.Error,
                "arc.zero_sweep",
                "Arc sweep must be greater than zero degrees.",
                arc.Id));
        }
    }

    private static void ValidateOpenEndpoints(
        SketchDocument document,
        DrawingValidationOptions options,
        List<DrawingValidationIssue> issues)
    {
        var endpoints = document.Entities
            .Where(entity => document.FindLayer(entity.Layer)?.Exportable ?? true)
            .SelectMany(GetContourEndpoints)
            .ToList();

        for (var i = 0; i < endpoints.Count; i++)
        {
            var endpoint = endpoints[i];
            var hasMatch = false;

            for (var j = 0; j < endpoints.Count; j++)
            {
                if (i == j)
                {
                    continue;
                }

                if (endpoint.Point.DistanceTo(endpoints[j].Point) <= options.EndpointToleranceMm)
                {
                    hasMatch = true;
                    break;
                }
            }

            if (!hasMatch)
            {
                issues.Add(new DrawingValidationIssue(
                    DrawingIssueSeverity.Warning,
                    "contour.open_endpoint",
                    "Endpoint is not connected to another exportable endpoint.",
                    endpoint.EntityId));
            }
        }
    }

    private static IEnumerable<(Guid EntityId, Point2 Point)> GetContourEndpoints(Entity entity)
    {
        switch (entity)
        {
            case LineEntity line:
                yield return (line.Id, line.Start);
                yield return (line.Id, line.End);
                break;
            case ArcEntity arc:
                yield return (arc.Id, arc.StartPoint);
                yield return (arc.Id, arc.EndPoint);
                break;
        }
    }
}
