"""T-Cobbler GPIO mounting plate — builds plate + labels (multi-color).
Faster than OpenSCAD because we use trimesh + manifold for booleans
and font-based glyph extrusion via shapely + matplotlib."""

import trimesh
import numpy as np
from shapely.geometry import box, Polygon
from shapely.affinity import translate as sh_translate, scale as sh_scale
from shapely.ops import unary_union
import matplotlib.font_manager as fm
from matplotlib.textpath import TextPath
from pathlib import Path

OUT = Path("/home/johnbarley/files")

# ── DIMENSIONS ──
PLATE_W      = 55
PLATE_L      = 70
PLATE_THK    = 3
PIN_AREA_W   = 20    # gap between two pin rows (X)
PIN_AREA_L   = 55    # along pin rows (Y) — 20 pins × 2.54mm + margin
PIN_PITCH    = 2.54
PIN_SLOT_W   = 3
HOLE_DIA     = 3.2
HOLE_INSET   = 4
LABEL_EMBOSS = 1.2          # raised relief
LABEL_DIG_IN = 0.3          # dive into plate (bonded)
LABEL_FONT_SIZE = 2.0       # height
LABEL_STROKE_BOOST = 0.17   # thicker strokes but holes (P, O, D, etc.) stay open

ODD_PINS = [
    (1,"3.3V"),(3,"GPIO2"),(5,"GPIO3"),(7,"GPIO4"),(9,"GND"),
    (11,"GPIO17"),(13,"GPIO27"),(15,"GPIO22"),(17,"3.3V"),(19,"MOSI"),
    (21,"MISO"),(23,"SCLK"),(25,"GND"),(27,"ID_SD"),(29,"GPIO5"),
    (31,"GPIO6"),(33,"GPIO13"),(35,"GPIO19"),(37,"GPIO26"),(39,"GND"),
]
EVEN_PINS = [
    (2,"5V"),(4,"5V"),(6,"GND"),(8,"TXD"),(10,"RXD"),
    (12,"GPIO18"),(14,"GND"),(16,"GPIO23"),(18,"GPIO24"),(20,"GND"),
    (22,"GPIO25"),(24,"CE0"),(26,"CE1"),(28,"ID_SC"),(30,"GND"),
    (32,"GPIO12"),(34,"GND"),(36,"GPIO16"),(38,"GPIO20"),(40,"GPIO21"),
]

# ── PLATE BODY ──
print("Building plate body...")
def rounded_rect_2d(w, l, r):
    return box(-w/2 + r, -l/2 + r, w/2 - r, l/2 - r).buffer(r, resolution=8)

plate_2d = rounded_rect_2d(PLATE_W, PLATE_L, 3)
plate_3d = trimesh.creation.extrude_polygon(plate_2d, height=PLATE_THK)

# Single rectangular cutout — exactly 20×52mm (matches pin area only)
CUTOUT_W = 20    # exact pin area width
CUTOUT_L = 52    # exact pin area length
cutout = trimesh.creation.box(extents=[CUTOUT_W, CUTOUT_L, PLATE_THK + 2])
cutout.apply_translation([0, 0, PLATE_THK/2])

# Mount holes (diagonal)
hole1 = trimesh.creation.cylinder(radius=HOLE_DIA/2, height=PLATE_THK + 2, sections=24)
hole1.apply_translation([-PLATE_W/2 + HOLE_INSET,  PLATE_L/2 - HOLE_INSET, PLATE_THK/2])
hole2 = trimesh.creation.cylinder(radius=HOLE_DIA/2, height=PLATE_THK + 2, sections=24)
hole2.apply_translation([ PLATE_W/2 - HOLE_INSET, -PLATE_L/2 + HOLE_INSET, PLATE_THK/2])

plate = trimesh.boolean.difference([plate_3d, cutout, hole1, hole2], engine='manifold')
print(f"  plate: V={len(plate.vertices)} watertight={plate.is_watertight}")

# ── LABELS (text → 2D shapes → extrude) ──
print("Building labels...")
font_path = fm.findfont(fm.FontProperties(family='DejaVu Sans', weight='bold'))

def text_to_shapely(s, size_mm):
    """Convert text to a (Multi)Polygon, properly handling character holes."""
    tp = TextPath((0, 0), s, size=size_mm,
                  prop=fm.FontProperties(fname=font_path, weight='bold'))
    # Use matplotlib's to_polygons which gives clean linearized polygons
    # closed_only=True returns only closed paths (good for text)
    polygons_pts = tp.to_polygons(closed_only=True)
    from shapely.geometry import Polygon as ShPoly, MultiPolygon
    # Each result is a Nx2 array of points forming a closed contour.
    # Letters with holes (O, P, D...) have multiple contours: outer CCW, inner CW.
    # Build shapely polygons and use difference for holes.
    shells = []
    for pts in polygons_pts:
        if len(pts) < 3:
            continue
        try:
            p = ShPoly(pts)
            if p.is_valid and p.area > 0.001:
                shells.append(p)
        except Exception:
            pass
    if not shells:
        return None
    # Symmetric difference handles letter holes (O, P, etc.)
    result = shells[0]
    for s2 in shells[1:]:
        try:
            result = result.symmetric_difference(s2)
        except Exception:
            result = unary_union([result, s2])
    # Dilate strokes to make letters bolder (visual + better print reliability)
    if LABEL_STROKE_BOOST > 0:
        try:
            result = result.buffer(LABEL_STROKE_BOOST, resolution=4, join_style=1)
        except Exception:
            pass
    return result


def text_mesh(s, size_mm, x, y, anchor='left'):
    poly = text_to_shapely(s, size_mm)
    if poly is None or poly.is_empty:
        return None
    minx, miny, maxx, maxy = poly.bounds
    if anchor == 'right':
        dx = x - maxx
    elif anchor == 'center':
        dx = x - (minx + maxx) / 2
    else:
        dx = x - minx
    dy = y - (miny + maxy) / 2
    poly = sh_translate(poly, dx, dy)

    # Handle MultiPolygon: extrude each member separately and concat
    from shapely.geometry import MultiPolygon, Polygon as ShPoly
    pieces = poly.geoms if isinstance(poly, MultiPolygon) else [poly]
    meshes = []
    for piece in pieces:
        if not isinstance(piece, ShPoly) or piece.is_empty or piece.area < 0.001:
            continue
        try:
            meshes.append(trimesh.creation.extrude_polygon(piece, height=LABEL_EMBOSS))
        except Exception as e:
            print(f"  extrude fail '{s}' part: {e}")
    if not meshes:
        return None
    return trimesh.util.concatenate(meshes)

label_meshes = []
pin_y_top = PIN_AREA_L/2 - 1  # top pin Y
for i, (num, name) in enumerate(ODD_PINS):
    y = pin_y_top - i * PIN_PITCH
    # Number close to slot, right-aligned at x = -PIN_AREA_W/2 - 1
    m = text_mesh(str(num), LABEL_FONT_SIZE, -PIN_AREA_W/2 - 1.0, y, anchor='right')
    if m: label_meshes.append(m)
    # Name further out
    m = text_mesh(name, LABEL_FONT_SIZE, -PIN_AREA_W/2 - 4.0, y, anchor='right')
    if m: label_meshes.append(m)

for i, (num, name) in enumerate(EVEN_PINS):
    y = pin_y_top - i * PIN_PITCH
    m = text_mesh(str(num), LABEL_FONT_SIZE, PIN_AREA_W/2 + 1.0, y, anchor='left')
    if m: label_meshes.append(m)
    m = text_mesh(name, LABEL_FONT_SIZE, PIN_AREA_W/2 + 4.0, y, anchor='left')
    if m: label_meshes.append(m)

print(f"  label glyphs: {len(label_meshes)}")
labels = trimesh.util.concatenate(label_meshes)
# Dive into plate by LABEL_DIG_IN so labels are bonded with plate (not floating).
# Net visible relief = LABEL_EMBOSS - LABEL_DIG_IN = 0.9mm above plate top.
labels.apply_translation([0, 0, PLATE_THK - LABEL_DIG_IN])
print(f"  labels combined: V={len(labels.vertices)} F={len(labels.faces)}")

# ── EXPORT ──
print("Exporting...")
plate.export(OUT / "tcobbler-plate-v2-plate.stl")
labels.export(OUT / "tcobbler-plate-v2-labels.stl")

# Combined for single-color users
combined = trimesh.util.concatenate([plate, labels])
combined.export(OUT / "tcobbler-plate-v2-combined.stl")
combined.export(OUT / "tcobbler-plate-v2-combined.3mf")

# Multi-color 3MF (scene with 2 parts)
scene = trimesh.Scene([plate, labels])
scene.export(OUT / "tcobbler-plate-v2-multicolor.3mf")

print(f"\n=== SAVED ===")
for n in ["plate", "labels", "combined"]:
    p = OUT / f"tcobbler-plate-v2-{n}.stl"
    sz = p.stat().st_size
    print(f"  {p.name}: {sz:,} bytes")
print(f"  tcobbler-plate-v2-combined.3mf")
print(f"  tcobbler-plate-v2-multicolor.3mf (multi-part)")
