"""
Coral Eye - a privacy-first AI camera enclosure for the Synaptics Coralboard SL.

Parametric source (build123d 0.8). All coordinates are in the board frame of the
official Coralboard SL reference STEP (Printables @Google_Coral, Apache-2.0):

    x  -> along the board, left to right (USB-C on the right)
    y  -> from the front edge (USB / buttons) to the back edge (GPIO header)
    z  -> out of the component side; the PCB top surface is z = 0

The device stands with the board vertical: board y is "up", board z is "towards
the viewer". The camera module (the kit's Arducam OV5647, UC-346 board) sits in
front of the SoM. Its flex cable (the ~55 mm one from the kit) leaves the camera's
bottom edge, U-turns and runs behind the camera into the CSI socket - the same
single fold as in Google's own sensor-HAT assembly.
"""
from build123d import *
import math
import os

HERE = os.path.dirname(os.path.abspath(__file__))
OUT_DIR = os.path.normpath(os.path.join(HERE, "..", "out"))

# --------------------------------------------------------------------------
# Board reference (measured from Synaptics_CoralBoard_SL.step)
# --------------------------------------------------------------------------
PCB_X0, PCB_X1 = -3.80, 69.20
PCB_Y0, PCB_Y1 = -2.025, 55.975
PCB_T = 1.618
HOLES = [(-0.3, 4.47), (-0.3, 52.48), (64.7, 4.47), (64.7, 52.48)]  # M3, d3.2

# Connectors that face the walls (centre / extents taken from the STEP)
USER_BTN_X, RESET_BTN_X, BTN_Z = 6.70, 42.92, 0.95   # side-push buttons, stems end at y = -3.0
BTN_STEM_Y = -3.0
LEDS = [(36.25, 0.0), (38.13, 0.0), (47.63, 0.0)]     # 0603 top-view LEDs, top at z = 0.9

# --------------------------------------------------------------------------
# Enclosure envelope
# --------------------------------------------------------------------------
WALL = 2.0
CAV_X0, CAV_X1 = -4.3, 69.7          # 0.5 mm around the PCB
CAV_Y0, CAV_Y1 = -3.6, 56.5          # USB shields stick out to y = -3.25
OUT_X0, OUT_X1 = CAV_X0 - WALL, CAV_X1 + WALL
OUT_Y0, OUT_Y1 = CAV_Y0 - WALL, CAV_Y1 + WALL
R_OUT = 6.0
R_IN = R_OUT - WALL

Z_BACK = -6.2       # rear outer surface (bed side of the body)
Z_FLOOR = -4.2      # inner floor: clears bumpers (-3.78), DSI socket (-3.50), pins (-3.34)
Z_SPLIT = 9.2       # face inner surface: clears header pins (8.59)
Z_RIM = Z_SPLIT - 0.2   # body rim stays 0.2 mm short: the pillars, not the rim, set the closure
FACE_T = 4.0
Z_FRONT = Z_SPLIT + FACE_T

CX, CY = (OUT_X0 + OUT_X1) / 2, (OUT_Y0 + OUT_Y1) / 2
W_OUT, H_OUT = OUT_X1 - OUT_X0, OUT_Y1 - OUT_Y0
CAV_CX, CAV_CY = (CAV_X0 + CAV_X1) / 2, (CAV_Y0 + CAV_Y1) / 2
W_CAV, H_CAV = CAV_X1 - CAV_X0, CAV_Y1 - CAV_Y0

# --------------------------------------------------------------------------
# Camera module (Arducam OV5647 "UC-346", Raspberry Pi camera v1.3 footprint)
# --------------------------------------------------------------------------
CAM_W, CAM_H, CAM_T = 25.0, 24.0, 1.0
CAM_X0 = 32.9                   # left edge; lens column lines up with the CSI socket
CAM_Y_TOP = 34.5                # edge opposite the flex connector (just below mikroBUS)
CAM_Z_FRONT = 5.8               # PCB front face; the back sits 2.2 mm above the SoM
CAM_HOLES_UV = [(2.0, 2.0), (23.0, 2.0), (2.0, 14.5), (23.0, 14.5)]   # from top-left, v down
CAM_PILOT_D = 1.45              # kit's M1.6 screws, or M2 x 4-5 self-tapping
LENS_UV = (12.5, 14.2)          # lens centre: Raspberry Pi camera drawing and Google's kit photo agree
LENS_BLOCK = 8.6                # square lens holder
LENS_BLOCK_H = 5.0
LENS_POCKET = (12.4, 16.0)      # generous: +-1.9 mm in x, +-3.7 mm in y around the estimate
LENS_HEADROOM = 0.8


def cam_xy(u, v):
    return CAM_X0 + u, CAM_Y_TOP - v


LENS_X, LENS_Y = cam_xy(*LENS_UV)
EYE_D = 11.6

# Lens: the twist shutter module (shutter.py) is screwed to the face with two M2 screws
# at +-LENS_SCREW_DX; the face only carries their blind pilot holes.
LENS_SCREW_DX = 13.0               # 2.5 mm from the camera posts: no clash with any camera screw
LENS_SCREW_PILOT = (1.6, 3.8)    # diameter, depth from the front (M2 x 6 reaches 3.4)
LENS_MODULE_D = 38.0             # twist ring diameter: sets the wordmark's margins

# Lite option without moving parts: a flat ring glued onto the face. Both mating
# surfaces are first layers on the bed, so there is no seat, bridge or membrane;
# the privacy cap, pushed through the ring into the eye, centres it while the glue sets.
RING_D = 36.0
RING_PROUD = 1.8         # ring height above the face
RING_BORE_H = 0.5        # straight part of the bore, where the cap's ribs grip
WORDMARK = "CORAL EYE"
WORDMARK_SIZE = 6.0      # strokes >= 0.8 mm: they survive the first layer on a 0.4 mm nozzle

# --------------------------------------------------------------------------
# Rear mount rail (female dovetail in the back, open at the bottom edge)
# --------------------------------------------------------------------------
RAIL_CX = CX
RAIL_W_OPEN = 16.0      # width at the back surface
RAIL_DEPTH = 2.0
RAIL_W_IN = RAIL_W_OPEN + 2 * RAIL_DEPTH    # 45 deg undercut -> 20 mm bridged ceiling
RAIL_Y_STOP = 26.0
RAIL_CLEAR = 0.2        # per side, for the male tongues
RAIL_TOP_CLEAR = 0.7    # tongue top stays this far under the (bridged) ceiling, even if it sags a little
RAIL_LATCH_Y = 10.5     # detent groove: low enough that a latch finger fits on either side of it
                        # (root below for mounts printed upright, root above for mounts printed upside down)
LATCH_H = 1.3           # wedge height on the tongue (0.6 mm into the ceiling, 0.4 mm beyond the play)
LATCH_ENTRY = 45.0      # entry face angle (deg from the tongue surface)
LATCH_HOLD = 55.0       # retaining face angle: holds 5-12 N, yet a firm pull releases it
GROOVE_D = 1.0          # detent groove depth in the rail ceiling

# Port bay in the bottom wall (Qwiic + USB-A): open up to the rim, the face closes it,
# so a USB-A overmold up to ~10 mm thick fits (the receptacle's centre is at z = 3.84)
BAY_X0, BAY_X1, BAY_Z0 = 11.5, 34.0, -1.7

# USER button plunger (captured between the wall and the board's button stem)
PLUNGER_HOLE_D = 3.2
PLUNGER_D = 2.8
PLUNGER_FLANGE = (4.6, 0.45)     # diameter, thickness
PLUNGER_PROUD = 0.7


# ==========================================================================
# helpers
# ==========================================================================
def rrect_prism(cx, cy, w, h, r, z0, z1):
    """Rounded rectangle in XY, extruded between z0 and z1."""
    return Pos(cx, cy, z0) * extrude(RectangleRounded(w, h, r), amount=z1 - z0)


def cyl(x, y, z0, z1, d):
    return Pos(x, y, z0) * Cylinder(radius=d / 2, height=z1 - z0,
                                    align=(Align.CENTER, Align.CENTER, Align.MIN))


def _rr(w, h, r):
    r = min(r, w / 2 - 0.01, h / 2 - 0.01)
    return RectangleRounded(w, h, r) if r > 0.05 else Rectangle(w, h)


def cut_y(x0, x1, z0, z1, r, y_from, y_to):
    """Rounded-rectangle hole (in the XZ plane) running along y from y_from to y_to."""
    sk = Plane.XZ.offset(-y_from) * Pos((x0 + x1) / 2, (z0 + z1) / 2) * _rr(x1 - x0, z1 - z0, r)
    return extrude(sk, amount=y_from - y_to)


def cut_x(y0, y1, z0, z1, r, x_from, x_to):
    """Rounded-rectangle hole (in the YZ plane) running along x from x_from to x_to."""
    sk = Plane.YZ.offset(x_from) * Pos((y0 + y1) / 2, (z0 + z1) / 2) * _rr(y1 - y0, z1 - z0, r)
    return extrude(sk, amount=x_to - x_from)


def funnel_y(x0, x1, z0, z1, r, y_face, depth, grow):
    """45-degree lead-in around an opening on a wall whose outer face is at y = y_face (outside is -y)."""
    w, h = x1 - x0, z1 - z0
    outer = Plane.XZ.offset(-(y_face - 0.01)) * Pos((x0 + x1) / 2, (z0 + z1) / 2) * _rr(w + 2 * grow, h + 2 * grow, r + grow)
    inner = Plane.XZ.offset(-(y_face + depth)) * Pos((x0 + x1) / 2, (z0 + z1) / 2) * _rr(w, h, r)
    return loft([outer, inner])


def funnel_x(y0, y1, z0, z1, r, x_face, depth, grow):
    """45-degree lead-in around an opening on the right wall (outer face at x = x_face, outside is +x)."""
    w, h = y1 - y0, z1 - z0
    outer = Plane.YZ.offset(x_face + 0.01) * Pos((y0 + y1) / 2, (z0 + z1) / 2) * _rr(w + 2 * grow, h + 2 * grow, r + grow)
    inner = Plane.YZ.offset(x_face - depth) * Pos((y0 + y1) / 2, (z0 + z1) / 2) * _rr(w, h, r)
    return loft([outer, inner])


def teardrop_y(x, z, d, y_from, y_to):
    """Horizontal round hole along y with a 45-degree roof (towards +z), so it prints without sagging."""
    r = d / 2
    k = r / math.sqrt(2)
    hole = Pos(x, (y_from + y_to) / 2, z) * Rot(90, 0, 0) * Cylinder(radius=r, height=abs(y_from - y_to))
    roof = Plane.XZ.offset(-y_from) * Polygon((x - k, z + k), (x + k, z + k), (x, z + r * math.sqrt(2)), align=None)
    return hole + extrude(roof, amount=y_from - y_to)


def dovetail_prism(w_narrow, w_wide, depth, z_narrow, y0, y1, cx, toward_minus_z=False):
    """Trapezoid (narrow at z_narrow, wide at z_narrow +/- depth) extruded along y."""
    zw = z_narrow - depth if toward_minus_z else z_narrow + depth
    pts = [(cx - w_narrow / 2, z_narrow), (cx + w_narrow / 2, z_narrow),
           (cx + w_wide / 2, zw), (cx - w_wide / 2, zw)]
    sk = Plane.XZ.offset(-y0) * Polygon(*pts, align=None)
    return extrude(sk, amount=y0 - y1)


def ridge_x(xc, y_apex, z0, height, width, entry_deg, hold_deg, up=True):
    """Asymmetric ridge running along x: apex at y_apex, rising `height` from the plane z = z0.
    The +y face has the entry angle, the -y face the holding angle (both from the plane)."""
    s = 1 if up else -1
    pts = [(y_apex - height / math.tan(math.radians(hold_deg)), z0),
           (y_apex + height / math.tan(math.radians(entry_deg)), z0),
           (y_apex, z0 + s * height)]
    sk = Plane.YZ.offset(xc - width / 2) * Polygon(*pts, align=None)
    return extrude(sk, amount=width)


# ==========================================================================
# BODY (rear shell) - printed back-down, no supports
# ==========================================================================
def make_body():
    body = rrect_prism(CX, CY, W_OUT, H_OUT, R_OUT, Z_BACK, Z_RIM)
    body = chamfer(body.edges().group_by(Axis.Z)[0], length=1.0)    # back edge
    body = chamfer(body.edges().group_by(Axis.Z)[-1], length=0.5)   # rim: forms the seam groove
    body -= rrect_prism(CAV_CX, CAV_CY, W_CAV, H_CAV, R_IN, Z_FLOOR, Z_SPLIT + 1)

    # thicker floor under the mount rail, with a pocket under the one USB-A shield tab
    # that comes through the PCB above it (x 31.95-32.25, y 6.40-7.94, down to z -2.16)
    pad_w = RAIL_W_IN + 4.0
    pad_top = -2.45
    body += rrect_prism(RAIL_CX, (CAV_Y0 + RAIL_Y_STOP + 2.0) / 2, pad_w,
                        RAIL_Y_STOP + 2.0 - CAV_Y0, 1.0, Z_FLOOR - 0.01, pad_top)
    body -= rrect_prism(32.1, 7.17, 3.2, 3.6, 0.8, -3.1, pad_top + 0.5)

    # board standoffs: wide foot (below the rubber bumpers) + column up to the PCB
    for i, (hx, hy) in enumerate(HOLES):
        col_d = 5.4 if i == 1 else 6.0     # top-left one clears the bumper next to it
        body += cyl(hx, hy, Z_FLOOR - 0.01, -3.95, 8.4)   # 8.4: merges into the wall, no tangency
        body += cyl(hx, hy, Z_FLOOR - 0.01, -PCB_T, col_d)
        body -= cyl(hx, hy, Z_BACK - 1, Z_SPLIT, 3.4)                  # M3 clearance
        # button-head counterbore, ending 0.2 mm inside the floor: printed back-down, the column
        # above then starts on solid floor instead of in mid-air (head sits 0.15 mm below the back)
        body -= cyl(hx, hy, Z_BACK - 1, Z_FLOOR - 0.2, 6.2)

    # ---- bottom wall (board front edge) ----
    yo, yi = OUT_Y0 - 1.0, CAV_Y0 + 1.0
    # one bay for Qwiic + USB-A, open up to the rim (no thin strip above it): the face closes it
    bay = (BAY_X0, BAY_X1, BAY_Z0, Z_RIM + 1.0, 1.5)
    body -= cut_y(bay[0], bay[1], bay[2], bay[3], bay[4], yo, yi)
    body -= funnel_y(bay[0], bay[1], bay[2], bay[3], bay[4], OUT_Y0, 0.8, 0.8)
    usbc = (49.28, 62.28, -2.48, 4.92, 1.5)                             # USB-C: admits rounded-rectangle overmolds
    body -= cut_y(usbc[0], usbc[1], usbc[2], usbc[3], usbc[4], yo, yi)
    body -= funnel_y(usbc[0], usbc[1], usbc[2], usbc[3], usbc[4], OUT_Y0, 0.6, 0.6)
    body -= Pos(RESET_BTN_X, 0, BTN_Z) * Rot(90, 0, 0) * Cylinder(radius=0.9, height=20)  # reset pinhole
    body -= Pos(RESET_BTN_X, OUT_Y0, BTN_Z) * Rot(-90, 0, 0) * Cone(bottom_radius=1.9, top_radius=0.9, height=1.0,
                                                                  align=(Align.CENTER, Align.CENTER, Align.MIN))
    for vx in (64.8,):                                                  # small vent right of USB-C
        body -= cut_y(vx - 0.8, vx + 0.8, -2.5, 5.0, 0.79, yo, yi)
    # USER button: hole for the captured printed plunger (teardrop: the roof doesn't sag onto the shaft)
    body -= teardrop_y(USER_BTN_X, BTN_Z, PLUNGER_HOLE_D, yi, yo)
    body -= Pos(USER_BTN_X, OUT_Y0, BTN_Z) * Rot(-90, 0, 0) * Cone(bottom_radius=PLUNGER_HOLE_D / 2 + 0.6,
                                                                 top_radius=PLUNGER_HOLE_D / 2, height=0.6,
                                                                 align=(Align.CENTER, Align.CENTER, Align.MIN))

    # ---- left wall: microSD slot + vertical finger scoop (keeps a 0.8 mm wall) ----
    xo, xi = OUT_X0 - 1.0, CAV_X0 + 1.0
    body -= cut_x(19.6, 31.6, -0.35, 2.05, 0.6, xo, xi)
    body -= cyl(OUT_X0 - 3.8, 25.6, -2.2, 3.9, 10.0)

    # ---- right wall: I2S2 (JST-SH 7) and battery (JST-PH 2), with lead-ins ----
    xo, xi = OUT_X1 + 1.0, CAV_X1 - 1.0
    i2s = (19.2, 28.8, -0.30, 3.30, 0.8)
    bat = (9.6, 16.1, -0.30, 6.50, 1.0)
    for o in (i2s, bat):
        body -= cut_x(o[0], o[1], o[2], o[3], o[4], xi, xo)
        body -= funnel_x(o[0], o[1], o[2], o[3], o[4], OUT_X1, 1.0, 1.0)

    # ---- top wall: chimney vents above the SoM / mikroBUS side ----
    for k in range(7):
        vx = 37.0 + 4.0 * k
        body -= cut_y(vx - 0.8, vx + 0.8, -3.0, 7.0, 0.79, OUT_Y1 + 1.0, CAV_Y1 - 1.0)

    # ---- rear mount rail (female dovetail, open at the bottom) + detent groove ----
    body -= dovetail_prism(RAIL_W_OPEN, RAIL_W_IN, RAIL_DEPTH, Z_BACK - 0.01,
                           OUT_Y0 - 1.0, RAIL_Y_STOP, RAIL_CX)
    body -= dovetail_prism(RAIL_W_OPEN + 2.0, RAIL_W_OPEN, 1.0, Z_BACK - 0.01,
                           OUT_Y0 - 1.0, OUT_Y0 + 1.0, RAIL_CX)        # lead-in chamfer
    body -= ridge_x(RAIL_CX, RAIL_LATCH_Y, Z_BACK + RAIL_DEPTH - 0.01, GROOVE_D, 6.6,
                    LATCH_ENTRY, LATCH_HOLD)

    # ---- engraved label on the back (reads correctly from behind) ----
    # (strokes >= 0.8 mm, so the first layer doesn't close them)
    lbl = Plane(origin=(CX, 47.0, Z_BACK), x_dir=(-1, 0, 0), z_dir=(0, 0, -1))
    body -= extrude(lbl * Text("CORAL EYE", font_size=9.0, font_style=FontStyle.BOLD), amount=-0.6)
    lbl2 = Plane(origin=(CX, 37.5, Z_BACK), x_dir=(-1, 0, 0), z_dir=(0, 0, -1))
    body -= extrude(lbl2 * Text("for Coralboard SL", font_size=5.5, font_style=FontStyle.BOLD), amount=-0.6)
    return body


def make_user_plunger():
    """Captured button plunger, in assembly position. Print it flange-down."""
    y_in = BTN_STEM_Y - 0.15                      # 0.15 mm short of the switch stem at rest
    y_out = OUT_Y0 - PLUNGER_PROUD
    shaft = Pos(USER_BTN_X, y_out, BTN_Z) * Rot(-90, 0, 0) * Cylinder(
        radius=PLUNGER_D / 2, height=y_in - y_out, align=(Align.CENTER, Align.CENTER, Align.MIN))
    flange = Pos(USER_BTN_X, y_in - PLUNGER_FLANGE[1], BTN_Z) * Rot(-90, 0, 0) * Cylinder(
        radius=PLUNGER_FLANGE[0] / 2, height=PLUNGER_FLANGE[1], align=(Align.CENTER, Align.CENTER, Align.MIN))
    p = shaft + flange
    return fillet(p.edges().group_by(Axis.Y)[0], radius=0.5)


# ==========================================================================
# FACE (front plate) - printed face-down, no supports
# ==========================================================================
def make_face(led_windows="skin"):
    face = rrect_prism(CX, CY, W_OUT, H_OUT, R_OUT, Z_SPLIT, Z_FRONT)
    face = chamfer(face.edges().group_by(Axis.Z)[-1], length=1.0)   # front edge
    face = chamfer(face.edges().group_by(Axis.Z)[0], length=0.5)    # rim: seam groove

    # alignment lips that drop into the body (kept clear of mikroBUS / headers)
    lip_t, lip_h, cl = 1.0, 1.6, 0.15
    xi0, xi1, yi0, yi1 = CAV_X0 + cl, CAV_X1 - cl, CAV_Y0 + cl, CAV_Y1 - cl
    z0 = Z_SPLIT - lip_h
    face += Pos(xi0 + lip_t / 2, 27.0, z0) * Box(lip_t, 42.0, lip_h + 0.01, align=(Align.CENTER, Align.CENTER, Align.MIN))
    face += Pos(xi1 - lip_t / 2, 27.0, z0) * Box(lip_t, 42.0, lip_h + 0.01, align=(Align.CENTER, Align.CENTER, Align.MIN))
    face += Pos(15.0, yi1 - lip_t / 2, z0) * Box(30.0, lip_t, lip_h + 0.01, align=(Align.CENTER, Align.CENTER, Align.MIN))
    for lx0, lx1 in ((0.0, BAY_X0 - 0.5), (BAY_X1 + 0.5, 48.0)):        # bottom lip, interrupted at the port bay
        face += Pos((lx0 + lx1) / 2, yi0 + lip_t / 2, z0) * Box(lx1 - lx0, lip_t, lip_h + 0.01,
                                                               align=(Align.CENTER, Align.CENTER, Align.MIN))

    # pillars clamp the PCB against the body standoffs; M3 screws come from the back
    for hx, hy in HOLES:
        face += cyl(hx, hy, 0.0, Z_SPLIT + 0.01, 6.0)
        face -= cyl(hx, hy, -1.0, 9.0, 2.6)          # pilot for M3 self-tapping

    # camera: four posts for the kit's M1.6 screws
    for u, v in CAM_HOLES_UV:
        px, py = cam_xy(u, v)
        face += cyl(px, py, CAM_Z_FRONT, Z_SPLIT + 0.01, 4.4)
        face -= cyl(px, py, CAM_Z_FRONT - 1.0, CAM_Z_FRONT + 3.0, CAM_PILOT_D)

    # lens holder pocket (inside), generous because the lens position is estimated;
    # in front of it the face stays solid (1.6 mm, printed on the bed) except for the eye
    pocket_top = CAM_Z_FRONT + LENS_BLOCK_H + LENS_HEADROOM          # 11.6
    face -= rrect_prism(LENS_X, LENS_Y, LENS_POCKET[0], LENS_POCKET[1], 1.0, Z_SPLIT - 1.0, pocket_top)
    face -= cyl(LENS_X, LENS_Y, pocket_top - 0.1, Z_FRONT + 1.0, EYE_D)
    # 0.4 mm lead-in at the eye's mouth: the shutter's sleeve enters here, and this edge is the
    # face's first layer (elephant foot)
    face -= Pos(LENS_X, LENS_Y, Z_FRONT - 0.4) * Cone(bottom_radius=EYE_D / 2, top_radius=EYE_D / 2 + 0.41,
                                                      height=0.41, align=(Align.CENTER, Align.CENTER, Align.MIN))
    for sx in (-1, 1):                              # pilots for the shutter module's M2 screws
        face -= cyl(LENS_X + sx * LENS_SCREW_DX, LENS_Y, Z_FRONT - LENS_SCREW_PILOT[1], Z_FRONT + 1.0,
                    LENS_SCREW_PILOT[0])

    # status light bar under the lens: one light pipe over the three LEDs,
    # hidden behind a 0.4 mm skin (glows through light filaments) or open
    tube_bottom = 2.0
    bx0, bx1 = LEDS[0][0] - 1.6, LEDS[2][0] + 1.6
    bcx, blen = (bx0 + bx1) / 2, bx1 - bx0
    wall = 0.85
    face += rrect_prism(bcx, 0.0, blen + 2 * wall, 1.4 + 2 * wall, 0.69 + wall, tube_bottom, Z_SPLIT + 0.01)
    skin = 0.4 if led_windows == "skin" else -1.0
    face -= rrect_prism(bcx, 0.0, blen, 1.4, 0.69, tube_bottom - 1.0, Z_FRONT - skin)

    # debossed wordmark, top-left, same margin as the lens ring has to the edges
    if WORDMARK:
        face -= wordmark_solid()
    return face


def wordmark_solid():
    """The front wordmark as a 0.6 mm solid: cut from the face, or printed as a colour inlay."""
    margin = OUT_X1 - (LENS_X + LENS_MODULE_D / 2)
    txt = Plane.XY.offset(Z_FRONT) * Pos(OUT_X0 + margin, OUT_Y1 - margin) * Text(
        WORDMARK, font_size=WORDMARK_SIZE, font_style=FontStyle.BOLD, align=(Align.MIN, Align.MAX))
    return extrude(txt, amount=-0.6)


# ==========================================================================
# LENS RING - printed back-down (flat back on the bed), glued flat onto the face
# ==========================================================================
def make_lens_ring():
    """Revolved profile: flat back, flat outer rim, then a shallow cone (lens hood) down
    to the eye. The bore matches the eye of the face, so the privacy cap centres the ring."""
    R, re = RING_D / 2, EYE_D / 2
    prof = [(re, 0.0), (R - 0.3, 0.0), (R, 0.3), (R, RING_PROUD - 0.4), (R - 0.4, RING_PROUD),
            (R - 2.6, RING_PROUD), (R - 2.9, RING_PROUD - 0.3), (7.6, RING_BORE_H + 0.3),
            (re + 0.3, RING_BORE_H + 0.3), (re, RING_BORE_H)]
    ring = revolve(Plane.XZ * Polygon(*prof, align=None), axis=Axis.Z)
    return Pos(LENS_X, LENS_Y, Z_FRONT) * ring


# ==========================================================================
# Reference models used for checks and renders (not printed)
# ==========================================================================
def make_camera_mock():
    pcb = Pos(CAM_X0 + CAM_W / 2, CAM_Y_TOP - CAM_H / 2, CAM_Z_FRONT - CAM_T) * Box(
        CAM_W, CAM_H, CAM_T, align=(Align.CENTER, Align.CENTER, Align.MIN))
    for u, v in CAM_HOLES_UV:
        hx, hy = cam_xy(u, v)
        pcb -= cyl(hx, hy, CAM_Z_FRONT - 2, CAM_Z_FRONT + 1, 2.2)
    block = Pos(LENS_X, LENS_Y, CAM_Z_FRONT) * Box(LENS_BLOCK, LENS_BLOCK, LENS_BLOCK_H - 0.6,
                                                   align=(Align.CENTER, Align.CENTER, Align.MIN))
    barrel = cyl(LENS_X, LENS_Y, CAM_Z_FRONT + LENS_BLOCK_H - 0.6, CAM_Z_FRONT + LENS_BLOCK_H, 7.0)
    conn = Pos(CAM_X0 + CAM_W / 2, CAM_Y_TOP - CAM_H + 2.6, CAM_Z_FRONT - CAM_T) * Box(
        20.0, 5.2, 1.3, align=(Align.CENTER, Align.CENTER, Align.MAX))
    flexj2 = Pos(CAM_X0 + 13.1, CAM_Y_TOP - 2.7, CAM_Z_FRONT) * Box(8.6, 3.8, 1.0, align=(Align.CENTER, Align.CENTER, Align.MIN))
    return pcb, block + barrel, conn + flexj2


def make_flex_cable():
    """15-to-22 pin flex: leaves the camera downward, U-turns and runs behind the camera into the CSI socket."""
    t = 0.15
    w15, w22 = 16.0, 11.5
    xc = 45.4
    zc = CAM_Z_FRONT - CAM_T - 0.65          # mid-height of the camera connector
    y_conn = CAM_Y_TOP - CAM_H + 2.0
    parts = []
    parts.append(Pos(xc, (y_conn + 6.0) / 2, zc) * Box(w15, y_conn - 6.0, t))
    r = (zc - 3.0) / 2
    tube = Pos(xc, 6.0, 3.0 + r) * Rot(0, 90, 0) * (Cylinder(radius=r + t / 2, height=w15) - Cylinder(radius=r - t / 2, height=w15 + 1))
    tube -= Pos(xc, 6.0 + 10, 3.0 + r) * Box(w15 + 2, 20, 20)
    parts.append(tube)
    parts.append(Pos(xc, (6.0 + 37.0) / 2, 3.0) * Box(w15, 31.0, t))
    parts.append(Pos(45.4, (37.0 + 45.5) / 2, 1.0) * Box(w22, 8.5, t))
    parts.append(Pos(xc, 37.0, 2.0) * Rot(-63, 0, 0) * Box(w22, 2.2, t))
    return parts


def export_all():
    os.makedirs(OUT_DIR, exist_ok=True)
    body = make_body()
    face = make_face("skin")
    face_open = make_face("open")
    ring = make_lens_ring()
    plunger = make_user_plunger()
    tol = dict(tolerance=0.01, angular_tolerance=0.2)
    export_step(body, os.path.join(OUT_DIR, "CoralEye_Body.step"))
    export_step(face, os.path.join(OUT_DIR, "CoralEye_Face.step"))
    export_step(ring, os.path.join(OUT_DIR, "CoralEye_LensRing_Lite.step"))
    export_step(plunger, os.path.join(OUT_DIR, "CoralEye_UserButton.step"))
    # print orientation: body back-down, face front-down, ring front-up, plunger flange-down
    export_stl(Pos(0, 0, -Z_BACK) * body, os.path.join(OUT_DIR, "CoralEye_Body.stl"), **tol)
    flip = Pos(0, 0, Z_FRONT) * Rot(180, 0, 0)
    export_stl(flip * face, os.path.join(OUT_DIR, "CoralEye_Face_hidden-LEDs.stl"), **tol)
    export_stl(flip * face_open, os.path.join(OUT_DIR, "CoralEye_Face_open-LEDs.stl"), **tol)
    # two-colour option: load this together with a face STL as one object with two parts
    export_stl(flip * wordmark_solid(), os.path.join(OUT_DIR, "CoralEye_Face_Wordmark-Inlay.stl"),
               tolerance=0.005, angular_tolerance=0.1)
    export_stl(Pos(-LENS_X, -LENS_Y, -Z_FRONT) * ring, os.path.join(OUT_DIR, "CoralEye_LensRing_Lite.stl"),
               tolerance=0.005, angular_tolerance=0.1)
    export_stl(user_plunger_print(plunger), os.path.join(OUT_DIR, "CoralEye_UserButton.stl"),
               tolerance=0.005, angular_tolerance=0.1)
    return body, face


def user_plunger_print(plunger=None):
    """The plunger in print orientation: axis along z, flange on the bed (used for the STL and the 3MF)."""
    pl = Rot(-90, 0, 0) * (plunger or make_user_plunger())       # board side (flange) goes to -z
    bb = pl.bounding_box()
    pl = Pos(-(bb.min.X + bb.max.X) / 2, -(bb.min.Y + bb.max.Y) / 2, -bb.min.Z) * pl
    foot = pl & Pos(0, 0, 0.2) * Box(20, 20, 0.4)
    assert foot and foot.volume > 3.0, "USER button is not standing on its flange"
    return pl


def export_render_meshes(body, face, folder):
    """Assembly-frame meshes for the renders (not meant for printing)."""
    os.makedirs(folder, exist_ok=True)
    tol = dict(tolerance=0.01, angular_tolerance=0.15)
    export_stl(body, os.path.join(folder, "body.stl"), **tol)
    export_stl(face, os.path.join(folder, "face.stl"), **tol)
    export_stl(make_lens_ring(), os.path.join(folder, "ring.stl"), tolerance=0.005, angular_tolerance=0.1)
    export_stl(make_user_plunger(), os.path.join(folder, "plunger.stl"), tolerance=0.005, angular_tolerance=0.1)
    # render-only hardware: the four M3 button heads in the back counterbores
    heads = None
    for hx, hy in HOLES:
        h = Pos(hx, hy, Z_BACK + 0.15) * (Cylinder(radius=2.85, height=1.65, align=(Align.CENTER, Align.CENTER, Align.MAX))
                                          - Pos(0, 0, -1.65) * Rot(0, 0, 0) * Box(1.6, 1.6, 2.0))
        heads = h if heads is None else heads + h
    export_stl(heads, os.path.join(folder, "screws_back.stl"), tolerance=0.01, angular_tolerance=0.15)
    pcb, block, conn = make_camera_mock()
    export_stl(pcb, os.path.join(folder, "cam_pcb.stl"), **tol)
    export_stl(block, os.path.join(folder, "cam_lens.stl"), **tol)
    export_stl(conn, os.path.join(folder, "cam_conn.stl"), **tol)
    flex = make_flex_cable()
    cable = flex[0]
    for p in flex[1:]:
        cable += p
    export_stl(cable, os.path.join(folder, "cable.stl"), **tol)


if __name__ == "__main__":
    import sys
    b, f = export_all()
    print("body volume %.1f cm3, face volume %.1f cm3" % (b.volume / 1000, f.volume / 1000))
    print("body bbox", b.bounding_box())
    print("face bbox", f.bounding_box())
    if "--render-meshes" in sys.argv:
        export_render_meshes(b, f, os.path.normpath(os.path.join(HERE, "..", "render", "meshes")))
