# SPDX-License-Identifier: Apache-2.0
"""Goldfish v01 — printable enclosure from the verified layout.

Outputs (out/): top_shell, bottom_shell, eye_cup, eye_lens, read_button
as STEP + STL, plus parts.json with every keep-out box for the viewer.

    .venv/bin/python enclosure.py
"""
from __future__ import annotations

import json
import math
from pathlib import Path

from build123d import (Align, Axis, Box, Cylinder, Location, Part, Pos, Rot,
                       export_step, export_stl, Mode)

import layout as LY

OUT = Path(__file__).parent / "out"
OUT.mkdir(exist_ok=True)

P = LY.Params(L=124, W=45, T=22, rz=22 * 0.4)
Li, Wi, Ti = LY.cavity_dims(P)
comps, info = LY.layout(P)
C = {c.name: c for c in comps}

# ---- fit constants -------------------------------------------------------
Z_SPLIT = 7.9            # cavity z of the parting plane (just above the battery)
TONGUE_W, TONGUE_H = 0.9, 1.8
FIT = 0.15               # sliding clearance
INSERT_D, INSERT_DEPTH, POST_D = 3.6, 6.0, 6.4   # M2.5 heat-set (ruthex RX-M2.5x5.7: OD 4.0 → 3.6 hole)
MIC_HOLE = 1.0


def box(size, at):
    return Box(*size, align=(Align.MIN, Align.MIN, Align.MIN)).moved(Location(at))


def halfspace(z0, up=True):
    big = 400
    return Box(big, big, big, align=(Align.CENTER, Align.CENTER, Align.MIN if up else Align.MAX)).moved(Location((0, 0, z0)))


outer = LY.make_body(P, 0.0)
cavity = LY.make_body(P, P.wall)
shell = outer - cavity

# ---- split + tongue ------------------------------------------------------
top = shell & halfspace(Z_SPLIT, up=True)
bottom = shell & halfspace(Z_SPLIT, up=False)
# tongue: a 0.9 mm band just inside the cavity wall (with sliding clearance), rising above the split
band = LY.make_body(P, P.wall + FIT) - LY.make_body(P, P.wall + FIT + TONGUE_W)
tongue = band & halfspace(Z_SPLIT - 0.01, up=True) & halfspace(Z_SPLIT + TONGUE_H, up=False)
root = (LY.make_body(P, P.wall - 0.2) - LY.make_body(P, P.wall + FIT + TONGUE_W)) & halfspace(Z_SPLIT - 1.5, up=True) & halfspace(Z_SPLIT, up=False)
bottom = bottom + root + tongue

# snap bumps on the straight sides: 4 bumps, 0.45 mm proud, matching dimples in the top shell
snap_x = (-28.0, 22.0)
for sx in snap_x:
    for sy in (-1, 1):
        y_face = sy * (Wi / 2 - FIT)
        zc = Z_SPLIT + TONGUE_H * 0.55
        bump = Cylinder(0.9, 6.0, rotation=(0, 90, 0)).moved(Location((sx, y_face - sy * 0.45, zc)))
        bottom = bottom + (bump & LY.make_body(P, P.wall - 0.45))
        top = top - Cylinder(1.0, 6.6, rotation=(0, 90, 0)).moved(Location((sx, y_face - sy * 0.45, zc)))

# coin slot at the chin to pry the halves apart
top = top - box((7, 6, 1.0), (P.L / 2 - 6, -3, Z_SPLIT - 0.01))

# ---- eye -----------------------------------------------------------------
x_eye = info["x_eye"]
r_out = P.eye_r + 4.55 + 1.6          # socket outer radius
led_top = info["led_z"] + 3.2
eye_hole = Cylinder(r_out, 40).moved(Location((x_eye, 0, Ti)))
top = top - eye_hole

# eye cup (print dark): a puck under the lens with 5 arc light-wells and a centre well for the mic-live dot
cup_h = Ti + P.wall - led_top           # LED top → outer surface
cup = Cylinder(r_out - FIT, cup_h, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((x_eye, 0, led_top)))
cup = cup & LY.make_body(P, 0.6)        # follow the dome, leaving 0.6 for the lens
for i in range(5):
    a = math.radians(90 - i * 72)
    cx, cy = x_eye + P.eye_r * math.cos(a), P.eye_r * math.sin(a)
    well = Cylinder(3.6, 40, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((cx, cy, led_top - 1)))
    cup = cup - well
cup = cup - Cylinder(2.0, 40, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((x_eye, 0, led_top - 1)))
# locating flange that sits under the shell
flange = Cylinder(r_out + 2.0, 1.0, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((x_eye, 0, led_top)))
cup = cup + (flange - Cylinder(r_out - 3, 2, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((x_eye, 0, led_top - 0.5))))
top = top - Cylinder(r_out + 2.0 + FIT, 1.0 + FIT, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((x_eye, 0, led_top - FIT)))

# lens (print natural/clear PETG): 0.6 mm skin flush with the dome
lens = (LY.make_body(P, 0.0) - LY.make_body(P, 0.6)) & Cylinder(r_out - FIT, 40).moved(Location((x_eye, 0, Ti)))
lens = lens & halfspace(led_top, True)

# ---- board standoffs (hang the Radxa from the top shell) ------------------
x_t, y0 = info["board_tail_x"], info["board_y0"]
pcb_top = info["pcb_top"]
holes = [(x_t + hx, y0 + hy) for hx in (3.5, 61.5) for hy in (3.5, 26.5)]
for hx, hy in holes:
    post = Cylinder(POST_D / 2, Ti + P.wall - pcb_top, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((hx, hy, pcb_top)))
    post = post & LY.make_body(P, 0.0)
    top = top + post
    top = top - Cylinder(INSERT_D / 2, INSERT_DEPTH, align=(Align.CENTER, Align.CENTER, Align.MIN)).moved(Location((hx, hy, pcb_top - 0.01)))

# ---- openings -------------------------------------------------------------
# mic ports
for name in ("mic head (SPH0645)", "mic tail (SPH0645)"):
    m = C[name]
    cx, cy = m.at[0] + m.size[0] / 2, m.at[1] + m.size[1] / 2
    top = top - Cylinder(MIC_HOLE / 2, 20).moved(Location((cx, cy, Ti + 2)))

# tail USB-C (charge): rounded slot through the tip
u = C["USB-C breakout (charge)"]
uz = u.at[2] + 1.6 + 1.6
usb_cut = Box(12, 9.6, 4.0).moved(Location((-P.L / 2, 0, uz)))
bottom = bottom - usb_cut
top = top - usb_cut

# mic slider on the belly side, beside the eye
slider_x = x_eye - 2
sl = Box(6.0, 6, 2.6).moved(Location((slider_x, -P.W / 2, Z_SPLIT + 3.2)))
top = top - sl

# read button on the belly side near the tail (thumb while the phone touches the tail)
rb_x = x_t + 10
top = top - Cylinder(2.1, 8, rotation=(90, 0, 0)).moved(Location((rb_x, -P.W / 2 + 1, Z_SPLIT + 4.2)))
read_button = (Cylinder(1.9, 3.2, rotation=(90, 0, 0)) + Cylinder(2.8, 0.8, rotation=(90, 0, 0)).moved(Location((0, 1.8, 0)))).moved(
    Location((rb_x, -P.W / 2 + 1.4, Z_SPLIT + 4.2)))

# battery cradle ribs on the floor
bx0 = C["battery"].at[0]
bottom = bottom + (box((1.0, 20, 3.0), (bx0 + C["battery"].size[0] + 0.2, -10, -0.5)) & LY.make_body(P, 0.0))

# ---- export --------------------------------------------------------------
parts = {"top_shell": top, "bottom_shell": bottom, "eye_cup": cup, "eye_lens": lens, "read_button": read_button}
for n, s in parts.items():
    export_step(s, str(OUT / f"{n}.step"))
    export_stl(s, str(OUT / f"{n}.stl"), tolerance=0.02, angular_tolerance=0.15)
    bb = s.bounding_box()
    print(f"{n:13s} vol {s.volume/1000:6.2f} cm³  bbox {bb.size.X:.1f}×{bb.size.Y:.1f}×{bb.size.Z:.1f}")

json.dump({
    "params": {k: getattr(P, k) for k in ("L", "W", "T", "wall", "rz", "rz_bot")},
    "info": info, "z_split": Z_SPLIT,
    "comps": [{"name": c.name, "size": c.size, "at": c.at, "group": c.group} for c in comps],
    "z_offset": P.wall,   # cavity floor is z=0; outer body bottom is at -wall
}, open(OUT / "parts.json", "w"), indent=1)
print("outer", P.L, P.W, P.T, "cavity", round(Li, 1), round(Wi, 1), round(Ti, 1))
