package main

import (
	"fmt"
	"math"
	"os"
)

// snapToLine rounds a dimension to the nearest quarter-multiple of lineWidth.
// If lineWidth is 0, the value is returned unchanged.
func snapToLine(v, lineWidth float64) float64 {
	if lineWidth <= 0 {
		return v
	}
	unit := lineWidth / 4.0
	return math.Round(v/unit) * unit
}

func WriteSCAD(filename string, triangles [][3]Point) error {
	// Fallback/legacy mesh WriteSCAD
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()

	fmt.Fprintf(f, "// Generated by pcb-to-stencil\npolyhedron(\n  points=[\n")
	for i, t := range triangles {
		fmt.Fprintf(f, "    [%f, %f, %f], [%f, %f, %f], [%f, %f, %f]", t[0].X, t[0].Y, t[0].Z, t[1].X, t[1].Y, t[1].Z, t[2].X, t[2].Y, t[2].Z)
		if i < len(triangles)-1 {
			fmt.Fprintf(f, ",\n")
		} else {
			fmt.Fprintf(f, "\n")
		}
	}
	fmt.Fprintf(f, "  ],\n  faces=[\n")
	for i := 0; i < len(triangles); i++ {
		idx := i * 3
		fmt.Fprintf(f, "    [%d, %d, %d]", idx, idx+1, idx+2)
		if i < len(triangles)-1 {
			fmt.Fprintf(f, ",\n")
		} else {
			fmt.Fprintf(f, "\n")
		}
	}
	fmt.Fprintf(f, "  ]\n);\n")
	return nil
}

// approximateArc returns intermediate arc points from (x1,y1) to (x2,y2),
// excluding the start point, including the end point.
func approximateArc(x1, y1, x2, y2, iVal, jVal float64, mode string) [][2]float64 {
	centerX := x1 + iVal
	centerY := y1 + jVal
	radius := math.Sqrt(iVal*iVal + jVal*jVal)
	startAngle := math.Atan2(y1-centerY, x1-centerX)
	endAngle := math.Atan2(y2-centerY, x2-centerX)
	if mode == "G03" {
		if endAngle <= startAngle {
			endAngle += 2 * math.Pi
		}
	} else {
		if startAngle <= endAngle {
			startAngle += 2 * math.Pi
		}
	}
	arcLen := math.Abs(endAngle-startAngle) * radius
	steps := int(arcLen * 8)
	if steps < 4 {
		steps = 4
	}
	if steps > 128 {
		steps = 128
	}
	pts := make([][2]float64, steps)
	for s := 0; s < steps; s++ {
		t := float64(s+1) / float64(steps)
		a := startAngle + t*(endAngle-startAngle)
		pts[s] = [2]float64{centerX + radius*math.Cos(a), centerY + radius*math.Sin(a)}
	}
	return pts
}

// writeApertureFlash2D writes a 2D aperture shape centered at (x, y) into a SCAD file.
// gf is needed to resolve macro apertures. lw is the nozzle line width for snapping.
func writeApertureFlash2D(f *os.File, gf *GerberFile, ap Aperture, x, y, lw float64, indent string) {
	switch ap.Type {
	case "C":
		if len(ap.Modifiers) > 0 {
			r := snapToLine(ap.Modifiers[0]/2, lw)
			fmt.Fprintf(f, "%stranslate([%f, %f]) circle(r=%f);\n", indent, x, y, r)
		}
	case "R":
		if len(ap.Modifiers) >= 2 {
			w, h := snapToLine(ap.Modifiers[0], lw), snapToLine(ap.Modifiers[1], lw)
			fmt.Fprintf(f, "%stranslate([%f, %f]) square([%f, %f], center=true);\n", indent, x, y, w, h)
		}
	case "O":
		if len(ap.Modifiers) >= 2 {
			w, h := snapToLine(ap.Modifiers[0], lw), snapToLine(ap.Modifiers[1], lw)
			r := math.Min(w, h) / 2
			fmt.Fprintf(f, "%stranslate([%f, %f]) hull() {\n", indent, x, y)
			if w >= h {
				d := (w - h) / 2
				fmt.Fprintf(f, "%s  translate([%f, 0]) circle(r=%f);\n", indent, d, r)
				fmt.Fprintf(f, "%s  translate([%f, 0]) circle(r=%f);\n", indent, -d, r)
			} else {
				d := (h - w) / 2
				fmt.Fprintf(f, "%s  translate([0, %f]) circle(r=%f);\n", indent, d, r)
				fmt.Fprintf(f, "%s  translate([0, %f]) circle(r=%f);\n", indent, -d, r)
			}
			fmt.Fprintf(f, "%s}\n", indent)
		}
	case "P":
		if len(ap.Modifiers) >= 2 {
			dia, numV := ap.Modifiers[0], int(ap.Modifiers[1])
			r := snapToLine(dia/2, lw)
			rot := 0.0
			if len(ap.Modifiers) >= 3 {
				rot = ap.Modifiers[2]
			}
			fmt.Fprintf(f, "%stranslate([%f, %f]) rotate([0, 0, %f]) circle(r=%f, $fn=%d);\n",
				indent, x, y, rot, r, numV)
		}
	default:
		// Macro aperture – compute bounding box from primitives and emit a simple square.
		if gf == nil {
			return
		}
		macro, ok := gf.State.Macros[ap.Type]
		if !ok {
			return
		}
		minX, minY := math.Inf(1), math.Inf(1)
		maxX, maxY := math.Inf(-1), math.Inf(-1)
		trackPt := func(px, py, radius float64) {
			if px-radius < minX { minX = px - radius }
			if px+radius > maxX { maxX = px + radius }
			if py-radius < minY { minY = py - radius }
			if py+radius > maxY { maxY = py + radius }
		}
		for _, prim := range macro.Primitives {
			switch prim.Code {
			case 1: // Circle
				if len(prim.Modifiers) >= 4 {
					dia := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
					cx := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
					cy := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
					trackPt(cx, cy, dia/2)
				}
			case 4: // Outline polygon
				if len(prim.Modifiers) >= 3 {
					numV := int(evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers))
					for i := 0; i < numV && 2+i*2+1 < len(prim.Modifiers); i++ {
						vx := evaluateMacroExpression(prim.Modifiers[2+i*2], ap.Modifiers)
						vy := evaluateMacroExpression(prim.Modifiers[2+i*2+1], ap.Modifiers)
						trackPt(vx, vy, 0)
					}
				}
			case 20: // Vector line
				if len(prim.Modifiers) >= 7 {
					w := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
					sx := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
					sy := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
					ex := evaluateMacroExpression(prim.Modifiers[4], ap.Modifiers)
					ey := evaluateMacroExpression(prim.Modifiers[5], ap.Modifiers)
					trackPt(sx, sy, w/2)
					trackPt(ex, ey, w/2)
				}
			case 21: // Center line rect
				if len(prim.Modifiers) >= 6 {
					w := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
					h := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
					cx := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
					cy := evaluateMacroExpression(prim.Modifiers[4], ap.Modifiers)
					trackPt(cx, cy, math.Max(w, h)/2)
				}
			}
		}
		if !math.IsInf(minX, 1) {
			w := snapToLine(maxX-minX, lw)
			h := snapToLine(maxY-minY, lw)
			cx := (minX + maxX) / 2
			cy := (minY + maxY) / 2
			fmt.Fprintf(f, "%stranslate([%f, %f]) square([%f, %f], center=true);\n",
				indent, x+cx, y+cy, w, h)
		}
	}
}

// writeMacroPrimitive2D emits a single macro primitive as 2D SCAD geometry.
func writeMacroPrimitive2D(f *os.File, prim MacroPrimitive, params []float64, indent string) {
	switch prim.Code {
	case 1: // Circle: Exposure, Diameter, CenterX, CenterY
		if len(prim.Modifiers) >= 4 {
			exposure := evaluateMacroExpression(prim.Modifiers[0], params)
			if exposure == 0 {
				return
			}
			dia := evaluateMacroExpression(prim.Modifiers[1], params)
			cx := evaluateMacroExpression(prim.Modifiers[2], params)
			cy := evaluateMacroExpression(prim.Modifiers[3], params)
			fmt.Fprintf(f, "%stranslate([%f, %f]) circle(r=%f);\n", indent, cx, cy, dia/2)
		}
	case 4: // Outline (Polygon): Exposure, NumVertices, X1,Y1,...,Xn,Yn, Rotation
		if len(prim.Modifiers) >= 3 {
			exposure := evaluateMacroExpression(prim.Modifiers[0], params)
			if exposure == 0 {
				return
			}
			numV := int(evaluateMacroExpression(prim.Modifiers[1], params))
			if len(prim.Modifiers) < 2+numV*2+1 {
				return
			}
			rot := evaluateMacroExpression(prim.Modifiers[2+numV*2], params)
			fmt.Fprintf(f, "%srotate([0, 0, %f]) polygon(points=[\n", indent, rot)
			for i := 0; i < numV; i++ {
				vx := evaluateMacroExpression(prim.Modifiers[2+i*2], params)
				vy := evaluateMacroExpression(prim.Modifiers[2+i*2+1], params)
				comma := ","
				if i == numV-1 {
					comma = ""
				}
				fmt.Fprintf(f, "%s  [%f, %f]%s\n", indent, vx, vy, comma)
			}
			fmt.Fprintf(f, "%s]);\n", indent)
		}
	case 5: // Regular Polygon: Exposure, NumVertices, CenterX, CenterY, Diameter, Rotation
		if len(prim.Modifiers) >= 6 {
			exposure := evaluateMacroExpression(prim.Modifiers[0], params)
			if exposure == 0 {
				return
			}
			numV := int(evaluateMacroExpression(prim.Modifiers[1], params))
			cx := evaluateMacroExpression(prim.Modifiers[2], params)
			cy := evaluateMacroExpression(prim.Modifiers[3], params)
			dia := evaluateMacroExpression(prim.Modifiers[4], params)
			rot := evaluateMacroExpression(prim.Modifiers[5], params)
			fmt.Fprintf(f, "%stranslate([%f, %f]) rotate([0, 0, %f]) circle(r=%f, $fn=%d);\n",
				indent, cx, cy, rot, dia/2, numV)
		}
	case 20: // Vector Line: Exposure, Width, StartX, StartY, EndX, EndY, Rotation
		if len(prim.Modifiers) >= 7 {
			exposure := evaluateMacroExpression(prim.Modifiers[0], params)
			if exposure == 0 {
				return
			}
			width := evaluateMacroExpression(prim.Modifiers[1], params)
			sx := evaluateMacroExpression(prim.Modifiers[2], params)
			sy := evaluateMacroExpression(prim.Modifiers[3], params)
			ex := evaluateMacroExpression(prim.Modifiers[4], params)
			ey := evaluateMacroExpression(prim.Modifiers[5], params)
			rot := evaluateMacroExpression(prim.Modifiers[6], params)
			// hull() of two squares at start/end for a rectangle with the given width
			fmt.Fprintf(f, "%srotate([0, 0, %f]) hull() {\n", indent, rot)
			fmt.Fprintf(f, "%s  translate([%f, %f]) square([0.001, %f], center=true);\n", indent, sx, sy, width)
			fmt.Fprintf(f, "%s  translate([%f, %f]) square([0.001, %f], center=true);\n", indent, ex, ey, width)
			fmt.Fprintf(f, "%s}\n", indent)
		}
	case 21: // Center Line (Rect): Exposure, Width, Height, CenterX, CenterY, Rotation
		if len(prim.Modifiers) >= 6 {
			exposure := evaluateMacroExpression(prim.Modifiers[0], params)
			if exposure == 0 {
				return
			}
			w := evaluateMacroExpression(prim.Modifiers[1], params)
			h := evaluateMacroExpression(prim.Modifiers[2], params)
			cx := evaluateMacroExpression(prim.Modifiers[3], params)
			cy := evaluateMacroExpression(prim.Modifiers[4], params)
			rot := evaluateMacroExpression(prim.Modifiers[5], params)
			fmt.Fprintf(f, "%stranslate([%f, %f]) rotate([0, 0, %f]) square([%f, %f], center=true);\n",
				indent, cx, cy, rot, w, h)
		}
	}
}

// writeApertureLinearDraw2D writes a 2D stroke between two points using hull() of the aperture.
func writeApertureLinearDraw2D(f *os.File, ap Aperture, x1, y1, x2, y2 float64, indent string) {
	switch ap.Type {
	case "C":
		if len(ap.Modifiers) > 0 {
			r := ap.Modifiers[0] / 2
			fmt.Fprintf(f, "%shull() {\n", indent)
			fmt.Fprintf(f, "%s  translate([%f, %f]) circle(r=%f);\n", indent, x1, y1, r)
			fmt.Fprintf(f, "%s  translate([%f, %f]) circle(r=%f);\n", indent, x2, y2, r)
			fmt.Fprintf(f, "%s}\n", indent)
		}
	case "R":
		if len(ap.Modifiers) >= 2 {
			w, h := ap.Modifiers[0], ap.Modifiers[1]
			fmt.Fprintf(f, "%shull() {\n", indent)
			fmt.Fprintf(f, "%s  translate([%f, %f]) square([%f, %f], center=true);\n", indent, x1, y1, w, h)
			fmt.Fprintf(f, "%s  translate([%f, %f]) square([%f, %f], center=true);\n", indent, x2, y2, w, h)
			fmt.Fprintf(f, "%s}\n", indent)
		}
	default:
		if len(ap.Modifiers) > 0 {
			r := ap.Modifiers[0] / 2
			fmt.Fprintf(f, "%shull() {\n", indent)
			fmt.Fprintf(f, "%s  translate([%f, %f]) circle(r=%f);\n", indent, x1, y1, r)
			fmt.Fprintf(f, "%s  translate([%f, %f]) circle(r=%f);\n", indent, x2, y2, r)
			fmt.Fprintf(f, "%s}\n", indent)
		}
	}
}

// writeGerberShapes2D writes a 2D SCAD union body representing all drawn shapes
// from the Gerber file. Call this inside a union() block.
func writeGerberShapes2D(f *os.File, gf *GerberFile, lw float64, indent string) {
	curX, curY := 0.0, 0.0
	curDCode := 0
	interpolationMode := "G01"
	inRegion := false
	var regionPts [][2]float64

	for _, cmd := range gf.Commands {
		if cmd.Type == "APERTURE" {
			if cmd.D != nil {
				curDCode = *cmd.D
			}
			continue
		}
		if cmd.Type == "G01" || cmd.Type == "G02" || cmd.Type == "G03" {
			interpolationMode = cmd.Type
			continue
		}
		if cmd.Type == "G36" {
			inRegion = true
			regionPts = nil
			continue
		}
		if cmd.Type == "G37" {
			if len(regionPts) >= 3 {
				fmt.Fprintf(f, "%spolygon(points=[\n", indent)
				for i, pt := range regionPts {
					fmt.Fprintf(f, "%s  [%f, %f]", indent, pt[0], pt[1])
					if i < len(regionPts)-1 {
						fmt.Fprintf(f, ",")
					}
					fmt.Fprintf(f, "\n")
				}
				fmt.Fprintf(f, "%s]);\n", indent)
			}
			inRegion = false
			regionPts = nil
			continue
		}

		prevX, prevY := curX, curY
		if cmd.X != nil {
			curX = *cmd.X
		}
		if cmd.Y != nil {
			curY = *cmd.Y
		}

		if inRegion {
			switch cmd.Type {
			case "MOVE":
				regionPts = append(regionPts, [2]float64{curX, curY})
			case "DRAW":
				if interpolationMode == "G01" {
					regionPts = append(regionPts, [2]float64{curX, curY})
				} else {
					iVal, jVal := 0.0, 0.0
					if cmd.I != nil {
						iVal = *cmd.I
					}
					if cmd.J != nil {
						jVal = *cmd.J
					}
					arcPts := approximateArc(prevX, prevY, curX, curY, iVal, jVal, interpolationMode)
					regionPts = append(regionPts, arcPts...)
				}
			}
			continue
		}

		ap, ok := gf.State.Apertures[curDCode]
		if !ok {
			continue
		}
		switch cmd.Type {
		case "FLASH":
			writeApertureFlash2D(f, gf, ap, curX, curY, lw, indent)
		case "DRAW":
			if interpolationMode == "G01" {
				writeApertureLinearDraw2D(f, ap, prevX, prevY, curX, curY, indent)
			} else {
				iVal, jVal := 0.0, 0.0
				if cmd.I != nil {
					iVal = *cmd.I
				}
				if cmd.J != nil {
					jVal = *cmd.J
				}
				arcPts := approximateArc(prevX, prevY, curX, curY, iVal, jVal, interpolationMode)
				all := append([][2]float64{{prevX, prevY}}, arcPts...)
				for i := 0; i < len(all)-1; i++ {
					writeApertureLinearDraw2D(f, ap, all[i][0], all[i][1], all[i+1][0], all[i+1][1], indent)
				}
			}
		}
	}
}

// WriteStencilSCAD generates native parametric OpenSCAD for a solder paste stencil.
// Instead of a rasterised mesh, it uses CSG primitives (circles, squares, hulls,
// polygons) so the result prints cleanly at any nozzle size.
func WriteStencilSCAD(filename string, gf *GerberFile, outlineGf *GerberFile, cfg Config, bounds *Bounds) error {
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()

	fmt.Fprintf(f, "// Generated by pcb-to-stencil (Native SCAD)\n")
	fmt.Fprintf(f, "$fn = 60;\n\n")
	lw := cfg.LineWidth
	fmt.Fprintf(f, "stencil_height = %f; // mm – solder paste layer thickness\n", snapToLine(cfg.StencilHeight, lw))
	fmt.Fprintf(f, "wall_height    = %f; // mm – alignment frame height\n", snapToLine(cfg.WallHeight, lw))
	fmt.Fprintf(f, "wall_thickness = %f; // mm – alignment frame wall thickness\n", snapToLine(cfg.WallThickness, lw))
	if lw > 0 {
		fmt.Fprintf(f, "// line_width  = %f; // mm – all dimensions snapped to multiples/fractions of this\n", lw)
	}
	fmt.Fprintf(f, "\n")

	var outlineVerts [][2]float64
	if outlineGf != nil {
		outlineVerts = ExtractPolygonFromGerber(outlineGf)
	}

	centerX := (bounds.MinX + bounds.MaxX) / 2.0
	centerY := (bounds.MinY + bounds.MaxY) / 2.0

	// Board outline module (2D)
	if len(outlineVerts) > 0 {
		fmt.Fprintf(f, "module board_outline() {\n  polygon(points=[\n")
		for i, v := range outlineVerts {
			fmt.Fprintf(f, "    [%f, %f]", v[0], v[1])
			if i < len(outlineVerts)-1 {
				fmt.Fprintf(f, ",")
			}
			fmt.Fprintf(f, "\n")
		}
		fmt.Fprintf(f, "  ]);\n}\n\n")
	} else {
		// Fallback: bounding rectangle
		fmt.Fprintf(f, "module board_outline() {\n")
		fmt.Fprintf(f, "  translate([%f, %f]) square([%f, %f]);\n",
			bounds.MinX, bounds.MinY, bounds.MaxX-bounds.MinX, bounds.MaxY-bounds.MinY)
		fmt.Fprintf(f, "}\n\n")
	}

	// Paste pad openings module (2D union of all aperture shapes)
	fmt.Fprintf(f, "module paste_pads() {\n  union() {\n")
	writeGerberShapes2D(f, gf, cfg.LineWidth, "    ")
	fmt.Fprintf(f, "  }\n}\n\n")

	// Main body – centred at origin for easy placement on the print bed
	fmt.Fprintf(f, "translate([%f, %f, 0]) {\n", -centerX, -centerY)
	fmt.Fprintf(f, "  difference() {\n")
	fmt.Fprintf(f, "    union() {\n")
	fmt.Fprintf(f, "      // Thin stencil plate\n")
	fmt.Fprintf(f, "      linear_extrude(height=stencil_height)\n")
	fmt.Fprintf(f, "        board_outline();\n")
	fmt.Fprintf(f, "      // Alignment wall – keeps stencil registered to the PCB edge\n")
	fmt.Fprintf(f, "      linear_extrude(height=wall_height)\n")
	fmt.Fprintf(f, "        difference() {\n")
	fmt.Fprintf(f, "          offset(r=wall_thickness) board_outline();\n")
	fmt.Fprintf(f, "          board_outline();\n")
	fmt.Fprintf(f, "        }\n")
	fmt.Fprintf(f, "    }\n")
	fmt.Fprintf(f, "    // Paste pad cutouts (punched through the stencil plate)\n")
	fmt.Fprintf(f, "    translate([0, 0, -0.1])\n")
	fmt.Fprintf(f, "      linear_extrude(height=stencil_height + 0.2)\n")
	fmt.Fprintf(f, "        paste_pads();\n")
	fmt.Fprintf(f, "  }\n")
	fmt.Fprintf(f, "}\n")

	return nil
}

// ExtractPolygonFromGerber traces the Edge.Cuts gerber to form a continuous 2D polygon
func ExtractPolygonFromGerber(gf *GerberFile) [][2]float64 {
	var strokes [][][2]float64
	var currentStroke [][2]float64
	curX, curY := 0.0, 0.0
	interpolationMode := "G01"

	for _, cmd := range gf.Commands {
		if cmd.Type == "G01" || cmd.Type == "G02" || cmd.Type == "G03" {
			interpolationMode = cmd.Type
			continue
		}

		prevX, prevY := curX, curY
		if cmd.X != nil {
			curX = *cmd.X
		}
		if cmd.Y != nil {
			curY = *cmd.Y
		}

		if cmd.Type == "MOVE" {
			if len(currentStroke) > 0 {
				strokes = append(strokes, currentStroke)
				currentStroke = nil
			}
		} else if cmd.Type == "DRAW" {
			if len(currentStroke) == 0 {
				currentStroke = append(currentStroke, [2]float64{prevX, prevY})
			}

			if interpolationMode == "G01" {
				currentStroke = append(currentStroke, [2]float64{curX, curY})
			} else {
				iVal, jVal := 0.0, 0.0
				if cmd.I != nil {
					iVal = *cmd.I
				}
				if cmd.J != nil {
					jVal = *cmd.J
				}
				centerX, centerY := prevX+iVal, prevY+jVal
				radius := math.Sqrt(iVal*iVal + jVal*jVal)
				startAngle := math.Atan2(prevY-centerY, prevX-centerX)
				endAngle := math.Atan2(curY-centerY, curX-centerX)
				if interpolationMode == "G03" {
					if endAngle <= startAngle {
						endAngle += 2 * math.Pi
					}
				} else {
					if startAngle <= endAngle {
						startAngle += 2 * math.Pi
					}
				}
				arcLen := math.Abs(endAngle-startAngle) * radius
				steps := int(arcLen * 10)
				if steps < 5 {
					steps = 5
				}
				for s := 1; s <= steps; s++ {
					t := float64(s) / float64(steps)
					a := startAngle + t*(endAngle-startAngle)
					ax, ay := centerX+radius*math.Cos(a), centerY+radius*math.Sin(a)
					currentStroke = append(currentStroke, [2]float64{ax, ay})
				}
			}
		}
	}
	if len(currentStroke) > 0 {
		strokes = append(strokes, currentStroke)
	}

	if len(strokes) == 0 {
		return nil
	}

	// Stitch strokes into closed loops
	var loops [][][2]float64
	used := make([]bool, len(strokes))
	epsilon := 0.05 // 0.05mm tolerance

	for startIdx := 0; startIdx < len(strokes); startIdx++ {
		if used[startIdx] {
			continue
		}
		used[startIdx] = true
		path := append([][2]float64{}, strokes[startIdx]...)

		for {
			endPt := path[len(path)-1]
			startPt := path[0]
			found := false

			for j := 0; j < len(strokes); j++ {
				if used[j] {
					continue
				}
				s := strokes[j]
				sStart := s[0]
				sEnd := s[len(s)-1]

				dist := func(a, b [2]float64) float64 {
					dx, dy := a[0]-b[0], a[1]-b[1]
					return math.Sqrt(dx*dx + dy*dy)
				}

				if dist(endPt, sStart) < epsilon {
					path = append(path, s[1:]...)
					used[j] = true
					found = true
					break
				} else if dist(endPt, sEnd) < epsilon {
					for k := len(s) - 2; k >= 0; k-- {
						path = append(path, s[k])
					}
					used[j] = true
					found = true
					break
				} else if dist(startPt, sEnd) < epsilon {
					// prepend
					newPath := append([][2]float64{}, s[:len(s)-1]...)
					path = append(newPath, path...)
					used[j] = true
					found = true
					break
				} else if dist(startPt, sStart) < epsilon {
					// reversed prepend
					var newPath [][2]float64
					for k := len(s) - 1; k > 0; k-- {
						newPath = append(newPath, s[k])
					}
					path = append(newPath, path...)
					used[j] = true
					found = true
					break
				}
			}
			if !found {
				break
			}
		}
		loops = append(loops, path)
	}

	// Find the longest loop (the main board outline)
	var bestLoop [][2]float64
	maxLen := 0.0
	for _, l := range loops {
		loopLen := 0.0
		for i := 0; i < len(l)-1; i++ {
			dx := l[i+1][0] - l[i][0]
			dy := l[i+1][1] - l[i][1]
			loopLen += math.Sqrt(dx*dx + dy*dy)
		}
		if loopLen > maxLen {
			maxLen = loopLen
			bestLoop = l
		}
	}

	// Always ensure path is closed
	if len(bestLoop) > 2 {
		first := bestLoop[0]
		last := bestLoop[len(bestLoop)-1]
		if math.Abs(first[0]-last[0]) > epsilon || math.Abs(first[1]-last[1]) > epsilon {
			bestLoop = append(bestLoop, first)
		}
	}

	return bestLoop
}

// WriteNativeSCAD generates native parametrically defined CSG OpenSCAD code
func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64, cfg EnclosureConfig, holes []DrillHole, cutouts []SideCutout, sides []BoardSide, minBX, maxBX, boardCenterY float64) error {
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()

	fmt.Fprintf(f, "// Generated by pcb-to-stencil (Native SCAD)\n")
	fmt.Fprintf(f, "$fn = 60;\n\n")

	// 1. Output the Board Polygon Module
	fmt.Fprintf(f, "module board_polygon() {\n  polygon(points=[\n")
	for i, v := range outlineVertices {
		fmt.Fprintf(f, "    [%f, %f]", v[0], v[1])
		if i < len(outlineVertices)-1 {
			fmt.Fprintf(f, ",\n")
		}
	}
	fmt.Fprintf(f, "\n  ]);\n}\n\n")

	// Dimensions
	clearance := cfg.Clearance
	wt := cfg.WallThickness
	lidThick := wt
	snapHeight := 2.5
	trayFloor := 1.5
	pcbT := cfg.PCBThickness
	totalH := cfg.WallHeight + pcbT + trayFloor
	lipH := pcbT + 1.5

	// Create Peg and Socket helper
	fmt.Fprintf(f, "module mounting_pegs(isSocket) {\n")
	for _, h := range holes {
		if h.Type == DrillTypeMounting {
			r := (h.Diameter / 2.0) - 0.15
			if isTray {
				// We subtract sockets from the tray floor
				r = (h.Diameter / 2.0) + 0.1
				fmt.Fprintf(f, "  translate([%f, %f, -1]) cylinder(r=%f, h=%f);\n", h.X, h.Y, r, trayFloor+2)
			} else {
				fmt.Fprintf(f, "  translate([%f, %f, 0]) cylinder(r=%f, h=%f);\n", h.X, h.Y, r, totalH-lidThick)
			}
		}
	}
	fmt.Fprintf(f, "}\n\n")

	// Print Side Cutouts module
	fmt.Fprintf(f, "module side_cutouts() {\n")
	for _, c := range cutouts {
		var bs *BoardSide
		for i := range sides {
			if sides[i].Num == c.Side {
				bs = &sides[i]
				break
			}
		}
		if bs == nil {
			continue
		}

		// Cutouts are relative to board. UI specifies c.Y from bottom, so c.Y adds to Z.
		z := c.Height/2 + trayFloor + pcbT + c.Y
		wallDepth := 2*(clearance+2*wt) + 2.0 // just enough to cut through walls
		w, d, h := c.Width, wallDepth, c.Height

		dx := bs.EndX - bs.StartX
		dy := bs.EndY - bs.StartY
		length := math.Sqrt(dx*dx + dy*dy)
		if length > 0 {
			dx /= length
			dy /= length
		}

		midX := bs.StartX + dx*(c.X+w/2)
		midY := bs.StartY + dy*(c.X+w/2)

		rotDeg := (bs.Angle * 180.0 / math.Pi) - 90.0

		if c.CornerRadius > 0 {
			r := c.CornerRadius
			fmt.Fprintf(f, "  translate([%f, %f, %f]) rotate([0, 0, %f]) {\n", midX, midY, z, rotDeg)
			fmt.Fprintf(f, "    hull() {\n")
			fmt.Fprintf(f, "      translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", w/2-r, h/2-r, r, d)
			fmt.Fprintf(f, "      translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", -(w/2 - r), h/2-r, r, d)
			fmt.Fprintf(f, "      translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", w/2-r, -(h/2 - r), r, d)
			fmt.Fprintf(f, "      translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", -(w/2 - r), -(h/2 - r), r, d)
			fmt.Fprintf(f, "    }\n")
			fmt.Fprintf(f, "  }\n")
		} else {
			fmt.Fprintf(f, "  translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n", midX, midY, z, rotDeg, w, d, h)
		}
	}
	fmt.Fprintf(f, "}\n\n")

	// Print Pry Slots Module
	fmt.Fprintf(f, "module pry_slots() {\n")
	pryW := 8.0
	pryD := 1.5
	fmt.Fprintf(f, "  translate([%f, %f, 0]) cube([%f, %f, %f], center=true);\n", minBX-clearance-wt+pryD/2, boardCenterY, pryD*2, pryW, snapHeight*3)
	fmt.Fprintf(f, "  translate([%f, %f, 0]) cube([%f, %f, %f], center=true);\n", maxBX+clearance+wt-pryD/2, boardCenterY, pryD*2, pryW, snapHeight*3)
	fmt.Fprintf(f, "}\n\n")

	// Print Pry Clips Module
	fmt.Fprintf(f, "module pry_clips() {\n")
	clipH := 0.8
	clipZ := trayFloor + snapHeight - clipH/2.0
	fmt.Fprintf(f, "  translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", minBX-clearance-wt-0.5, boardCenterY, clipZ, 1.0, pryW, clipH)
	fmt.Fprintf(f, "  translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", maxBX+clearance+wt+0.5, boardCenterY, clipZ, 1.0, pryW, clipH)
	fmt.Fprintf(f, "}\n\n")

	// cutoutMid returns the midpoint XY and rotation angle for a side cutout,
	// matching the geometry used in side_cutouts().
	cutoutMid := func(c SideCutout) (midX, midY, rotDeg float64, ok bool) {
		for i := range sides {
			if sides[i].Num != c.Side {
				continue
			}
			bs := &sides[i]
			dx := bs.EndX - bs.StartX
			dy := bs.EndY - bs.StartY
			if l := math.Sqrt(dx*dx + dy*dy); l > 0 {
				dx /= l
				dy /= l
			}
			midX = bs.StartX + dx*(c.X+c.Width/2)
			midY = bs.StartY + dy*(c.X+c.Width/2)
			rotDeg = (bs.Angle*180.0/math.Pi) - 90.0
			ok = true
			return
		}
		return
	}

	centerX := cfg.OutlineBounds.MinX + (cfg.OutlineBounds.MaxX-cfg.OutlineBounds.MinX)/2.0
	centerY := cfg.OutlineBounds.MinY + (cfg.OutlineBounds.MaxY-cfg.OutlineBounds.MinY)/2.0
	fmt.Fprintf(f, "translate([%f, %f, 0]) {\n", -centerX, -centerY)

	if isTray {
		// --- TRAY ---
		fmt.Fprintf(f, "// --- TRAY ---\n")
		fmt.Fprintf(f, "difference() {\n")
		fmt.Fprintf(f, "  union() {\n")
		fmt.Fprintf(f, "    // Tray Floor (extends to clearance + 2*wt so it is flush with enclosure outside)\n")
		fmt.Fprintf(f, "    linear_extrude(height=%f) offset(r=%f) board_polygon();\n", trayFloor, clearance+2*wt)
		fmt.Fprintf(f, "    // Tray Inner Wall (thickness wt)\n")
		fmt.Fprintf(f, "    translate([0,0,%f]) linear_extrude(height=%f) difference() {\n", trayFloor, snapHeight)
		fmt.Fprintf(f, "      offset(r=%f) board_polygon();\n", clearance+wt)
		fmt.Fprintf(f, "      offset(r=%f) board_polygon();\n", clearance)
		fmt.Fprintf(f, "    }\n")
		fmt.Fprintf(f, "  }\n")

		fmt.Fprintf(f, "  // Subtract Lip Recess (for easy opening)\n")
		fmt.Fprintf(f, "  translate([0,0,-1]) linear_extrude(height=%f) difference() {\n", trayFloor+lipH+0.5)
		fmt.Fprintf(f, "    offset(r=%f) board_polygon();\n", clearance+2*wt+1.0)
		fmt.Fprintf(f, "    offset(r=%f) board_polygon();\n", clearance+2*wt-0.5) // Assuming lipCut is 0.5mm
		fmt.Fprintf(f, "  }\n")

		// Remove peg holes from floor
		for _, hole := range holes {
			if hole.Type != DrillTypeMounting {
				continue
			}
			socketRadius := (hole.Diameter / 2.0) + 0.1
			fmt.Fprintf(f, "  translate([%f,%f,-1]) cylinder(h=%f, r=%f, $fn=32);\n", hole.X, hole.Y, trayFloor+2, socketRadius)
		}
		fmt.Fprintf(f, "  side_cutouts();\n")
		// Board dado on tray: layer-aware groove on each side with port cutouts.
		{
			trayWallDepth := 2*(clearance+wt) + 2.0
			type trayDadoInfo struct {
				hasF     bool
				hasB     bool
				fPortTop float64
				bPortBot float64
			}
			trayDadoSides := make(map[int]*trayDadoInfo)
			for _, c := range cutouts {
				di, ok := trayDadoSides[c.Side]
				if !ok {
					di = &trayDadoInfo{fPortTop: 0, bPortBot: 1e9}
					trayDadoSides[c.Side] = di
				}
				portBot := trayFloor + pcbT + c.Y
				portTop := portBot + c.Height
				if c.Layer == "F" {
					di.hasF = true
					if portTop > di.fPortTop {
						di.fPortTop = portTop
					}
				} else {
					di.hasB = true
					if portBot < di.bPortBot {
						di.bPortBot = portBot
					}
				}
			}
			trayH := trayFloor + snapHeight + wt + pcbT + 2.0
			for _, bs := range sides {
				di, ok := trayDadoSides[bs.Num]
				if !ok {
					continue
				}
				midX := (bs.StartX + bs.EndX) / 2.0
				midY := (bs.StartY + bs.EndY) / 2.0
				rotDeg := (bs.Angle*180.0/math.Pi) - 90.0
				dadoLen := bs.Length + 1.0
				if di.hasF {
					// F-layer: dado above ports (toward lid), same direction as enclosure
					dadoBot := di.fPortTop
					dadoH := trayH - dadoBot
					if dadoH > 0.1 {
						fmt.Fprintf(f, "  // Board dado (F-layer) on side %d\n", bs.Num)
						fmt.Fprintf(f, "  translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
							midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, trayWallDepth, dadoH)
					}
				}
				if di.hasB {
					// B-layer: dado below ports (toward floor)
					dadoBot := trayFloor + 0.3
					dadoH := di.bPortBot - dadoBot
					if dadoH > 0.1 {
						fmt.Fprintf(f, "  // Board dado (B-layer) on side %d\n", bs.Num)
						fmt.Fprintf(f, "  translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
							midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, trayWallDepth, dadoH)
					}
				}
			}
		}
		fmt.Fprintf(f, "}\n")
		fmt.Fprintf(f, "pry_clips();\n\n")

	} else {
		// --- ENCLOSURE ---
		fmt.Fprintf(f, "// --- ENCLOSURE ---\n")
		fmt.Fprintf(f, "difference() {\n")
		fmt.Fprintf(f, "  union() {\n")
		fmt.Fprintf(f, "    // Outer Enclosure block (accommodates Tray Wall + Enclosure Wall)\n")
		fmt.Fprintf(f, "    translate([0,0,%f]) linear_extrude(height=%f) offset(r=%f) board_polygon();\n", trayFloor, totalH-trayFloor, clearance+2*wt)
		fmt.Fprintf(f, "  }\n")
		fmt.Fprintf(f, "  // Subtract Inner Cavity (Base clearance around board)\n")
		fmt.Fprintf(f, "  translate([0,0,-1]) linear_extrude(height=%f) offset(r=%f) board_polygon();\n", totalH-lidThick+1, clearance)
		fmt.Fprintf(f, "  // Subtract Tray Recess (Accommodates Tray Wall)\n")
		fmt.Fprintf(f, "  translate([0,0,-1]) linear_extrude(height=%f) offset(r=%f) board_polygon();\n", trayFloor+snapHeight+0.2, clearance+wt+0.15)

		fmt.Fprintf(f, "  // Vertical relief slots for the tray clips to slide into\n")
		reliefClipZ := trayFloor + snapHeight
		reliefH := reliefClipZ + 1.0
		reliefZ := trayFloor - 1.0
		fmt.Fprintf(f, "  translate([%f, %f, %f]) cube([1.5, %f, %f], center=true);\n", minBX-clearance-wt-0.25, boardCenterY, reliefZ, pryW+1.0, reliefH)
		fmt.Fprintf(f, "  translate([%f, %f, %f]) cube([1.5, %f, %f], center=true);\n", maxBX+clearance+wt+0.25, boardCenterY, reliefZ, pryW+1.0, reliefH)

		fmt.Fprintf(f, "  pry_slots();\n")

		// Port cutouts – only these go through the full wall to the outside
		fmt.Fprintf(f, "  side_cutouts();\n")

		wallDepth := 2*(clearance+2*wt) + 2.0
		lidBottom := totalH - lidThick

		// Inner wall ring helper – used to limit slots and dado to the
		// inner rim only (outer wall stays solid, only ports break through).
		// Inner wall spans from offset(clearance) to offset(clearance+wt).
		fmt.Fprintf(f, "  // --- Entry slots & board dado (inner wall only) ---\n")
		fmt.Fprintf(f, "  intersection() {\n")
		fmt.Fprintf(f, "    // Clamp to inner wall ring\n")
		fmt.Fprintf(f, "    translate([0,0,%f]) linear_extrude(height=%f) difference() {\n", trayFloor-1, totalH+2)
		fmt.Fprintf(f, "      offset(r=%f) board_polygon();\n", clearance+wt+0.5)
		fmt.Fprintf(f, "      offset(r=%f) board_polygon();\n", clearance-0.5)
		fmt.Fprintf(f, "    }\n")
		fmt.Fprintf(f, "    union() {\n")

		// Port entry slots – vertical channel from port to lid/floor,
		// only in the inner wall so the outer wall stays solid.
		for _, c := range cutouts {
			mX, mY, mRot, ok := cutoutMid(c)
			if !ok {
				continue
			}
			zTopCut := trayFloor + pcbT + c.Y + c.Height

			if c.Layer == "F" {
				// F-layer: ports on top of board, slot from port top toward lid (plate)
				slotH := lidBottom - zTopCut
				if slotH > 0.1 {
					fmt.Fprintf(f, "      // Port entry slot (F-layer, open toward plate)\n")
					fmt.Fprintf(f, "      translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
						mX, mY, zTopCut+slotH/2.0, mRot, c.Width, wallDepth, slotH)
				}
			} else {
				// B-layer: ports under board, slot from floor up to port bottom
				zBotCut := trayFloor + pcbT + c.Y
				slotH := zBotCut - (trayFloor + 0.3)
				if slotH > 0.1 {
					slotBot := trayFloor + 0.3
					fmt.Fprintf(f, "      // Port entry slot (B-layer, open toward rim)\n")
					fmt.Fprintf(f, "      translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
						mX, mY, slotBot+slotH/2.0, mRot, c.Width, wallDepth, slotH)
				}
			}
		}

		// Board dado – full-length groove at PCB height, inner wall only.
		// For F-layer: dado sits below ports (board under ports), from tray floor to port bottom.
		// For B-layer: dado sits above ports (board over ports), from port top to lid.
		// Collect per-side: lowest port bottom (F) or highest port top (B).
		type dadoInfo struct {
			hasF     bool
			hasB     bool
			fPortTop float64 // highest port-top on this side (F-layer)
			bPortBot float64 // lowest port-bottom on this side (B-layer)
		}
		dadoSides := make(map[int]*dadoInfo)
		for _, c := range cutouts {
			di, ok := dadoSides[c.Side]
			if !ok {
				di = &dadoInfo{fPortTop: 0, bPortBot: 1e9}
				dadoSides[c.Side] = di
			}
			portBot := trayFloor + pcbT + c.Y
			portTop := portBot + c.Height
			if c.Layer == "F" {
				di.hasF = true
				if portTop > di.fPortTop {
					di.fPortTop = portTop
				}
			} else {
				di.hasB = true
				if portBot < di.bPortBot {
					di.bPortBot = portBot
				}
			}
		}
		for _, bs := range sides {
			di, ok := dadoSides[bs.Num]
			if !ok {
				continue
			}
			midX := (bs.StartX + bs.EndX) / 2.0
			midY := (bs.StartY + bs.EndY) / 2.0
			rotDeg := (bs.Angle*180.0/math.Pi) - 90.0
			dadoLen := bs.Length + 1.0
			if di.hasF {
				// F-layer: ports on top of board, dado above ports (toward lid/plate)
				dadoBot := di.fPortTop
				dadoH := lidBottom - dadoBot
				if dadoH > 0.1 {
					fmt.Fprintf(f, "      // Board dado (F-layer) on side %d\n", bs.Num)
					fmt.Fprintf(f, "      translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
						midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, wallDepth, dadoH)
				}
			}
			if di.hasB {
				// B-layer: ports under board, dado below ports (toward open rim)
				dadoBot := trayFloor + 0.3
				dadoH := di.bPortBot - dadoBot
				if dadoH > 0.1 {
					fmt.Fprintf(f, "      // Board dado (B-layer) on side %d\n", bs.Num)
					fmt.Fprintf(f, "      translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
						midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, wallDepth, dadoH)
				}
			}
		}

		fmt.Fprintf(f, "    } // end union\n")
		fmt.Fprintf(f, "  } // end intersection\n")
		fmt.Fprintf(f, "}\n")
		fmt.Fprintf(f, "mounting_pegs(false);\n")
	}

	fmt.Fprintf(f, "}\n") // Close the top-level translate

	return nil
}
