using OpenCvSharp;
using OpenCvSharp.Aruco;
using TraceCad.Vision.Calibration;
using Xunit;

namespace TraceCad.Tests;

public sealed class SheetCalibratorTests
{
    [Fact]
    public void CalibrateDetectsSyntheticA4SheetMarkers()
    {
        const double pixelsPerMillimetre = 4.0;
        var template = ReferenceSheetTemplate.DefaultA4();
        using var sheet = RenderSyntheticSheet(template, pixelsPerMillimetre);

        var tempDirectory = Directory.CreateTempSubdirectory("easytrace-calibration-test-").FullName;
        try
        {
            var imagePath = Path.Combine(tempDirectory, "sheet.png");
            Cv2.ImWrite(imagePath, sheet);

            var result = new SheetCalibrator().Calibrate(imagePath, tempDirectory, template);

            Assert.True(result.Success, result.Message);
            Assert.Equal(8, result.MatchedMarkerCount);
            Assert.Equal(0.0625, result.MmPerPixel, 9);
            Assert.Equal("perspective", result.CorrectionMode);
            Assert.Null(result.Quality);
            Assert.True(File.Exists(result.CorrectedImagePath));
        }
        finally
        {
            Directory.Delete(tempDirectory, recursive: true);
        }
    }

    [Fact]
    public void CalibrateAppliesResidualWarpForSyntheticLensDistortion()
    {
        const double pixelsPerMillimetre = 8.0;
        var template = ReferenceSheetTemplate.DefaultA4();
        using var sheet = RenderSyntheticSheet(template, pixelsPerMillimetre);
        using var distorted = ApplyRadialDistortion(sheet, -0.025);

        var tempDirectory = Directory.CreateTempSubdirectory("easytrace-distortion-test-").FullName;
        try
        {
            var imagePath = Path.Combine(tempDirectory, "distorted-sheet.png");
            Cv2.ImWrite(imagePath, distorted);

            var result = new SheetCalibrator().Calibrate(
                imagePath,
                tempDirectory,
                template,
                SheetCalibrationOptions.Default with
                {
                    PixelsPerMillimetre = 8.0,
                    MinimumResidualImprovementMm = -100.0
                });

            Assert.True(result.Success, result.Message);
            Assert.True(result.MatchedMarkerCount >= 4);
            Assert.True(
                result.CorrectionMode.StartsWith("perspective + cross-validated residual warp", StringComparison.Ordinal),
                $"{result.Message} mode {result.CorrectionMode}");
            Assert.True(File.Exists(result.CorrectedImagePath));
        }
        finally
        {
            Directory.Delete(tempDirectory, recursive: true);
        }
    }

    private static Mat RenderSyntheticSheet(ReferenceSheetTemplate template, double pixelsPerMillimetre)
    {
        var sheet = new Mat(
            (int)Math.Round(template.HeightMm * pixelsPerMillimetre),
            (int)Math.Round(template.WidthMm * pixelsPerMillimetre),
            MatType.CV_8UC3,
            Scalar.White);
        var dictionary = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict5X5_1000);

        foreach (var marker in template.Markers)
        {
            using var markerImage = new Mat();
            dictionary.GenerateImageMarker(
                marker.Id,
                (int)Math.Round(marker.SizeMm * pixelsPerMillimetre),
                markerImage,
                1);
            using var markerColor = new Mat();
            Cv2.CvtColor(markerImage, markerColor, ColorConversionCodes.GRAY2BGR);

            var target = new Rect(
                (int)Math.Round(marker.TopLeftMm.X * pixelsPerMillimetre),
                (int)Math.Round(marker.TopLeftMm.Y * pixelsPerMillimetre),
                markerColor.Width,
                markerColor.Height);
            markerColor.CopyTo(new Mat(sheet, target));
        }

        DrawControlGeometry(sheet, template, pixelsPerMillimetre);
        return sheet;
    }

    private static void DrawControlGeometry(
        Mat sheet,
        ReferenceSheetTemplate template,
        double pixelsPerMillimetre)
    {
        var thickness = Math.Max(1, (int)Math.Round(0.2 * pixelsPerMillimetre));
        Cv2.Line(
            sheet,
            ToPixel(template.ReferenceLineStartMm, pixelsPerMillimetre),
            ToPixel(template.ReferenceLineEndMm, pixelsPerMillimetre),
            Scalar.Black,
            thickness);

        var center = ToPixel(template.CrosshairMm, pixelsPerMillimetre);
        var halfTick = (int)Math.Round(6.0 * pixelsPerMillimetre);
        Cv2.Line(
            sheet,
            new Point(center.X, center.Y - halfTick),
            new Point(center.X, center.Y + halfTick),
            Scalar.Black,
            thickness);
    }

    private static Point ToPixel(TraceCad.Core.Geometry.Point2 point, double pixelsPerMillimetre) =>
        new(
            (int)Math.Round(point.X * pixelsPerMillimetre),
            (int)Math.Round(point.Y * pixelsPerMillimetre));

    private static Mat ApplyRadialDistortion(Mat source, double coefficient)
    {
        using var mapX = new Mat(source.Height, source.Width, MatType.CV_32FC1);
        using var mapY = new Mat(source.Height, source.Width, MatType.CV_32FC1);
        var centerX = (source.Width - 1.0) / 2.0;
        var centerY = (source.Height - 1.0) / 2.0;
        var radius = Math.Min(centerX, centerY);

        for (var y = 0; y < source.Height; y++)
        {
            for (var x = 0; x < source.Width; x++)
            {
                var normalizedX = (x - centerX) / radius;
                var normalizedY = (y - centerY) / radius;
                var r2 = (normalizedX * normalizedX) + (normalizedY * normalizedY);
                var scale = 1.0 + (coefficient * r2);
                mapX.Set(y, x, (float)(centerX + ((x - centerX) * scale)));
                mapY.Set(y, x, (float)(centerY + ((y - centerY) * scale)));
            }
        }

        var distorted = new Mat();
        Cv2.Remap(source, distorted, mapX, mapY, InterpolationFlags.Linear, BorderTypes.Constant, Scalar.White);
        return distorted;
    }
}
