using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;

namespace OpenNest.Tests.CutOffs;

public class CutOffGeometryTests
{
    private static readonly CutOffSettings ZeroClearance = new() { PartClearance = 0.0 };

    private static double TotalCutLength(Program program, CutOffAxis axis = CutOffAxis.Vertical)
    {
        var total = 0.0;
        for (var i = 0; i < program.Codes.Count - 1; i += 2)
        {
            if (program.Codes[i] is RapidMove rapid &&
                program.Codes[i + 1] is LinearMove linear)
            {
                total += axis == CutOffAxis.Vertical
                    ? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y)
                    : System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X);
            }
        }
        return total;
    }

    private static Program MakeSquare(double size)
    {
        var pgm = new Program();
        pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(size, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(size, size)));
        pgm.Codes.Add(new LinearMove(new Vector(0, size)));
        pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
        return pgm;
    }

    private static Program MakeCircle(double radius)
    {
        // Rapid to (radius, 0) relative to center at (0, 0),
        // then full-circle arc back to same point.
        var pgm = new Program();
        pgm.Codes.Add(new RapidMove(new Vector(radius, 0)));
        pgm.Codes.Add(new ArcMove(new Vector(radius, 0), new Vector(0, 0)));
        return pgm;
    }

    private static Program MakeDiamond(double halfSize)
    {
        // Diamond: points at (half,0), (2*half,half), (half,2*half), (0,half)
        var pgm = new Program();
        pgm.Codes.Add(new RapidMove(new Vector(halfSize, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(halfSize * 2, halfSize)));
        pgm.Codes.Add(new LinearMove(new Vector(halfSize, halfSize * 2)));
        pgm.Codes.Add(new LinearMove(new Vector(0, halfSize)));
        pgm.Codes.Add(new LinearMove(new Vector(halfSize, 0)));
        return pgm;
    }

    private static Program MakeTriangle(double width, double height)
    {
        // Right triangle: (0,0) -> (width,0) -> (0,height) -> (0,0)
        var pgm = new Program();
        pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(width, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(0, height)));
        pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
        return pgm;
    }

    [Fact]
    public void Square_GeometryExclusionMatchesBoundingBox()
    {
        // For a square, geometry and BB should produce the same exclusion.
        var drawing = new Drawing("sq", MakeSquare(20));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(10, 10);
        plate.Parts.Add(part);

        // Vertical cut at X=20 (through the middle of the square).
        // BB exclusion Y = [10, 30]. Geometry should give the same.
        var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, ZeroClearance);

        var codes = cutoff.Drawing.Program.Codes;
        // Two segments: before and after the square → 4 codes
        Assert.Equal(4, codes.Count);
    }

    [Fact]
    public void Circle_GeometryExclusionNarrowerThanBoundingBox()
    {
        // Circle radius=10, center at (10,10) after placement.
        // BB = (0,0,20,20). Vertical cut at X=2 clips the circle edge.
        // BB would exclude full Y=[0,20].
        // Geometry: at X=2, the chord is much narrower.
        var drawing = new Drawing("circ", MakeCircle(10));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(0, 0);
        plate.Parts.Add(part);

        var cache = Plate.BuildPerimeterCache(plate);

        // Cut at X=2: inside the BB but near the edge of the circle.
        var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, ZeroClearance, cache);

        // The circle chord at X=2 from center (10,0) is much shorter than 20.
        // With geometry, we get a tighter exclusion, so the segments should
        // cover more of the plate than with BB.
        // Total cut length should be greater than 80 (BB would give 100-20=80)
        var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
        Assert.True(totalCutLength > 80, $"Geometry should give more cut length than BB. Got {totalCutLength:F2}");
    }

    [Fact]
    public void Diamond_GeometryExclusionNarrowerThanBoundingBox()
    {
        // Diamond half=10 → points at (10,0), (20,10), (10,20), (0,10).
        // BB = (0,0,20,20).
        // Vertical cut at X=5: BB excludes Y=[0,20].
        // Diamond edge at X=5: intersects at Y=5 and Y=15 → exclusion [5,15].
        var drawing = new Drawing("dia", MakeDiamond(10));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(0, 0);
        plate.Parts.Add(part);

        var cache = Plate.BuildPerimeterCache(plate);
        var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, ZeroClearance, cache);

        // BB would exclude full 20 → cut length = 80.
        // Geometry excludes only 10 → cut length = 90.
        var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
        Assert.True(totalCutLength > 85, $"Diamond geometry should give more cut than BB. Got {totalCutLength:F2}");
    }

    [Fact]
    public void Triangle_AsymmetricExclusion()
    {
        // Right triangle: (0,0)→(30,0)→(0,30)→(0,0) placed at (10,10).
        // Vertical cut at X=20 (10 into the triangle from left).
        // The hypotenuse from (40,10) to (10,40): at X=20, Y = 30.
        // So geometry exclusion should be roughly [10, 30], not [10, 40] like BB.
        var drawing = new Drawing("tri", MakeTriangle(30, 30));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(10, 10);
        plate.Parts.Add(part);

        var cache = Plate.BuildPerimeterCache(plate);
        var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, ZeroClearance, cache);

        // BB would exclude [10,40] = 30 → cut = 70.
        // Geometry excludes [10,30] = 20 → cut = 80.
        var totalCutLength = TotalCutLength(cutoff.Drawing.Program);
        Assert.True(totalCutLength > 75, $"Triangle geometry should give more cut than BB. Got {totalCutLength:F2}");
    }

    [Fact]
    public void CutLineMissesPart_NoExclusion()
    {
        var drawing = new Drawing("sq", MakeSquare(10));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(50, 50);
        plate.Parts.Add(part);

        // Vertical cut at X=5: well outside the part at X=[50,60].
        var cutoff = new CutOff(new Vector(5, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, ZeroClearance);

        // Single full-length segment → 2 codes
        Assert.Equal(2, cutoff.Drawing.Program.Codes.Count);
    }

    [Fact]
    public void HorizontalCut_Circle_UsesGeometry()
    {
        var drawing = new Drawing("circ", MakeCircle(10));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(0, 0);
        plate.Parts.Add(part);

        var cache = Plate.BuildPerimeterCache(plate);

        // Horizontal cut at Y=2: near the edge of the circle.
        var cutoff = new CutOff(new Vector(0, 2), CutOffAxis.Horizontal);
        cutoff.Regenerate(plate, ZeroClearance, cache);

        // BB would exclude X=[0,20] → cut = 80.
        // Circle chord at Y=2 is much shorter → cut > 80.
        var totalCutLength = TotalCutLength(cutoff.Drawing.Program, CutOffAxis.Horizontal);
        Assert.True(totalCutLength > 80, $"Circle horizontal cut should use geometry. Got {totalCutLength:F2}");
    }

    [Fact]
    public void Clearance_ExpandsGeometryExclusion()
    {
        var drawing = new Drawing("sq", MakeSquare(20));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(10, 10);
        plate.Parts.Add(part);

        var settings = new CutOffSettings { PartClearance = 5.0 };
        var cache = Plate.BuildPerimeterCache(plate);
        var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, settings, cache);

        // Square at Y=[10,30]. With 5 clearance → exclusion [5,35].
        // Segments: [0,5] and [35,100] → 4 codes.
        Assert.Equal(4, cutoff.Drawing.Program.Codes.Count);
    }

    [Fact]
    public void BuildPerimeterCache_OpenContourGetsConvexHull()
    {
        var pgm = new Program();
        pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
        pgm.Codes.Add(new LinearMove(new Vector(10, 10)));

        var plate = new Plate(100, 100);
        plate.Parts.Add(new Part(new Drawing("open", pgm)));

        var cache = Plate.BuildPerimeterCache(plate);
        Assert.Single(cache);

        var perimeter = cache[plate.Parts[0]];
        Assert.NotNull(perimeter);
        Assert.IsType<Polygon>(perimeter);
    }

    [Fact]
    public void NullCache_FallsBackToBoundingBox()
    {
        // Without a cache, should still work (using BB fallback).
        var drawing = new Drawing("sq", MakeSquare(20));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        part.Location = new Vector(10, 10);
        plate.Parts.Add(part);

        var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, ZeroClearance, null);

        Assert.True(cutoff.Drawing.Program.Codes.Count > 0);
    }

    [Fact]
    public void MultipleParts_IndependentExclusions()
    {
        var plate = new Plate(100, 100);

        var sq1 = new Drawing("sq1", MakeSquare(10));
        var p1 = Part.CreateAtOrigin(sq1);
        p1.Location = new Vector(10, 10);
        plate.Parts.Add(p1);

        var sq2 = new Drawing("sq2", MakeSquare(10));
        var p2 = Part.CreateAtOrigin(sq2);
        p2.Location = new Vector(10, 50);
        plate.Parts.Add(p2);

        // Vertical cut at X=15 crosses both parts.
        var cutoff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
        cutoff.Regenerate(plate, ZeroClearance);

        // 3 segments: before p1, between p1 and p2, after p2 → 6 codes
        Assert.Equal(6, cutoff.Drawing.Program.Codes.Count);
    }

    [Fact]
    public void CollectPoints_LinesAndArcs_ReturnsAllPoints()
    {
        var entities = new List<Entity>
        {
            new Line(new Vector(0, 0), new Vector(10, 0)),
            new Arc(new Vector(5, 5), 5, 0, System.Math.PI)
        };

        var points = entities.CollectPoints();

        // Line: 2 points. Arc: 2 endpoints + 4 cardinals = 6. Total = 8.
        Assert.Equal(8, points.Count);
    }

    [Fact]
    public void PlatePerimeterCache_ReturnsOneEntryPerPart()
    {
        var plate = new Plate(100, 100);
        plate.Parts.Add(new Part(new Drawing("a", MakeSquare(10))));
        plate.Parts.Add(new Part(new Drawing("b", MakeCircle(5))));
        plate.Parts.Add(new Part(new Drawing("c", MakeDiamond(8))));

        var cache = Plate.BuildPerimeterCache(plate);
        Assert.Equal(3, cache.Count);
    }

    [Fact]
    public void PlatePerimeterCache_SkipsCutOffParts()
    {
        var plate = new Plate(100, 100);
        plate.Parts.Add(new Part(new Drawing("real", MakeSquare(10))));
        plate.Parts.Add(new Part(new Drawing("cutoff", new Program()) { IsCutOff = true }));

        var cache = Plate.BuildPerimeterCache(plate);
        Assert.Single(cache);
    }

    [Fact]
    public void RegenerateCutOffs_UsesGeometryExclusions()
    {
        // Circle radius=10 at origin. Vertical cut at X=2.
        // With geometry: tighter exclusion than BB.
        var drawing = new Drawing("circ", MakeCircle(10));
        var plate = new Plate(100, 100);
        var part = Part.CreateAtOrigin(drawing);
        plate.Parts.Add(part);

        var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical);
        plate.CutOffs.Add(cutoff);
        plate.RegenerateCutOffs(new CutOffSettings { PartClearance = 0 });

        // Find the materialized cut-off part
        var cutPart = plate.Parts.First(p => p.BaseDrawing.IsCutOff);
        // BB would give 80 (100 - 20). Geometry should give more.
        var totalCutLength = TotalCutLength(cutPart.BaseDrawing.Program);
        Assert.True(totalCutLength > 80, $"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}");
    }

    [Fact]
    public void ShapeProfile_SelectsLargestShapeAsPerimeter()
    {
        // Outer square: (5,0)→(25,0)→(25,20)→(5,20)→(5,0)
        // Inner cutout: (0,5)→(10,5)→(10,15)→(0,15)→(0,5)
        // The cutout has Left=0, perimeter has Left=5.
        // Old heuristic would pick the cutout as perimeter.
        var outer = new Shape();
        outer.Entities.Add(new Line(new Vector(5, 0), new Vector(25, 0)));
        outer.Entities.Add(new Line(new Vector(25, 0), new Vector(25, 20)));
        outer.Entities.Add(new Line(new Vector(25, 20), new Vector(5, 20)));
        outer.Entities.Add(new Line(new Vector(5, 20), new Vector(5, 0)));

        var inner = new Shape();
        inner.Entities.Add(new Line(new Vector(0, 5), new Vector(10, 5)));
        inner.Entities.Add(new Line(new Vector(10, 5), new Vector(10, 15)));
        inner.Entities.Add(new Line(new Vector(10, 15), new Vector(0, 15)));
        inner.Entities.Add(new Line(new Vector(0, 15), new Vector(0, 5)));

        // Combine all entities (simulating what ShapeBuilder.GetShapes would produce)
        var entities = new List<Entity>();
        entities.AddRange(inner.Entities);  // inner first — worst case for old heuristic
        entities.AddRange(outer.Entities);

        var profile = new ShapeProfile(entities);

        // Perimeter should be the outer (larger) shape
        var bb = profile.Perimeter.BoundingBox;
        Assert.Equal(20.0, bb.Width, 1);
        Assert.Equal(20.0, bb.Length, 1);
    }
}
