"""
Coral Eye mounts. Every mount carries the same male dovetail "tongue" that slides
up into the rail on the back of the camera. The GoPro and tripod adapters click into
place with a flexible latch (an asymmetric wedge on a 16 mm finger that drops into a
groove in the rail); the wall plate has no latch, the camera simply hangs on it.

The finger always grows from its root upwards in the print, so its free end is never
printed in mid-air: on the tripod adapter (printed upright) the root is below the
wedge, on the GoPro adapter (printed upside down) it is above it.

    * GoPro-style adapter  - tongue + 2 fingers pointing down (standard GoPro layout)
    * Desk stand           - base + post + 3 fingers (tilt, locked by an M5 bolt)
    * Thumb knob           - holds the head of the M5 bolt
    * Tripod adapter       - L-foot with a side-loaded 1/4"-20 nut, thread pointing down
    * Wall plate           - tongue with two countersunk screw holes
    * Magnet plate         - the wall plate with four 10 x 3 mm magnets: sticks to any steel surface

Coordinates are the camera's (see coral_eye.py): y up, z towards the viewer,
the camera's back surface is z = Z_BACK. Every part prints without supports in
the orientation given by PRINT_ORIENTATION.
"""
import os
from build123d import *
import coral_eye as ce

OUT_DIR = ce.OUT_DIR

# --------------------------------------------------------------------------
# tongue (male dovetail) with a flexible latch
# --------------------------------------------------------------------------
T_CLEAR = ce.RAIL_CLEAR                     # per side
T_H = ce.RAIL_DEPTH - ce.RAIL_TOP_CLEAR     # stays under the bridged channel ceiling
T_W0 = ce.RAIL_W_OPEN - 2 * T_CLEAR         # width at the camera's back surface
T_W1 = T_W0 + 2 * T_H                       # width at the tongue top (45 deg flanks)
T_Y0 = ce.OUT_Y0                            # flush with the camera's bottom edge
T_Y1 = ce.RAIL_Y_STOP - 0.5
LATCH_Y = ce.RAIL_LATCH_Y
FINGER_W, SLOT = 6.0, 0.8
FINGER_L = {"root_below": 18.5, "root_above": 17.5}   # 15.5 / 14.5 mm free to the wedge: ~0.5 % strain
WEDGE_TO_TIP = 3.0                          # finger length beyond the wedge
RELIEF_D = 1.6                              # round ends of the side slots at the root (no sharp notch)


def finger_span(root_above):
    """(y of the free-end slot edge, y of the root) ordered low to high, slot included."""
    if root_above:                          # root above the wedge, free end below it
        tip = LATCH_Y - WEDGE_TO_TIP
        return tip - SLOT, tip + FINGER_L["root_above"]
    tip = LATCH_Y + WEDGE_TO_TIP            # root below the wedge, free end above it
    return tip - FINGER_L["root_below"], tip + SLOT


def hex_prism(af, h):
    return extrude(RegularPolygon(radius=af / 2 / 0.8660254, side_count=6), amount=h)


def tongue(wedge=True):
    zb = ce.Z_BACK
    pts = [(ce.RAIL_CX - T_W0 / 2, zb), (ce.RAIL_CX + T_W0 / 2, zb),
           (ce.RAIL_CX + T_W1 / 2, zb + T_H), (ce.RAIL_CX - T_W1 / 2, zb + T_H)]
    t = extrude(Plane.XZ.offset(-T_Y0) * Polygon(*pts, align=None), amount=T_Y0 - T_Y1)
    if wedge:
        t += ce.ridge_x(ce.RAIL_CX, LATCH_Y, zb + T_H - 0.01, ce.LATCH_H, 4.0, ce.LATCH_ENTRY, ce.LATCH_HOLD)
    return t


def latch_cut(plate_t, root_above):
    """U-shaped slot through tongue and plate; the plate behind the finger is removed
    so the finger (tongue thickness only) can flex towards the back over its full length."""
    zb = ce.Z_BACK
    zc = zb - plate_t / 2
    y0, y1 = finger_span(root_above)
    yc, h = (y0 + y1) / 2, y1 - y0
    cut = Pos(ce.RAIL_CX - FINGER_W / 2 - SLOT / 2, yc, zc) * Box(SLOT, h, 20)
    cut += Pos(ce.RAIL_CX + FINGER_W / 2 + SLOT / 2, yc, zc) * Box(SLOT, h, 20)
    y_end = y0 + SLOT / 2 if root_above else y1 - SLOT / 2
    cut += Pos(ce.RAIL_CX, y_end, zc) * Box(FINGER_W + 2 * SLOT, SLOT, 20)
    cut += Pos(ce.RAIL_CX, yc, zb - plate_t / 2 - 0.01) * Box(FINGER_W + 2 * SLOT, h, plate_t + 0.02)
    y_root = y1 if root_above else y0
    for sx in (-1, 1):
        cut += Pos(ce.RAIL_CX + sx * (FINGER_W / 2 + SLOT / 2), y_root, zc) * Cylinder(radius=RELIEF_D / 2, height=20)
    return cut


def back_plate(y0, y1, w, t):
    """Plate lying against the camera's back, from Z_BACK backwards by t."""
    return Pos(ce.RAIL_CX, (y0 + y1) / 2, ce.Z_BACK - t) * extrude(RectangleRounded(w, y1 - y0, 3.0), amount=t)


def railed_plate(w, plate_t, y0=T_Y0, y1=T_Y1, latch="root_below"):
    """latch: "root_below" (printed upright), "root_above" (printed upside down) or None.
    The plate may reach above the tongue (y1 > T_Y1): it lies flat on the camera's back."""
    p = back_plate(y0, y1, w, plate_t) + tongue(wedge=latch is not None)
    if latch:
        p -= latch_cut(plate_t, root_above=(latch == "root_above"))
    return p


def gusset(y_base, z_base, depth, rise, width, downward=False):
    """Triangular prism behind the plate (in the YZ plane), extruded along x."""
    s = -1 if downward else 1
    prof = [(y_base, z_base + 0.01), (y_base, z_base - depth), (y_base + s * rise, z_base + 0.01)]
    return extrude(Plane.YZ.offset(ce.RAIL_CX - width / 2) * Polygon(*prof, align=None), amount=width,
                   dir=(1, 0, 0))


# --------------------------------------------------------------------------
# GoPro-style fingers (vertical plates, hole axis along x)
# --------------------------------------------------------------------------
GP_T = 3.0          # finger thickness
GP_GAP = 3.3        # gap that receives a mating finger
GP_R = 7.5          # radius of the rounded end
GP_HOLE = 5.3       # M5
GP_REACH = 8.5      # hole centre distance from the finger base


def finger(xc, base_y, axis_z, direction):
    """Finger centred at x = xc; it grows from y = base_y in `direction` (+1 up / -1 down)."""
    hy = base_y + direction * GP_REACH
    sk = Plane.YZ.offset(xc - GP_T / 2) * (
        Pos((base_y + hy) / 2, axis_z) * Rectangle(abs(hy - base_y), 2 * GP_R)
        + Pos(hy, axis_z) * Circle(GP_R))
    f = extrude(sk, amount=GP_T)
    f -= Pos(xc, hy, axis_z) * Rot(0, 90, 0) * Cylinder(radius=GP_HOLE / 2, height=GP_T + 2)
    return f


ADAPTER_T = 3.0
GUSSET_D = 2 * GP_R                                     # depth of the block that carries the fingers
AXIS_Z = ce.Z_BACK - ADAPTER_T - GP_R                   # fingers sit right behind the plate
BLOCK_Y0 = T_Y0 - 3.0                                   # underside of the adapter block
AXIS_Y = BLOCK_Y0 - GP_REACH
GOPRO_PLATE_TOP = T_Y1 + 4.5                            # plate beyond the tongue: solid ground for the finger root


def gopro_adapter():
    top = GOPRO_PLATE_TOP
    a = railed_plate(26.0, ADAPTER_T, y0=BLOCK_Y0, y1=top, latch="root_above")   # printed upside down
    zb = ce.Z_BACK - ADAPTER_T
    # lower block (carries the fingers), with a 39-degree gusset towards the plate
    prof = [(BLOCK_Y0, zb + 0.01), (BLOCK_Y0, zb - GUSSET_D), (BLOCK_Y0 + 3.0, zb - GUSSET_D),
            (BLOCK_Y0 + 3.0 + GUSSET_D * 1.25, zb + 0.01)]
    blk = extrude(Plane.YZ.offset(ce.RAIL_CX - 13.0) * Polygon(*prof, align=None), amount=26.0)
    a += fillet(blk.edges().filter_by(Axis.X).group_by(Axis.Z)[0], radius=2.0)
    # upper gusset: widens the footprint the part stands on while printing (upside down)
    a += gusset(top, zb, 10.0, -10.0 * 1.25, 26.0)
    o = GP_GAP / 2 + GP_T / 2
    for dx in (-o, o):
        a += finger(ce.RAIL_CX + dx, BLOCK_Y0 + 0.01, AXIS_Z, -1)
    return a


STAND_POST_H = 35.0          # leaves ~55 mm under the camera for a straight USB-C plug
STAND_BASE_T = 6.0


def desk_stand():
    """Base + post + 3 upward fingers. The base reaches forward under the camera
    (centre of mass) and leaves room for the USB-C cable to drop down in front of the post."""
    o = GP_T + GP_GAP
    post_top = AXIS_Y - GP_REACH
    base_top = post_top - STAND_POST_H
    post_w, post_d = 3 * GP_T + 2 * GP_GAP, 2 * GP_R
    stand = Pos(ce.RAIL_CX, (base_top + post_top) / 2, AXIS_Z) * Box(post_w, post_top - base_top + 0.02, post_d)
    stand = fillet(stand.edges().filter_by(Axis.Y), radius=3.0)
    for dx in (-o, 0.0, o):
        stand += finger(ce.RAIL_CX + dx, post_top - 0.01, AXIS_Z, +1)
    # nut trap on the +x outer finger: block standing on the post, M5 hex pocket open outwards
    nx = ce.RAIL_CX + o + GP_T / 2
    nut_blk = Pos(nx + 2.3, (post_top + AXIS_Y + 6.0) / 2 - 0.01, AXIS_Z) * Box(4.6, AXIS_Y + 6.0 - post_top, 2 * GP_R - 2.0)
    stand += fillet(nut_blk.edges().filter_by(Axis.X).group_by(Axis.Y)[-1], radius=2.5)
    stand -= Pos(nx + 0.8, AXIS_Y, AXIS_Z) * Rot(0, 90, 0) * hex_prism(8.3, 4.0)
    stand -= Pos(nx - 1.0, AXIS_Y, AXIS_Z) * Rot(0, 90, 0) * Cylinder(radius=GP_HOLE / 2, height=8.0,
                                                                     align=(Align.CENTER, Align.CENTER, Align.MIN))
    # base
    bz0 = AXIS_Z - GP_R - 10.0
    bz1 = ce.Z_FRONT + 12.0
    base = Pos(ce.RAIL_CX, base_top + 0.01, (bz0 + bz1) / 2) * Box(
        78.0, STAND_BASE_T, bz1 - bz0, align=(Align.CENTER, Align.MAX, Align.CENTER))
    base = fillet(base.edges().filter_by(Axis.Y), radius=12.0)
    base = chamfer(base.edges().group_by(Axis.Y)[-1], length=1.2)
    stand += base
    # cable clip on the back of the post (USB-C cable up to 4.3 mm)
    cy = base_top + 2.0 + 3.5
    cz = AXIS_Z - GP_R
    clip = Pos(ce.RAIL_CX, cy, cz - 3.2) * Box(12.0, 7.0, 6.4)
    clip -= Pos(ce.RAIL_CX, cy, cz - 3.6) * Rot(90, 0, 0) * Cylinder(radius=2.15, height=10)
    clip -= Pos(ce.RAIL_CX, cy, cz - 6.2) * Box(3.0, 10, 3.0)
    stand += clip
    return stand


def thumb_knob():
    k = Cylinder(radius=9.0, height=7.0, align=(Align.CENTER, Align.CENTER, Align.MIN))
    for i in range(10):
        k -= Rot(0, 0, i * 36) * Pos(9.7, 0, -1) * Cylinder(radius=1.6, height=10, align=(Align.CENTER, Align.CENTER, Align.MIN))
    k = chamfer(k.edges().group_by(Axis.Z)[-1], length=0.6)
    k -= Pos(0, 0, 3.0) * hex_prism(8.2, 5.0)                                  # M5 hex head
    k -= Pos(0, 0, -1) * Cylinder(radius=GP_HOLE / 2, height=10, align=(Align.CENTER, Align.CENTER, Align.MIN))
    return k


TRIPOD_DROP = 18.0       # foot sits this far below the camera's bottom edge (cables go down in front)


def tripod_adapter():
    """Plate + tongue, and an L-foot behind the camera with a side-loaded 1/4"-20 nut.
    The thread points down, so the camera stands upright on a level tripod head; the
    screw axis sits ~21 mm behind the USB plugs, so platforms up to ~40 mm across stay clear."""
    plate_t = 3.0
    foot_y0 = T_Y0 - TRIPOD_DROP            # underside (tripod face)
    a = railed_plate(26.0, plate_t, y0=foot_y0, latch="root_below")         # printed upright
    zb = ce.Z_BACK - plate_t
    foot_d, foot_h = 28.0, 9.0
    foot = Pos(ce.RAIL_CX, foot_y0, zb - foot_d / 2 + 0.01) * Box(26.0, foot_h, foot_d, align=(Align.CENTER, Align.MIN, Align.CENTER))
    a += fillet(foot.edges().filter_by(Axis.Y).group_by(Axis.Z)[0], radius=3.0)
    a += gusset(foot_y0 + foot_h - 0.01, zb, foot_d * 0.6, foot_d * 0.6, 26.0)
    # nut: 1/4"-20 hex (11.11 mm AF, 5.56 mm), slid in from the +x side above a 1.5 mm floor
    # (a 4.5 mm tripod screw still gets ~2.5 turns in the nut)
    nz = zb - foot_d / 2
    floor = 1.5
    nut_af, nut_t = 11.35, 5.9
    slot = Pos(ce.RAIL_CX + 7.0, foot_y0 + floor, nz) * Box(14.0 + 0.2, nut_t, nut_af,
                                                             align=(Align.CENTER, Align.MIN, Align.CENTER))
    a -= slot
    a -= Pos(ce.RAIL_CX, foot_y0 + floor, nz) * Rot(-90, 0, 0) * hex_prism(nut_af, nut_t)
    a -= Pos(ce.RAIL_CX, foot_y0 - 1.0, nz) * Rot(-90, 0, 0) * Cylinder(radius=3.4, height=foot_h + 2.0,
                                                                      align=(Align.CENTER, Align.CENTER, Align.MIN))
    return a


WALL_HOLE_DX = 13.0      # screws either side of the tongue (resist the camera's tilting moment)


def wall_plate():
    plate_t = 3.0
    a = railed_plate(2 * (WALL_HOLE_DX + 6.0), plate_t, latch=None)   # printed flat: no finger to hang in the air
    y = (T_Y0 + T_Y1) / 2
    for dx in (-WALL_HOLE_DX, WALL_HOLE_DX):
        x = ce.RAIL_CX + dx
        a -= Pos(x, y, ce.Z_BACK - plate_t - 1) * Cylinder(radius=2.1, height=10, align=(Align.CENTER, Align.CENTER, Align.MIN))
        # countersunk from the camera side: the heads end flush with the plate, under the camera's back
        a -= Pos(x, y, ce.Z_BACK - 2.2) * Cone(bottom_radius=2.1, top_radius=4.3, height=2.21,
                                               align=(Align.CENTER, Align.CENTER, Align.MIN))
    return a


MAGNET_D, MAGNET_T = 10.0, 3.0      # 4 x neodymium discs 10 x 3 mm, glued in from the back


MAGNET_DX = 14.6                    # pockets clear of the tongue's flanks (half-width 9.1 at its top)


def magnet_plate():
    """Sticks to steel (a fridge, a metal shelf, a PC case); the camera hangs on it like on the
    wall plate. The magnets drop into pockets on the camera side, beside the tongue and hidden
    under the camera's back, and pull against a 0.6 mm skin: no glue is loaded when the plate
    comes off the steel."""
    skin = 0.6
    plate_t = MAGNET_T + skin + 0.1
    w = 2 * (MAGNET_DX + MAGNET_D / 2 + 2.3)
    a = railed_plate(w, plate_t, latch=None)
    zb = ce.Z_BACK - plate_t
    for dx in (-MAGNET_DX, MAGNET_DX):
        for y in (T_Y0 + 6.6, T_Y1 - 6.6):
            a -= Pos(ce.RAIL_CX + dx, y, zb + skin) * Cylinder(radius=MAGNET_D / 2 + 0.15, height=plate_t - skin + 0.01,
                                                               align=(Align.CENTER, Align.CENTER, Align.MIN))
    return a


# rotation that brings each part into its print orientation (applied before dropping to z = 0)
PRINT_ORIENTATION = {
    "CoralEye_Mount_GoPro": Rot(-90, 0, 0),      # upside down: fingers up, standing on the upper gusset
    "CoralEye_Stand": Rot(90, 0, 0),             # base on the bed
    "CoralEye_Stand_Knob": Rot(0, 0, 0),
    "CoralEye_Mount_Tripod": Rot(90, 0, 0),      # foot on the bed
    "CoralEye_Mount_Wall": Rot(0, 0, 0),         # flat, plate on the bed, tongue up
    "CoralEye_Mount_Magnet": Rot(0, 0, 0),       # flat, steel side on the bed, magnet pockets and tongue up
}


def all_parts():
    return {
        "CoralEye_Mount_GoPro": gopro_adapter(),
        "CoralEye_Stand": desk_stand(),
        "CoralEye_Stand_Knob": thumb_knob(),
        "CoralEye_Mount_Tripod": tripod_adapter(),
        "CoralEye_Mount_Wall": wall_plate(),
        "CoralEye_Mount_Magnet": magnet_plate(),
    }


def to_print(name, part):
    p = PRINT_ORIENTATION[name] * part
    bb = p.bounding_box()
    return Pos(-(bb.min.X + bb.max.X) / 2, -(bb.min.Y + bb.max.Y) / 2, -bb.min.Z) * p


def export_all(render_folder=None):
    os.makedirs(OUT_DIR, exist_ok=True)
    parts = all_parts()
    for name, p in parts.items():
        export_step(p, os.path.join(OUT_DIR, name + ".step"))
        export_stl(to_print(name, p), os.path.join(OUT_DIR, name + ".stl"), tolerance=0.01, angular_tolerance=0.15)
        if render_folder:
            export_stl(p, os.path.join(render_folder, name + ".stl"), tolerance=0.01, angular_tolerance=0.15)
        bb = p.bounding_box()
        print(f"{name:24s} {p.volume / 1000:5.1f} cm3  size {bb.size.X:.1f} x {bb.size.Y:.1f} x {bb.size.Z:.1f}")
    return parts


if __name__ == "__main__":
    import sys
    rf = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "render", "meshes")) \
        if "--render-meshes" in sys.argv else None
    export_all(rf)
