"""Ring parts kit — validated components the archetype templates assemble from.

v2 (castable core): every metal element is built to FUSE into one watertight
body and to obey lost-wax casting floors (Formlabs/Materialise guides):
band >=1.0 mm thick, prongs >=0.8 mm base / 0.6 mm tip diameter, bead prongs
>=0.5 mm. Stones are separate solids (set after casting, never printed) and
the templates cut real seats for them.

Grounding:
- US size -> inner diameter: d(mm) = 0.8128*size + 11.63 (standard US formula).
- carat -> stone dims: cube-root scaling from standard 1ct reference sizes.
- Gem angles from open faceting references (crown ~34 deg, pavilion ~41 deg,
  table ~55%) — lofted polygon facets so stones read FACETED.
"""
from build123d import *
import math

# ---- casting floors (mm) ---------------------------------------------------
MIN_WALL = 1.0          # band / structural wall thickness
MIN_PRONG_R = 0.40      # 0.8 mm diameter prong base
MIN_TIP_R = 0.30        # 0.6 mm diameter prong tip
MIN_BEAD_R = 0.25       # 0.5 mm diameter shared bead prongs

# ---- palette (high-contrast CAD scheme: saturated metals, ice-blue stones,
# green supports — per the jeweler's reference screenshots) --------------------
METALS = {
    "yellow": Color(0.87, 0.62, 0.12),
    "rose": Color(0.82, 0.44, 0.30),
    "white": Color(0.55, 0.59, 0.66),
    "platinum": Color(0.50, 0.55, 0.63),
}
DIAMOND = Color(0.60, 0.82, 1.00)   # ice blue — pops against every metal
STONE_COLORS = {                     # center-stone gemstones (RingSpec stone_color)
    "diamond": (0.60, 0.82, 1.00),
    "emerald": (0.13, 0.62, 0.38),
    "ruby": (0.78, 0.16, 0.28),
    "sapphire": (0.16, 0.30, 0.86),
    "morganite": (0.95, 0.66, 0.66),
}
SUPPORT = Color(0.15, 0.72, 0.30)   # casting sprues/stems (never part of the ring)


def metal_color(metal: str) -> Color:
    for key, c in METALS.items():
        if key in metal.lower():
            return c
    return METALS["white"]


def is_metal(part) -> bool:
    """Anything whose color matches a known STONE color is a stone; everything
    else is metal. (Purely hue-heuristic tests broke the moment gemstone
    colors joined the palette — green reads 'metal' on blue-dominance.)"""
    c = getattr(part, "color", None)
    if c is None:
        return True
    r, g, b = tuple(c)[:3]
    for sr, sg, sb in STONE_COLORS.values():
        if abs(r - sr) < 0.04 and abs(g - sg) < 0.04 and abs(b - sb) < 0.04:
            return False
    return True


def scaled_about_center(s, f: float):
    """Uniform scale about the shape's own center (plain scale() is about the
    origin and would displace off-center stones)."""
    c = s.center()
    return Pos(c.X, c.Y, c.Z) * scale(Pos(-c.X, -c.Y, -c.Z) * s, f)


def fuse_all(solids):
    """Boolean fuse of many solids into one body.

    Balanced tree for normal counts; above ~64 parts switch to chunked mode
    (balanced-fuse groups of 16, then accumulate sequentially) — a full tree
    keeps O(n) complex intermediates alive and a full-circle pave band
    (~180 parts) got the process OOM-killed."""
    items = [s for s in solids if s is not None]
    if not items:
        raise ValueError("nothing to fuse")

    def tree(chunk):
        while len(chunk) > 1:
            nxt = []
            for i in range(0, len(chunk) - 1, 2):
                nxt.append(chunk[i] + chunk[i + 1])
            if len(chunk) % 2:
                nxt.append(chunk[-1])
            chunk = nxt
        return chunk[0]

    if len(items) <= 64:
        return tree(items)
    acc = None
    for start in range(0, len(items), 16):
        g = tree(items[start:start + 16])
        acc = g if acc is None else acc + g
    return acc


# ---- sizing ----------------------------------------------------------------
def inner_radius(us_size: float) -> float:
    return (0.8128 * us_size + 11.63) / 2


# ---- stones: carat -> dims (mm), cube-root scaled from 1ct references ------
_REF_1CT = {  # (width, length) of a 1.0 ct stone
    "round": (6.5, 6.5), "oval": (5.7, 8.0), "cushion": (5.6, 5.6),
    "elongated_cushion": (5.0, 7.0), "princess": (5.5, 5.5),
    "emerald": (4.3, 6.0), "radiant": (4.5, 6.3), "asscher": (5.1, 5.1),
    "pear": (4.9, 7.4), "marquise": (4.2, 8.4),
}


def stone_dims(cut: str, carat: float) -> tuple[float, float]:
    w, length = _REF_1CT.get(cut, _REF_1CT["round"])
    s = carat ** (1 / 3)
    return w * s, length * s


# ---- 2D outline faces ------------------------------------------------------
def teardrop_face(r, tip_y, soft=0.4):
    face = Circle(r) + Polygon((-r, 0), (r, 0), (0, tip_y))
    tip_vertex = face.vertices().sort_by(Axis.Y)[-1]
    return fillet(tip_vertex, soft)


def marquise_face(w, L):
    R = (w * w + L * L) / (2 * w)
    d = R - w
    return (Pos(-d, 0) * Circle(R)) & (Pos(d, 0) * Circle(R))


# ---- gems (faceted) --------------------------------------------------------
CROWN_DEG, PAV_DEG, TABLE_PCT = 34.0, 41.0, 0.55


def gem_round(gr, girdle_z, elong=1.0, pav_h=None, exact=False):
    """Faceted brilliant: octagon loft (8 kite crown facets + table).
    pav_h overrides the angle-derived pavilion depth; exact=True uses a RULED
    loft so the girdle diameter is exact (the default B-spline loft bulges
    ~1.5% between sections — fine for renders, fatal for a real stone's seat)."""
    table_r = gr * TABLE_PCT
    crown_h = (gr - table_r) * math.tan(math.radians(CROWN_DEG))
    if pav_h is None:
        pav_h = gr * math.tan(math.radians(PAV_DEG)) * 0.85
    gird = Pos(0, 0, girdle_z) * RegularPolygon(gr, 8)
    tab = Pos(0, 0, girdle_z + crown_h) * RegularPolygon(table_r, 8).rotate(Axis.Z, 22.5)
    keel = Pos(0, 0, girdle_z - pav_h) * RegularPolygon(gr * 0.04, 8)
    s = loft([keel, gird, tab], ruled=exact)
    if elong != 1.0:
        s = scale(s, (1, elong, 1))
    s.color = DIAMOND
    return s, crown_h


def gem_rect(gw, gL, girdle_z, corner=0.45, step=True, pav_h=None, exact=False):
    """Step-cut (emerald/radiant/asscher) or cushion: lofted rounded rectangles.
    step=True adds an intermediate crown step so it reads step-cut."""
    crown_h = min(gw, gL) * 0.28
    if pav_h is None:
        pav_h = min(gw, gL) * 0.75
    gird = Pos(0, 0, girdle_z) * RectangleRounded(gw * 2, gL * 2, corner)
    faces = [Pos(0, 0, girdle_z - pav_h) * RectangleRounded(gw * 0.35, gL * 0.35, corner * 0.3),
             gird]
    if step:
        faces.append(Pos(0, 0, girdle_z + crown_h * 0.55)
                     * RectangleRounded(gw * 1.7, gL * 1.7, corner * 0.8))
    faces.append(Pos(0, 0, girdle_z + crown_h)
                 * RectangleRounded(gw * 1.35, gL * 1.35, corner * 0.7))
    s = loft(faces, ruled=exact)
    s.color = DIAMOND
    return s, crown_h


def gem_princess(g, girdle_z, kite=False, pav_h=None, exact=False):
    """Princess: square pyramid loft; kite=True turns it 45 deg (corners on axes)."""
    crown_h = g * 0.30
    if pav_h is None:
        pav_h = g * 0.85
    gird = Pos(0, 0, girdle_z) * Rectangle(g * 2, g * 2)
    tab = Pos(0, 0, girdle_z + crown_h) * Rectangle(g * 1.25, g * 1.25)
    keel = Pos(0, 0, girdle_z - pav_h) * Rectangle(g * 0.1, g * 0.1)
    s = loft([keel, gird, tab], ruled=exact)
    if kite:
        s = s.rotate(Axis.Z, 45)
    s.color = DIAMOND
    return s, crown_h


def gem_outline(face, girdle_z, crown_h, pav_h, table_scale=0.62,
                keel_scale=0.14, exact=False):
    """Pear/marquise/any outline: lofted crown + pavilion from a 2D face."""
    gird = Pos(0, 0, girdle_z) * face
    tab = Pos(0, 0, girdle_z + crown_h) * scale(face, table_scale)
    keel = Pos(0, 0, girdle_z - pav_h) * scale(face, keel_scale)
    s = loft([keel, gird, tab], ruled=exact)
    s.color = DIAMOND
    return s


# ---- metalwork -------------------------------------------------------------
def band(us_size, thickness=0.75, width=1.5, metal="white"):
    """Comfort shank: a rounded-rectangle cross-section REVOLVED 360 degrees
    (v2 — replaces the square-edged tube; reads and casts like a real band).
    Casting floors applied: thickness >=1.0, width >=1.5."""
    inner_r = inner_radius(us_size)
    t = max(thickness, MIN_WALL)
    w = max(width, 1.5)
    outer_r = inner_r + t
    mid = inner_r + t / 2
    prof = Plane.XZ * Pos(mid, 0) * RectangleRounded(t, w, min(t, w) * 0.32)
    b = revolve(prof, Axis.Z).rotate(Axis.X, 90)
    b.color = metal_color(metal)
    return b, inner_r, outer_r


def shoulders(outer_r, band_t, band_w, hw, girdle_z, angle_deg=34, metal="white"):
    """Cathedral shoulders v2: LOFTED tapering wedges rising from inside the
    band to under the head rim — replaces the rod-with-ball-end struts that
    read unfinished in every judged round."""
    out = []
    sh = math.radians(angle_deg)
    for sgn in (1, -1):
        root_r = outer_r - 0.45
        p1 = (sgn * root_r * math.sin(sh), 0.0, root_r * math.cos(sh))
        p2 = (sgn * hw * 0.85, 0.0, girdle_z - 0.30)
        d = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
        L = math.sqrt(sum(x * x for x in d))
        zdir = (d[0] / L, d[1] / L, d[2] / L)
        base = Plane(origin=p1, z_dir=zdir) * RectangleRounded(
            max(band_t * 1.15, 1.1), max(band_w * 0.85, 1.2), 0.25)
        top = Plane(origin=p2, z_dir=zdir) * RectangleRounded(
            max(band_t * 0.7, 0.8), max(band_w * 0.5, 0.9), 0.2)
        s = loft([base, top])
        s.color = metal_color(metal)
        out.append(s)
    return out


def basket(rim_r, girdle_z, outer_r, metal="white", bore=None):
    """Open cathedral basket bridging band top -> under the stone.
    bore: bottom opening radius — must exceed the pavilion's width at that
    depth or the stone physically cannot seat (a 3ct pavilion is far wider
    than the old fixed 0.65 hole)."""
    gbot = outer_r - 0.25
    bh = girdle_z - gbot
    b0 = max(0.65, bore or 0.65)
    wall = max(1.05, b0 + 0.4)
    bk = (Pos(0, 0, gbot) * Cone(wall, rim_r, bh, align=(Align.CENTER, Align.CENTER, Align.MIN))
          - Pos(0, 0, gbot - 0.1) * Cone(b0, rim_r * 0.76, bh + 0.2,
                                         align=(Align.CENTER, Align.CENTER, Align.MIN)))
    bk.color = metal_color(metal)
    return bk


def _seg(p0, p1, r0, r1):
    dx, dy, dz = p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]
    L = math.sqrt(dx * dx + dy * dy + dz * dz)
    if abs(r0 - r1) < 1e-6:                       # OCC rejects cones with equal radii
        cone = Cylinder(r0, L, align=(Align.CENTER, Align.CENTER, Align.MIN))
    else:
        cone = Cone(r0, r1, L, align=(Align.CENTER, Align.CENTER, Align.MIN))
    cone = cone.rotate(Axis.Y, math.degrees(math.atan2(math.hypot(dx, dy), dz)))
    cone = cone.rotate(Axis.Z, math.degrees(math.atan2(dy, dx)))
    return Pos(*p0) * cone


seg = _seg          # public alias: straight tapered strut between two 3D points


def claw(root, gx, gy, girdle_z, crown_h, w, cx0=0.0, cy0=0.0,
         inward_mm=None):
    """Three-segment gripping claw: root arm from the basket rim, near-vertical
    post at the stone edge, edge-hugging curl whose bead rests on the crown
    slope. v2: casting floors — base >=0.8 mm dia, tip >=0.6 mm dia.
    inward_mm: absolute curl reach past the girdle edge — outline stones
    (pear/marquise) need this; the default 20%-of-radius curl lies a full
    millimetre ACROSS a long teardrop's crown (r74)."""
    w = max(w, MIN_PRONG_R)
    tip_r = max(w * 0.30, MIN_TIP_R)
    d = math.hypot(gx, gy)
    f = max(0.55, 1 - inward_mm / d) if inward_mm else 0.80
    base = (cx0 + gx * 1.04, cy0 + gy * 1.04, girdle_z - 0.55)
    over = (cx0 + gx * 1.04, cy0 + gy * 1.04, girdle_z + crown_h * 0.30)
    tip = (cx0 + gx * f, cy0 + gy * f, girdle_z + crown_h * 0.52)
    c = (_seg(root, base, w, w * 0.9) + _seg(base, over, w * 0.9, w * 0.72)
         + _seg(over, tip, w * 0.72, tip_r))
    c += (Pos(*base) * Sphere(w * 0.9) + Pos(*over) * Sphere(w * 0.78)
          + Pos(*tip) * Sphere(max(w * 0.34, MIN_TIP_R)))
    return c


def claw_set(points, rim_r, girdle_z, crown_h, w, metal="white", cx0=0.0,
             cy0=0.0, root_frac=None):
    """One claw per (gx, gy) girdle point. Roots sit on the basket rim circle,
    or — for elongated outlines (pear/marquise) — at root_frac of each point's
    own radius, so tip claws rise instead of lying sideways (r73)."""
    out = []
    for gx, gy in points:
        d = math.hypot(gx, gy)
        rr = d * root_frac if root_frac else rim_r
        root = (cx0 + gx / d * rr, cy0 + gy / d * rr, girdle_z - 0.10)
        c = claw(root, gx, gy, girdle_z, crown_h, w, cx0, cy0,
                 inward_mm=0.55 if root_frac else None)
        c.color = metal_color(metal)
        out.append(c)
    return out


def claw_straight(root, gx, gy, girdle_z, crown_h, w, overlength=1.2, gap=0.05):
    """PRODUCTION claw: a settable straight prong blank. Root arm from the
    basket rim to a base just outside the girdle, then a VERTICAL tapered post
    ending in a domed tip `overlength` mm above the crown. The setter cuts the
    bearing at girdle height and bends the remainder over the real stone —
    nothing pre-curls, so a real diamond drops straight in."""
    w = max(w, MIN_PRONG_R)
    tip_r = max(w * 0.55, MIN_TIP_R)
    d = math.hypot(gx, gy)
    ux, uy = gx / d, gy / d
    px, py = gx + ux * (gap + w), gy + uy * (gap + w)   # post axis stand-off
    base = (px, py, girdle_z - 0.55)
    tip = (px, py, girdle_z + crown_h + overlength)
    c = (_seg(root, base, w, w * 0.95) + _seg(base, tip, w * 0.95, tip_r)
         + Pos(*base) * Sphere(w * 0.95) + Pos(*tip) * Sphere(tip_r))
    return c


def claw_set_straight(points, rim_r, girdle_z, crown_h, w, metal="white",
                      overlength=1.2, gap=0.05, root_frac=None):
    """Production prong set: one straight settable blank per girdle point."""
    out = []
    for gx, gy in points:
        d = math.hypot(gx, gy)
        rr = d * root_frac if root_frac else rim_r
        root = (gx / d * rr, gy / d * rr, girdle_z - 0.10)
        c = claw_straight(root, gx, gy, girdle_z, crown_h, w,
                          overlength=overlength, gap=gap)
        c.color = metal_color(metal)
        out.append(c)
    return out


def seat_cutter_prod(stone, clr: float):
    """Production seat cutter: the stone grown by a TRUE 3D offset — exact
    absolute bearing clearance everywhere. (Scale-based growth proved unsafe:
    build123d scale/center on transformed lofts can leave parts of the stone
    OUTSIDE the 'grown' copy, i.e. an overlapping seat.) clr in mm."""
    try:
        return offset(stone, clr, kind=Kind.INTERSECTION)
    except Exception:
        return offset(stone, clr, kind=Kind.ARC)


def gallery_rail(face, girdle_z, wall=0.22, drop=0.34, metal="white"):
    """Slim visible rail just under the girdle tying every claw post together.
    Thin band (not a bezel wall): the pavilion stays exposed below it."""
    ring = (offset(face, 0.16, kind=Kind.ARC)
            - offset(face, -wall + 0.16, kind=Kind.ARC))
    rail = loft([Pos(0, 0, girdle_z - drop) * ring,
                 Pos(0, 0, girdle_z - drop + 0.20) * ring])
    rail.color = metal_color(metal)
    return rail


def bezel_collar(face, girdle_z, wall=0.32, lip=0.20, depth=0.55, metal="white",
                 clearance_mm=None):
    """Smooth full-bezel collar wrapping a 2D outline. Uniform offset (Kind.ARC)
    keeps the rim even and rounds sharp points — no miter fins.
    clearance_mm (production): the inner cut is the outline offset OUT by an
    absolute bearing clearance instead of the proportional 0.98 shrink."""
    wall = max(wall, MIN_WALL * 0.3)
    rim = offset(face, wall, kind=Kind.ARC)
    if clearance_mm is None:
        inner = scale(face, 0.98)
    else:
        inner = offset(face, clearance_mm, kind=Kind.ARC)
    lo, hi = girdle_z - depth, girdle_z + lip
    col = (loft([Pos(0, 0, lo) * rim, Pos(0, 0, hi) * rim])
           - loft([Pos(0, 0, lo - 0.1) * inner,
                   Pos(0, 0, hi + 0.1) * inner]))
    col.color = metal_color(metal)
    return col


def pave_row(outer_r, n=16, r=0.26, start_deg=16, step_deg=4.2, metal="white",
             rows=1):
    """Pave v2 — a real setting, not surface dots:
    - melee girdles sit AT the band surface (crown proud, pavilion inside)
    - an azure hole is drilled under every melee (returned as cutters for the
      fused band) so plaster/metal flows correctly and stones can seat
    - shared bead prongs (>=0.5 mm) sit fused to the surface between stones
    rows: parallel rows across the band width (2 = double-row pave, offset a
    half step so the rows brick-lay like real micro-pave).
    Returns {"stones": [...], "prongs": [...], "cutters": [...]}."""
    stones, prongs, cutters = [], [], []
    rad = outer_r + r * 0.05
    dot_r = max(r * 0.34, MIN_BEAD_R)
    ys = [0.0] if rows <= 1 else [
        (j - (rows - 1) / 2) * (2 * r * 1.15) for j in range(rows)]
    for row_i, y in enumerate(ys):
        stagger = 0.5 * (row_i % 2)          # brick-lay offset between rows
        for side in (1, -1):
            for k in range(n):
                th = math.radians(start_deg + (k + stagger) * step_deg)
                sx, sz = math.sin(th), math.cos(th)
                g = Pos(side * rad * sx, y, rad * sz) * Sphere(r)
                g.color = DIAMOND
                stones.append(g)
                cutters.append(_seg(
                    (side * (outer_r - 1.4 * r) * sx, y, (outer_r - 1.4 * r) * sz),
                    (side * (outer_r + 0.5 * r) * sx, y, (outer_r + 0.5 * r) * sz),
                    r * 0.55, r * 0.55))
                if k + 1 < n:
                    tm = math.radians(start_deg + (k + stagger + 0.5) * step_deg)
                    pr = outer_r + dot_r * 0.15
                    d = Pos(side * pr * math.sin(tm), y,
                            pr * math.cos(tm)) * Sphere(dot_r)
                    d.color = metal_color(metal)
                    prongs.append(d)
    return {"stones": stones, "prongs": prongs, "cutters": cutters}


def outline_points(face, offset_out, spacing):
    """Evenly spaced (x, y) points along the TRUE offset outline of any 2D face."""
    ring = offset(face, offset_out, kind=Kind.ARC)
    faces = ring.faces() if hasattr(ring, "faces") else [ring]
    wire = faces[0].outer_wire()
    n = max(12, int(wire.length / spacing))
    pts = []
    for i in range(n):
        p = wire.position_at(i / n)
        pts.append((p.X, p.Y))
    return pts


def halo_ring(face, girdle_z, offset_out=0.44, stone_r=0.27, metal="white"):
    """Dense accent-stone ring set into the halo seat with shared bead prongs
    (>=0.5 mm). Returns {"stones": [...], "prongs": [...]} — global gem-seat
    cutting in the template sinks the stones into the seat metal."""
    stones, prongs = [], []
    pts = outline_points(face, offset_out, spacing=stone_r * 2.05)
    dot_r = max(stone_r * 0.36, MIN_BEAD_R)
    n = len(pts)
    for i, (x, y) in enumerate(pts):
        s = Pos(x, y, girdle_z + 0.04) * Sphere(stone_r)
        s.color = DIAMOND
        stones.append(s)
        nx, ny = pts[(i + 1) % n]
        p = Pos((x + nx) / 2, (y + ny) / 2, girdle_z + 0.02) * Sphere(dot_r)
        p.color = metal_color(metal)
        prongs.append(p)
    return {"stones": stones, "prongs": prongs}


def side_stone(cxp, sz, sr, band_z, metal="white"):
    """Side stone v2: a real FACETED ROUND BRILLIANT (not a cone) seated in a
    slim open collar merged into the shank, held by mini-claws at the casting
    floor. Returns {"stones": [...], "metal": [...]}."""
    stones, metals = [], []
    st, s_crown = gem_round(sr, 0)
    st = Pos(cxp, 0, sz) * st
    st.color = DIAMOND
    stones.append(st)
    col = (Pos(cxp, 0, band_z - 0.35) * Cone(max(0.55, sr * 0.45), sr * 0.9,
                                             (sz - band_z) + 0.15,
                                             align=(Align.CENTER, Align.CENTER, Align.MIN))
           - Pos(cxp, 0, band_z - 0.45) * Cone(0.3, sr * 0.55,
                                               (sz - band_z) + 0.45,
                                               align=(Align.CENTER, Align.CENTER, Align.MIN)))
    col.color = metal_color(metal)
    metals.append(col)
    for i in range(4):
        ang = math.radians(45 + i * 90)
        ux, uy = math.cos(ang), math.sin(ang)
        root = (cxp + sr * 0.55 * ux, sr * 0.55 * uy, sz - 0.12)
        c = claw(root, sr * ux, sr * uy, sz, s_crown, MIN_TIP_R, cx0=cxp, cy0=0.0)
        c.color = metal_color(metal)
        metals.append(c)
    return {"stones": stones, "metal": metals}


# ---- freeform details (RingSpec `details`: customization as data) ----------
def detail_wire(points, profile_mm=1.0, metal="white"):
    """Open swept wire — scrollwork, vines, struts. Casting floor 0.8mm."""
    pts = [tuple(map(float, q)) for q in points]
    path = Spline(*pts)
    prof = Plane(origin=pts[0], z_dir=path % 0) * Circle(max(profile_mm, 0.8) / 2)
    s = sweep(prof, path)
    s.color = metal_color(metal)
    return s


def detail_bead(at, d_mm=0.8, metal="white"):
    """Placed sphere — granulation, milgrain dots. Casting floor 0.5mm."""
    b = Pos(*[float(v) for v in at]) * Sphere(max(d_mm, 0.5) / 2)
    b.color = metal_color(metal)
    return b
