package main

import (
	"fmt"
	"image"
	"image/color"
	"math"
)

// EnclosureConfig holds parameters for enclosure generation
type EnclosureConfig struct {
	PCBThickness  float64 // mm
	WallThickness float64 // mm
	WallHeight    float64 // mm (height of walls above PCB)
	Clearance     float64 // mm (gap between PCB and enclosure wall)
	DPI           float64
	OutlineBounds *Bounds // gerber coordinate bounds for drill mapping
}

// Default enclosure values
const (
	DefaultPCBThickness  = 1.6
	DefaultEncWallHeight = 10.0
	DefaultEncWallThick  = 1.5
	DefaultClearance     = 0.3
)

// EnclosureResult contains the generated meshes
type EnclosureResult struct {
	EnclosureTriangles [][3]Point
	TrayTriangles      [][3]Point
}

// SideCutout defines a cutout on a side wall face
type SideCutout struct {
	Side         int     // 1-indexed side number (matches BoardSide.Num)
	X, Y         float64 // Position on the face in mm (from StartX/StartY, from bottom)
	Width        float64 // Width in mm
	Height       float64 // Height in mm
	CornerRadius float64 // Corner radius in mm (0 for square)
	Layer        string  // "F" (front/top) or "B" (back/bottom); defaults to "F"
}

// BoardSide represents a physical straight edge of the board outline
type BoardSide struct {
	Num    int     `json:"num"`
	Label  string  `json:"label"`
	Length float64 `json:"length"`
	StartX float64 `json:"startX"`
	StartY float64 `json:"startY"`
	EndX   float64 `json:"endX"`
	EndY   float64 `json:"endY"`
	Angle  float64 `json:"angle"` // Angle in radians of the normal vector pushing OUT of the board
}

func perpendicularDistance(pt, lineStart, lineEnd [2]float64) float64 {
	dx := lineEnd[0] - lineStart[0]
	dy := lineEnd[1] - lineStart[1]

	// Normalize line vector
	mag := math.Sqrt(dx*dx + dy*dy)
	if mag == 0 {
		return math.Sqrt((pt[0]-lineStart[0])*(pt[0]-lineStart[0]) + (pt[1]-lineStart[1])*(pt[1]-lineStart[1]))
	}
	dx /= mag
	dy /= mag

	// Vector from lineStart to pt
	px := pt[0] - lineStart[0]
	py := pt[1] - lineStart[1]

	// Cross product gives perpendicular distance
	return math.Abs(px*dy - py*dx)
}

func simplifyPolygonRDP(points [][2]float64, epsilon float64) [][2]float64 {
	if len(points) < 3 {
		return points
	}

	dmax := 0.0
	index := 0
	end := len(points) - 1

	for i := 1; i < end; i++ {
		d := perpendicularDistance(points[i], points[0], points[end])
		if d > dmax {
			index = i
			dmax = d
		}
	}

	if dmax > epsilon {
		recResults1 := simplifyPolygonRDP(points[:index+1], epsilon)
		recResults2 := simplifyPolygonRDP(points[index:], epsilon)

		result := append([][2]float64{}, recResults1[:len(recResults1)-1]...)
		result = append(result, recResults2...)
		return result
	}

	return [][2]float64{points[0], points[end]}
}

func ExtractBoardSides(poly [][2]float64) []BoardSide {
	if len(poly) < 3 {
		return nil
	}

	// Determine "center" of polygon to find outward normals
	cx, cy := 0.0, 0.0
	for _, p := range poly {
		cx += p[0]
		cy += p[1]
	}
	cx /= float64(len(poly))
	cy /= float64(len(poly))

	// Ensure the polygon is closed for RDP, if it isn't already
	if poly[0][0] != poly[len(poly)-1][0] || poly[0][1] != poly[len(poly)-1][1] {
		poly = append(poly, poly[0])
	}

	simplified := simplifyPolygonRDP(poly, 0.2) // 0.2mm tolerance
	fmt.Printf("[DEBUG] ExtractBoardSides: poly points = %d, simplified points = %d\n", len(poly), len(simplified))

	var sides []BoardSide
	sideNum := 1

	for i := 0; i < len(simplified)-1; i++ {
		p1 := simplified[i]
		p2 := simplified[i+1]

		dx := p2[0] - p1[0]
		dy := p2[1] - p1[1]
		length := math.Sqrt(dx*dx + dy*dy)

		// Only keep substantial straight edges (e.g. > 4mm)
		if length > 4.0 {
			// Calculate outward normal angle
			// The segment path vector is (dx, dy). Normal is either (-dy, dx) or (dy, -dx)
			nx := dy
			ny := -dx
			// Dot product with center->midpoint to check if it points out
			midX := (p1[0] + p2[0]) / 2.0
			midY := (p1[1] + p2[1]) / 2.0
			vx := midX - cx
			vy := midY - cy
			if nx*vx+ny*vy < 0 {
				nx = -nx
				ny = -ny
			}
			angle := math.Atan2(ny, nx)

			sides = append(sides, BoardSide{
				Num:    sideNum,
				Label:  fmt.Sprintf("Side %d (%.1fmm)", sideNum, length),
				Length: length,
				StartX: p1[0],
				StartY: p1[1],
				EndX:   p2[0],
				EndY:   p2[1],
				Angle:  angle,
			})
			sideNum++
		}
	}
	return sides
}

// ExtractBoardSidesFromMask traces the outer boundary of a boolean mask
// and simplifies it into BoardSides. This perfectly matches the 3D generation.
func ExtractBoardSidesFromMask(mask []bool, imgW, imgH int, pixelToMM float64, bounds *Bounds) []BoardSide {
	// Find top-leftmost pixel of mask
	startX, startY := -1, -1
outer:
	for y := 0; y < imgH; y++ {
		for x := 0; x < imgW; x++ {
			if mask[y*imgW+x] {
				startX, startY = x, y
				break outer
			}
		}
	}
	if startX == -1 {
		return nil
	}

	// Moore-neighbor boundary tracing
	var boundary [][2]int
	dirs := [8][2]int{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}}

	curX, curY := startX, startY
	boundary = append(boundary, [2]int{curX, curY})

	// Initial previous neighbor direction (up/west of top-left is empty)
	pDir := 6

	for {
		found := false
		for i := 0; i < 8; i++ {
			// Scan clockwise starting from dir after the previous background pixel
			testDir := (pDir + 1 + i) % 8
			nx, ny := curX+dirs[testDir][0], curY+dirs[testDir][1]
			if nx >= 0 && nx < imgW && ny >= 0 && ny < imgH && mask[ny*imgW+nx] {
				curX, curY = nx, ny
				boundary = append(boundary, [2]int{curX, curY})
				// The new background pixel is opposite to the direction we found the solid one
				pDir = (testDir + 4) % 8
				found = true
				break
			}
		}
		if !found {
			break // Isolated pixel
		}
		// Stop when we return to the start and moved in the same direction
		if curX == startX && curY == startY {
			break
		}
		// Failsafe for complex shapes
		if len(boundary) > imgW*imgH {
			break
		}
	}

	// Convert boundary pixels to Gerber mm coordinates
	var poly [][2]float64
	for _, p := range boundary {
		px := float64(p[0])*pixelToMM + bounds.MinX
		// Image Y=0 is MaxY in Gerber space
		py := bounds.MaxY - float64(p[1])*pixelToMM
		poly = append(poly, [2]float64{px, py})
	}

	sides := ExtractBoardSides(poly)
	fmt.Printf("[DEBUG] ExtractBoardSidesFromMask: mask size=%dx%d, boundary pixels=%d, sides extracted=%d\n", imgW, imgH, len(boundary), len(sides))
	if len(sides) == 0 && len(poly) > 0 {
		fmt.Printf("[DEBUG] poly[0]=%v, poly[n/2]=%v, poly[last]=%v\n", poly[0], poly[len(poly)/2], poly[len(poly)-1])
	}
	return sides
}

// GenerateEnclosure creates enclosure + tray meshes from a board outline image and drill holes.
// The enclosure walls conform to the actual board outline shape.
// courtyardImg is optional — if provided, component courtyard regions are cut from the lid (flood-filled).
// soldermaskImg is optional — if provided, soldermask pad openings are also cut from the lid.
func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg EnclosureConfig, courtyardImg image.Image, soldermaskImg image.Image, sideCutouts []SideCutout, boardSides []BoardSide) *EnclosureResult {
	pixelToMM := 25.4 / cfg.DPI
	bounds := outlineImg.Bounds()
	imgW := bounds.Max.X
	imgH := bounds.Max.Y

	// Use ComputeWallMask to get the board shape and wall around it
	// WallThickness for enclosure = clearance + 2 * wall thickness
	clearance := cfg.Clearance
	wt := cfg.WallThickness
	lidThick := wt
	snapHeight := 2.5
	totalWallMM := clearance + 2.0*wt
	fmt.Printf("Computing board shape (wall=%.1fmm)...\n", totalWallMM)
	wallMask, boardMask := ComputeWallMask(outlineImg, totalWallMM, pixelToMM) // wallMask is now an int slice

	// Determine the actual enclosure boundary = wall | board (expanded by clearance)
	// wallMask = pixels that are the wall
	// boardMask = pixels inside the board outline
	// clearanceMask is just an expansion of boardMask using distance logic up to clearance
	// However, we already have wallDist which measures distance OUTWARD from board
	clearanceDistPx := int(clearance * cfg.DPI / 25.4)
	trayWallOuterPx := int((clearance + wt) * cfg.DPI / 25.4)
	encWallOuterPx := int((clearance + 2.0*wt) * cfg.DPI / 25.4)
	snapDepthPx := int(0.5 * cfg.DPI / 25.4)
	if snapDepthPx < 1 {
		snapDepthPx = 1
	}

	// Total height of the enclosure (from bottom of tray to top of lid)
	pcbT := cfg.PCBThickness
	trayFloor := pcbT + 0.5 // Tray floor is 0.5mm thick, sits below PCB
	totalH := trayFloor + cfg.WallHeight + lidThick

	size := imgW * imgH

	var encTris [][3]Point
	var trayTris [][3]Point

	// Mounting pegs from NPTH holes: cylinders going from lid downward
	pegMask := make([]bool, size) // true = peg/socket at this pixel (exclude from tray floor)
	if cfg.OutlineBounds != nil {
		mountingHoles := 0
		for _, h := range drillHoles {
			if h.Type != DrillTypeMounting {
				continue
			}
			mountingHoles++

			// Convert drill mm coordinates to pixel coordinates
			px := (h.X - cfg.OutlineBounds.MinX) * cfg.DPI / 25.4
			py := (h.Y - cfg.OutlineBounds.MinY) * cfg.DPI / 25.4

			// Peg radius slightly smaller than hole for press fit
			pegRadiusMM := (h.Diameter / 2) - 0.15
			pegRadiusPx := pegRadiusMM * cfg.DPI / 25.4
			// Socket radius slightly larger for easy insertion
			socketRadiusPx := (h.Diameter/2 + 0.1) * cfg.DPI / 25.4

			// Peg height: from bottom (z=0) up to lid
			pegH := totalH - lidThick

			// Scan a bounding box around the hole
			rInt := int(socketRadiusPx) + 2
			cx, cy := int(px), int(py)

			for dy := -rInt; dy <= rInt; dy++ {
				for dx := -rInt; dx <= rInt; dx++ {
					ix, iy := cx+dx, cy+dy
					if ix < 0 || ix >= imgW || iy < 0 || iy >= imgH {
						continue
					}
					dist := math.Sqrt(float64(dx*dx + dy*dy))

					// Peg cylinder (in enclosure, from z=0 up to lid)
					if dist <= pegRadiusPx {
						bx := float64(ix) * pixelToMM
						by := float64(iy) * pixelToMM
						addBoxAtZ(&encTris, bx, by, 0, pixelToMM, pixelToMM, pegH)
					}

					// Socket mask (for tray floor removal)
					if dist <= socketRadiusPx {
						pegMask[iy*imgW+ix] = true
					}
				}
			}
		}
		if mountingHoles > 0 {
			fmt.Printf("Generated %d mounting pegs\n", mountingHoles)
		}
	}

	// Pre-compute board bounding box (needed for side cutout detection and removal tabs)
	minBX, minBY := imgW, imgH
	maxBX, maxBY := 0, 0
	boardCenterX, boardCenterY := 0.0, 0.0
	boardCount := 0
	for y := 0; y < imgH; y++ {
		for x := 0; x < imgW; x++ {
			if boardMask[y*imgW+x] {
				boardCenterX += float64(x)
				boardCenterY += float64(y)
				boardCount++
				if x < minBX {
					minBX = x
				}
				if x > maxBX {
					maxBX = x
				}
				if y < minBY {
					minBY = y
				}
				if y > maxBY {
					maxBY = y
				}
			}
		}
	}

	// === APPLY PRY SLOT CUTOUTS TO WALL MASK BEFORE MESHING ===
	// We want 8mm wide by 1.5mm deep slots in the left and right exterior walls
	if boardCount > 0 {
		pryWPx := int(8.0 * cfg.DPI / 25.4)
		pryDPx := int(1.5 * cfg.DPI / 25.4)
		if pryWPx < 1 {
			pryWPx = 1
		}
		if pryDPx < 1 {
			pryDPx = 1
		}

		centerYPx := int(boardCenterY / float64(boardCount))
		leftXPx := minBX
		rightXPx := maxBX

		// For the left side, we clear the wall mask from minBX-wallPx up to minBX-wallPx+pryDPx
		for y := centerYPx - pryWPx/2; y <= centerYPx+pryWPx/2; y++ {
			if y < 0 || y >= imgH {
				continue
			}
			// Find outer edge of wall on the left
			for x := 0; x < leftXPx; x++ {
				idx := y*imgW + x
				if wallMask[idx] > clearanceDistPx {
					// Blank out the outermost pryDPx pixels of the wall
					for dx := 0; dx < pryDPx; dx++ {
						if x+dx < imgW {
							wallMask[y*imgW+(x+dx)] = -1
						}
					}
					break // Only do the outer edge
				}
			}

			// Find outer edge of wall on the right (search backwards)
			for x := imgW - 1; x > rightXPx; x-- {
				idx := y*imgW + x
				if wallMask[idx] > clearanceDistPx {
					// Blank out the outermost pryDPx pixels of the wall
					for dx := 0; dx < pryDPx; dx++ {
						if x-dx >= 0 {
							wallMask[y*imgW+(x-dx)] = -1
						}
					}
					break // Only do the outer edge
				}
			}
		}
	}

	// Build wall-cutout mask from side cutouts
	// For each side cutout, determine which wall pixels to subtract
	wallCutoutMask := make([]bool, size)
	if len(sideCutouts) > 0 && cfg.OutlineBounds != nil {
		// Board bounding box in pixels
		for y := 0; y < imgH; y++ {
			for x := 0; x < imgW; x++ {
				idx := y*imgW + x
				if !(wallMask[idx] > clearanceDistPx && wallMask[idx] <= encWallOuterPx && !boardMask[idx]) {
					continue // not a wall pixel
				}

				// Determine which side this wall pixel belongs to
				bx := float64(x)*pixelToMM + cfg.OutlineBounds.MinX
				by := cfg.OutlineBounds.MaxY - float64(y)*pixelToMM

				sideNum := -1
				minDist := math.MaxFloat64
				var posAlongSide float64

				for _, bs := range boardSides {
					dx := bs.EndX - bs.StartX
					dy := bs.EndY - bs.StartY
					lenSq := dx*dx + dy*dy
					if lenSq == 0 {
						continue
					}

					t := ((bx-bs.StartX)*dx + (by-bs.StartY)*dy) / lenSq
					tClamp := math.Max(0, math.Min(1, t))

					projX := bs.StartX + tClamp*dx
					projY := bs.StartY + tClamp*dy

					dist := math.Sqrt((bx-projX)*(bx-projX) + (by-projY)*(by-projY))
					if dist < minDist {
						minDist = dist
						sideNum = bs.Num
						posAlongSide = t * bs.Length
					}
				}

				// Check all cutouts for this side
				for _, c := range sideCutouts {
					if c.Side != sideNum {
						continue
					}
					// Check if this pixel's position falls within the cutout X range
					if posAlongSide >= c.X && posAlongSide <= c.X+c.Width {
						wallCutoutMask[idx] = true
						break
					}
				}
			}
		}
		fmt.Printf("Wall cutout mask: applied %d side cutouts\n", len(sideCutouts))
	}

	// ENCLOSURE (top shell — conforms to board shape)
	// ==========================================
	fmt.Println("Generating edge-cut conforming enclosure...")

	// The Enclosure Wall sits on top of the Tray Floor (starts at Z = trayFloor)
	// Inner Wall (above snapHeight) = `clearanceDistPx` to `trayWallOuterPx`
	// Outer Wall (full height) = `trayWallOuterPx` to `encWallOuterPx`
	for y := 0; y < imgH; y++ {
		runStartX := -1
		curIsInner := false
		curIsSnap := false
		for x := 0; x <= imgW; x++ {
			isWallPx := false
			isInnerWall := false
			isSnapGroove := false
			if x < imgW {
				idx := y*imgW + x
				dist := wallMask[idx]
				if dist > clearanceDistPx && dist <= encWallOuterPx && !boardMask[idx] && !pegMask[idx] {
					isWallPx = true
					if dist <= trayWallOuterPx {
						isInnerWall = true
					} else if dist <= trayWallOuterPx+snapDepthPx {
						isSnapGroove = true
					}
				}
			}

			if isWallPx {
				if runStartX == -1 {
					runStartX = x
					curIsInner = isInnerWall
					curIsSnap = isSnapGroove
				} else if isInnerWall != curIsInner || isSnapGroove != curIsSnap {
					// boundary between inner, outer, and snap groove
					bx := float64(runStartX) * pixelToMM
					by := float64(y) * pixelToMM
					bw := float64(x-runStartX) * pixelToMM
					bh := pixelToMM

					isSpringRelief := false
					if !curIsInner && bx >= float64(minBX)*pixelToMM-clearance-wt-1.0 && bx <= float64(maxBX)*pixelToMM+clearance+wt+1.0 {
						// Check if the current pixel run constitutes either the left or right clip relief
						pryWMM := 8.0
						by_center := float64(int(boardCenterY/float64(boardCount))) * pixelToMM

						leftClipX := float64(minBX)*pixelToMM - clearance - wt
						rightClipX := float64(maxBX)*pixelToMM + clearance + wt

						if by >= by_center-pryWMM/2.0-0.5 && by <= by_center+pryWMM/2.0+0.5 {
							if math.Abs(bx-leftClipX) <= 1.5 || math.Abs(bx-rightClipX) <= 1.5 {
								isSpringRelief = true
							}
						}
					}

					if curIsInner {
						addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight, bw, bh, totalH-(trayFloor+snapHeight))
					} else {
						if isSpringRelief {
							// For relief wall cut, omit the bottom solid wall material from the tray floor
							addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight+1.0, bw, bh, totalH-(trayFloor+snapHeight+1.0))
						} else if curIsSnap {
							// Snap groove: remove material from (trayFloor+snapHeight-0.7) to (trayFloor+snapHeight-0.1)
							addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, snapHeight-0.7)
							addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight-0.1, bw, bh, totalH-(trayFloor+snapHeight-0.1))
						} else {
							// Outer wall
							addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, totalH-trayFloor)
						}
					}

					runStartX = x
					curIsInner = isInnerWall
					curIsSnap = isSnapGroove
				}
			} else {
				if runStartX != -1 {
					bx := float64(runStartX) * pixelToMM
					by := float64(y) * pixelToMM
					bw := float64(x-runStartX) * pixelToMM
					bh := pixelToMM

					isSpringRelief := false
					if !curIsInner && bx >= float64(minBX)*pixelToMM-clearance-wt-1.0 && bx <= float64(maxBX)*pixelToMM+clearance+wt+1.0 {
						pryWMM := 8.0
						by_center := float64(int(boardCenterY/float64(boardCount))) * pixelToMM

						leftClipX := float64(minBX)*pixelToMM - clearance - wt
						rightClipX := float64(maxBX)*pixelToMM + clearance + wt

						if by >= by_center-pryWMM/2.0-0.5 && by <= by_center+pryWMM/2.0+0.5 {
							if math.Abs(bx-leftClipX) <= 1.5 || math.Abs(bx-rightClipX) <= 1.5 {
								isSpringRelief = true
							}
						}
					}

					if curIsInner {
						addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight, bw, bh, totalH-(trayFloor+snapHeight))
					} else {
						if isSpringRelief {
							// For relief wall cut, omit the bottom solid wall material from the tray floor
							addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight+1.0, bw, bh, totalH-(trayFloor+snapHeight+1.0))
						} else if curIsSnap {
							addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, snapHeight-0.7)
							addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight-0.1, bw, bh, totalH-(trayFloor+snapHeight-0.1))
						} else {
							addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, totalH-trayFloor)
						}
					}
					runStartX = -1
				}
			}
		}
	}

	// Now subtract side cutout regions from the walls
	// For each cutout, we remove wall material in the Z range [cutout.Y, cutout.Y+cutout.H]
	// by NOT generating boxes in that region. Since we already generated full-height walls,
	// we rebuild wall columns where cutouts exist with gaps.
	if len(sideCutouts) > 0 {
		var cutoutEncTris [][3]Point
		for y := 0; y < imgH; y++ {
			runStart := -1
			for x := 0; x <= imgW; x++ {
				isCutWall := false
				if x < imgW {
					idx := y*imgW + x
					isCutWall = wallCutoutMask[idx]
				}

				if isCutWall {
					if runStart == -1 {
						runStart = x
					}
				} else {
					if runStart != -1 {
						// This run of wall pixels has cutouts — find which cutout
						midX := (runStart + x) / 2
						midIdx := y*imgW + midX
						_ = midIdx

						// Find the dominant side and cutout for this run
						bx := float64(midX)*pixelToMM + cfg.OutlineBounds.MinX
						by := cfg.OutlineBounds.MaxY - float64(y)*pixelToMM

						sideNum := -1
						minDist := math.MaxFloat64
						var bestPosAlongSide float64
						for _, bs := range boardSides {
							dx := bs.EndX - bs.StartX
							dy := bs.EndY - bs.StartY
							lenSq := dx*dx + dy*dy
							if lenSq == 0 {
								continue
							}
							t := ((bx-bs.StartX)*dx + (by-bs.StartY)*dy) / lenSq
							tClamp := math.Max(0, math.Min(1, t))
							projX := bs.StartX + tClamp*dx
							projY := bs.StartY + tClamp*dy
							dist := math.Sqrt((bx-projX)*(bx-projX) + (by-projY)*(by-projY))
							if dist < minDist {
								minDist = dist
								sideNum = bs.Num
								bestPosAlongSide = t * bs.Length
							}
						}

						bx2 := float64(runStart) * pixelToMM
						by2 := float64(y) * pixelToMM
						bw := float64(x-runStart) * pixelToMM
						bh := pixelToMM

						// Find the matching cutout for this side
						for _, c := range sideCutouts {
							if c.Side != sideNum {
								continue
							}

							minZ, maxZ := cutoutZBounds(c, bestPosAlongSide)
							minZ += trayFloor + pcbT
							maxZ += trayFloor + pcbT

							// Wall below cutout: from trayFloor to minZ (preserve enclosure floor)
							if minZ > trayFloor+0.3 {
								addBoxAtZ(&cutoutEncTris, bx2, by2, trayFloor, bw, bh, minZ-trayFloor)
							}
							// Wall above cutout: from maxZ to totalH
							if maxZ < totalH-0.05 {
								addBoxAtZ(&cutoutEncTris, bx2, by2, maxZ, bw, bh, totalH-maxZ)
							}
							break
						}
						runStart = -1
					}
				}
			}
		}
		// Replace full-height walls with cutout walls
		// First remove the original full-height boxes for cutout pixels
		// (They were already added above, so we need to rebuild)
		// Simpler approach: rebuild encTris without cutout regions, then add partial walls
		var newEncTris [][3]Point
		// Re-generate walls, skipping cutout pixels
		for y := 0; y < imgH; y++ {
			runStart := -1
			for x := 0; x <= imgW; x++ {
				isWallPixel := false
				if x < imgW {
					idx := y*imgW + x
					isWallPixel = wallMask[idx] > clearanceDistPx && wallMask[idx] <= encWallOuterPx && !boardMask[idx] && !wallCutoutMask[idx]
				}

				if isWallPixel {
					if runStart == -1 {
						runStart = x
					}
				} else {
					if runStart != -1 {
						bx := float64(runStart) * pixelToMM
						by2 := float64(y) * pixelToMM
						bw := float64(x-runStart) * pixelToMM
						bh := pixelToMM
						addBoxAtZ(&newEncTris, bx, by2, trayFloor, bw, bh, totalH-trayFloor)
						runStart = -1
					}
				}
			}
		}
		// Add the partial (cut) wall sections
		newEncTris = append(newEncTris, cutoutEncTris...)
		encTris = newEncTris
	}

	// Note: We handled pry slots by cropping the wallMask before running the generation.

	// Lid: cover the entire enclosure footprint at the top
	// Lid pixels = any pixel in wallMask OR clearanceMask OR boardMask
	// Subtract courtyard regions (component footprints) from the lid
	fmt.Println("Generating lid...")

	// Build courtyard cutout mask using flood-fill
	courtyardMask := buildCutoutMask(courtyardImg, imgW, imgH, true) // flood-fill closed outlines
	if courtyardImg != nil {
		cutoutCount := 0
		for _, v := range courtyardMask {
			if v {
				cutoutCount++
			}
		}
		fmt.Printf("Courtyard cutout (flood-filled): %d pixels\n", cutoutCount)
	}

	// Build soldermask cutout mask (direct pixel match, no flood-fill)
	soldermaskMask := buildCutoutMask(soldermaskImg, imgW, imgH, false)
	if soldermaskImg != nil {
		cutoutCount := 0
		for _, v := range soldermaskMask {
			if v {
				cutoutCount++
			}
		}
		fmt.Printf("Soldermask cutout: %d pixels\n", cutoutCount)
	}

	// Combined cutout: union of courtyard (filled) and soldermask
	combinedCutout := make([]bool, size)
	for i := 0; i < size; i++ {
		combinedCutout[i] = courtyardMask[i] || soldermaskMask[i]
	}

	for y := 0; y < imgH; y++ {
		runStart := -1
		for x := 0; x <= imgW; x++ {
			isLidPixel := false
			if x < imgW {
				idx := y*imgW + x
				inFootprint := (wallMask[idx] >= 0 && wallMask[idx] <= encWallOuterPx) || boardMask[idx]
				// Cut lid where combined cutout exists inside the board area
				isCutout := combinedCutout[idx] && boardMask[idx]
				isLidPixel = inFootprint && !isCutout
			}

			if isLidPixel {
				if runStart == -1 {
					runStart = x
				}
			} else {
				if runStart != -1 {
					bx := float64(runStart) * pixelToMM
					by := float64(y) * pixelToMM
					bw := float64(x-runStart) * pixelToMM
					bh := pixelToMM
					addBoxAtZ(&encTris, bx, by, totalH-lidThick, bw, bh, lidThick)
					runStart = -1
				}
			}
		}
	}
	// (Peg calculations moved above)

	// ==========================================
	// TRAY (bottom — conforms to board shape)
	// ==========================================
	fmt.Println("Generating edge-cut conforming tray...")

	for y := 0; y < imgH; y++ {
		runStartX := -1
		curIsWall := false
		for x := 0; x <= imgW; x++ {
			isTrayFloor := false
			isTrayWall := false
			if x < imgW {
				idx := y*imgW + x
				if !pegMask[idx] {
					dist := wallMask[idx]
					// Tray Floor covers everything up to encWallOuterPx
					if (dist >= 0 && dist <= encWallOuterPx) || boardMask[idx] {
						isTrayFloor = true
					}
					// Tray Wall goes from clearance to trayWallOuterPx
					if dist > clearanceDistPx && dist <= trayWallOuterPx && !boardMask[idx] {
						isTrayWall = true
					}
				}
			}

			if isTrayFloor {
				if runStartX == -1 {
					runStartX = x
					curIsWall = isTrayWall
				} else if isTrayWall != curIsWall {
					bx := float64(runStartX) * pixelToMM
					by := float64(y) * pixelToMM
					bw := float64(x-runStartX) * pixelToMM
					bh := pixelToMM

					addBoxAtZ(&trayTris, bx, by, 0, bw, bh, trayFloor)

					wallBase := trayFloor
					wallH := snapHeight

					// Evaluate cutout limits if this pixel run falls into a cutout mask
					isCutout := false
					for testX := runStartX; testX < x; testX++ {
						if wallCutoutMask[y*imgW+testX] {
							isCutout = true
							break
						}
					}

					if isCutout && len(sideCutouts) > 0 {
						midX := (runStartX + x) / 2
						bxMid := float64(midX)*pixelToMM + cfg.OutlineBounds.MinX
						byMid := cfg.OutlineBounds.MaxY - float64(y)*pixelToMM

						sideNum := -1
						minDist := math.MaxFloat64
						var bestPosAlongSide float64
						for _, bs := range boardSides {
							dx := bs.EndX - bs.StartX
							dy := bs.EndY - bs.StartY
							lenSq := dx*dx + dy*dy
							if lenSq == 0 {
								continue
							}
							t := ((bxMid-bs.StartX)*dx + (byMid-bs.StartY)*dy) / lenSq
							tClamp := math.Max(0, math.Min(1, t))
							projX := bs.StartX + tClamp*dx
							projY := bs.StartY + tClamp*dy
							dist := math.Sqrt((bxMid-projX)*(bxMid-projX) + (byMid-projY)*(byMid-projY))
							if dist < minDist {
								minDist = dist
								sideNum = bs.Num
								bestPosAlongSide = t * bs.Length
							}
						}

						for _, c := range sideCutouts {
							if c.Side == sideNum {
								minZ, _ := cutoutZBounds(c, bestPosAlongSide)
								minZ += trayFloor + pcbT

								// Tray wall goes up to trayFloor + snapHeight. If minZ is lower, truncate it.
								if minZ < trayFloor+wallH {
									wallH = minZ - trayFloor
									if wallH < 0 {
										wallH = 0
									}
								}
								break
							}
						}
					}

					if curIsWall && wallH > 0.05 {
						addBoxAtZ(&trayTris, bx, by, wallBase, bw, bh, wallH)
					}

					runStartX = x
					curIsWall = isTrayWall
				}
			} else {
				if runStartX != -1 {
					bx := float64(runStartX) * pixelToMM
					by := float64(y) * pixelToMM
					bw := float64(x-runStartX) * pixelToMM
					bh := pixelToMM

					addBoxAtZ(&trayTris, bx, by, 0, bw, bh, trayFloor)

					wallBase := trayFloor
					wallH := snapHeight

					// Evaluate cutout limits if this pixel run falls into a cutout mask
					isCutout := false
					for testX := runStartX; testX < x; testX++ {
						if wallCutoutMask[y*imgW+testX] {
							isCutout = true
							break
						}
					}

					if isCutout && len(sideCutouts) > 0 {
						midX := (runStartX + x) / 2
						bxMid := float64(midX)*pixelToMM + cfg.OutlineBounds.MinX
						byMid := cfg.OutlineBounds.MaxY - float64(y)*pixelToMM

						sideNum := -1
						minDist := math.MaxFloat64
						var bestPosAlongSide float64
						for _, bs := range boardSides {
							dx := bs.EndX - bs.StartX
							dy := bs.EndY - bs.StartY
							lenSq := dx*dx + dy*dy
							if lenSq == 0 {
								continue
							}
							t := ((bxMid-bs.StartX)*dx + (byMid-bs.StartY)*dy) / lenSq
							tClamp := math.Max(0, math.Min(1, t))
							projX := bs.StartX + tClamp*dx
							projY := bs.StartY + tClamp*dy
							dist := math.Sqrt((bxMid-projX)*(bxMid-projX) + (byMid-projY)*(byMid-projY))
							if dist < minDist {
								minDist = dist
								sideNum = bs.Num
								bestPosAlongSide = t * bs.Length
							}
						}

						for _, c := range sideCutouts {
							if c.Side == sideNum {
								minZ, _ := cutoutZBounds(c, bestPosAlongSide)
								minZ += trayFloor + pcbT

								if minZ < trayFloor+wallH {
									wallH = minZ - trayFloor
									if wallH < 0 {
										wallH = 0
									}
								}
								break
							}
						}
					}

					if curIsWall && wallH > 0.05 {
						addBoxAtZ(&trayTris, bx, by, wallBase, bw, bh, wallH)
					}
					runStartX = -1
				}
			}
		}
	}

	// Add Pry Clips to the Tray to sit under the Enclosure Pry Slots
	if boardCount > 0 {
		pryWMM := 8.0
		pryDMM := 1.0
		clipH := 0.8

		leftX := float64(minBX)*pixelToMM - clearance - wt
		rightX := float64(maxBX)*pixelToMM + clearance + wt
		by_center := float64(int(boardCenterY/float64(boardCount))) * pixelToMM

		// Z coordinates: trayFloor + snapHeight - clipH ensures the clip finishes flush with the top of the tray wall
		addBoxAtZ(&trayTris, leftX-pryDMM, by_center-pryWMM/2.0, trayFloor+snapHeight-clipH, pryDMM, pryWMM, clipH)
		addBoxAtZ(&trayTris, rightX, by_center-pryWMM/2.0, trayFloor+snapHeight-clipH, pryDMM, pryWMM, clipH)
	}

	_ = math.Pi // keep math import for Phase 2 cylindrical pegs

	// Shift meshes to origin so the exported STL is centered
	offsetX := float64(imgW) * pixelToMM / 2.0
	offsetY := float64(imgH) * pixelToMM / 2.0

	for i := range encTris {
		for j := 0; j < 3; j++ {
			encTris[i][j].X -= offsetX
			encTris[i][j].Y -= offsetY
		}
	}
	for i := range trayTris {
		for j := 0; j < 3; j++ {
			trayTris[i][j].X -= offsetX
			trayTris[i][j].Y -= offsetY
		}
	}

	return &EnclosureResult{
		EnclosureTriangles: encTris,
		TrayTriangles:      trayTris,
	}
}

// addBoxAtZ creates a box at a specific Z offset
func addBoxAtZ(triangles *[][3]Point, x, y, z, w, h, zHeight float64) {
	x0, y0 := x, y
	x1, y1 := x+w, y+h
	z0, z1 := z, z+zHeight

	p000 := Point{x0, y0, z0}
	p100 := Point{x1, y0, z0}
	p110 := Point{x1, y1, z0}
	p010 := Point{x0, y1, z0}
	p001 := Point{x0, y0, z1}
	p101 := Point{x1, y0, z1}
	p111 := Point{x1, y1, z1}
	p011 := Point{x0, y1, z1}

	addQuad := func(a, b, c, d Point) {
		*triangles = append(*triangles, [3]Point{a, b, c})
		*triangles = append(*triangles, [3]Point{c, d, a})
	}

	addQuad(p000, p010, p110, p100) // Bottom
	addQuad(p101, p111, p011, p001) // Top
	addQuad(p000, p100, p101, p001) // Front
	addQuad(p100, p110, p111, p101) // Right
	addQuad(p110, p010, p011, p111) // Back
	addQuad(p010, p000, p001, p011) // Left
}

// buildCutoutMask creates a boolean mask from an image.
// If floodFill is true, it flood-fills from the edges to find closed regions.
func buildCutoutMask(img image.Image, w, h int, floodFill bool) []bool {
	size := w * h
	mask := make([]bool, size)

	if img == nil {
		return mask
	}

	// First: build raw pixel mask from the image
	bounds := img.Bounds()
	rawPixels := make([]bool, size)
	for y := 0; y < h && y < bounds.Max.Y; y++ {
		for x := 0; x < w && x < bounds.Max.X; x++ {
			r, g, b, _ := img.At(x+bounds.Min.X, y+bounds.Min.Y).RGBA()
			gray := color.GrayModel.Convert(color.NRGBA{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8), 255}).(color.Gray)
			if gray.Y > 128 {
				rawPixels[y*w+x] = true
			}
		}
	}

	if !floodFill {
		// Direct mode: raw pixels are the mask
		return rawPixels
	}

	// Flood-fill mode: fill from edges to find exterior, invert to get interiors
	// Exterior = everything reachable from edges without crossing a white pixel
	exterior := floodFillExterior(rawPixels, w, h)

	// Interior = NOT exterior AND NOT raw pixel (the outline itself)
	// Actually, interior = NOT exterior (includes both outline pixels and filled regions)
	for i := 0; i < size; i++ {
		mask[i] = !exterior[i]
	}

	return mask
}

// floodFillExterior marks all pixels reachable from the image edges
// without crossing a white (true) pixel as exterior
func floodFillExterior(pixels []bool, w, h int) []bool {
	size := w * h
	exterior := make([]bool, size)

	// BFS queue starting from all edge pixels that are not white
	queue := make([]int, 0, w*2+h*2)

	for x := 0; x < w; x++ {
		// Top edge
		if !pixels[x] {
			exterior[x] = true
			queue = append(queue, x)
		}
		// Bottom edge
		idx := (h-1)*w + x
		if !pixels[idx] {
			exterior[idx] = true
			queue = append(queue, idx)
		}
	}
	for y := 0; y < h; y++ {
		// Left edge
		idx := y * w
		if !pixels[idx] {
			exterior[idx] = true
			queue = append(queue, idx)
		}
		// Right edge
		idx = y*w + (w - 1)
		if !pixels[idx] {
			exterior[idx] = true
			queue = append(queue, idx)
		}
	}

	// BFS
	for len(queue) > 0 {
		cur := queue[0]
		queue = queue[1:]

		x := cur % w
		y := cur / w

		for _, d := range [][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} {
			nx, ny := x+d[0], y+d[1]
			if nx >= 0 && nx < w && ny >= 0 && ny < h {
				ni := ny*w + nx
				if !exterior[ni] && !pixels[ni] {
					exterior[ni] = true
					queue = append(queue, ni)
				}
			}
		}
	}

	return exterior
}

// cutoutZBounds calculates the accurate Z bounds taking into account corner radii
func cutoutZBounds(c SideCutout, posAlongSide float64) (float64, float64) {
	minZ := c.Y
	maxZ := c.Y + c.Height

	if c.CornerRadius > 0 {
		r := c.CornerRadius
		localX := posAlongSide - c.X

		if localX < r {
			dx := r - localX
			dy := r - math.Sqrt(math.Max(0, r*r-dx*dx))
			minZ += dy
			maxZ -= dy
		} else if localX > c.Width-r {
			dx := localX - (c.Width - r)
			dy := r - math.Sqrt(math.Max(0, r*r-dx*dx))
			minZ += dy
			maxZ -= dy
		}
	}
	return minZ, maxZ
}
