"""
Porti — sarhoş tekerli küçük robot arabanın basılabilir parçaları (N20 enkoderli motorlu sürüm).

Tek kaynak: bütün ölçüler aşağıdaki P sözlüğünde. Kendi parçanı kumpasla ölç, değiştir, tekrar çalıştır:

    pip install build123d
    python porti.py            # STL'leri ../../public/robotics/porti/ altına yazar

Koordinatlar (montaj): x ileri, y sola, z yukarı, birim mm, zemin z=0, teker aksı x=0.
Her parça iki kez yazılır:
  - <ad>.stl         : montajdaki yerinde (sitedeki 3B görüntüleyici bunu yükler)
  - print/<ad>.stl   : baskı yönünde, tablaya oturmuş (Bambu Studio'ya bunu at)
"""

import json
import math
from pathlib import Path

from build123d import (
    Axis,
    Box,
    BuildPart,
    BuildSketch,
    Cylinder,
    GeomType,
    Locations,
    Mode,
    Plane,
    Pos,
    RectangleRounded,
    Rot,
    Sphere,
    Torus,
    export_stl,
    extrude,
    fillet,
)

P = dict(
    # motor: JGA12-N20B 12 V 300 RPM enkoderli
    n20_w=12.0,  # dişli kutusu genişliği (yuvarlak taraf)
    n20_h=10.0,  # dişli kutusu yüksekliği (yassı taraflar arası)
    n20_len=34.0,  # dişli kutusu + motor + enkoder
    shaft_d=3.0,  # D mil
    shaft_flat=2.5,
    shaft_len=10.0,
    # tekerlek
    wheel_d=63.0,  # lastik dahil dış çap
    wheel_w=18.0,
    rim_d=51.0,  # jant dış çapı; TPU lastik bunun üstüne geçer
    wheel_gap=0.8,
    # şasi
    plate_front=70.0,  # aksın önündeki uzunluk: teker gövdenin ortasında
    plate_back=80.0,  # aksın arkasındaki uzunluk
    plate_w=110.0,
    plate_t=3.0,
    corner_r=22.0,
    clamp_floor=1.5,  # motor kelepçesinin taban kalınlığı
    caster_x=-62.0,
    caster_hole_spacing=30.0,  # sarhoş tekerin iki vida deliği; yarıklar ±6 mm tolere eder
    # üst kat (breadboard + ESP32)
    deck_z=49.0,  # üst katın alt yüzü (pilin üstünden geçer)
    deck_t=3.0,
    # kabuk
    shell_h=52.0,
    shell_wall=2.2,
    shell_r=18.0,
    eye_z=50.0,  # ultrasonik "gözlerin" merkezi (yerden)
    eye_d=17.0,
    eye_gap=26.0,  # HC-SR04 iki dönüştürücü merkezi arası
    m3=3.3,  # geçme delik
    m3_tap=2.8,  # plastiğe doğrudan M3 vida
)

OUT = Path(__file__).resolve().parents[2] / "public" / "robotics" / "porti"

R = P["wheel_d"] / 2  # aks yüksekliği
PLATE_Z = R - P["n20_h"] / 2 - P["clamp_floor"] - P["plate_t"]  # plakanın alt yüzü = sarhoş teker yüksekliği
TOP = PLATE_Z + P["plate_t"]
YB = P["plate_w"] / 2  # kelepçenin dış yüzü = plaka kenarı = dişli kutusu yüzü
WHEEL_Y = YB + P["wheel_gap"] + P["wheel_w"] / 2
PLATE_LEN = P["plate_front"] + P["plate_back"]
PLATE_CX = (P["plate_front"] - P["plate_back"]) / 2
FRONT = PLATE_CX + PLATE_LEN / 2
BACK = PLATE_CX - PLATE_LEN / 2
CLAMP_LEN = 16.0  # kelepçe boyu (y): dişli kutusunu ve motorun bir kısmını sarar

# vida noktaları: plaka ve parçalar aynı listeyi kullanır
CLAMP_HOLES = [(sx * 14.0, sy * (YB - CLAMP_LEN / 2)) for sx in (1, -1) for sy in (1, -1)]
DECK_LEGS = [(-64.0, 48.5), (-64.0, -48.5), (-26.0, 48.5), (-26.0, -48.5)]
SHELL_HOLES = [(-77.0, 0.0), (52.0, 42.0), (52.0, -42.0)]
EYE_HOLES = [(59.0, -14.0), (59.0, 14.0)]
IMU_HOLES = [(26.0, 11.0), (26.0, 26.0)]


def holes(points, d, z=0.0, h=40.0):
    """Verilen noktalarda dikey delik (BuildPart içinde)."""
    with Locations(*[(x, y, z) for x, y in points]):
        Cylinder(d / 2, h, mode=Mode.SUBTRACT)


# --------------------------------------------------------------------------- şasi
def plate():
    with BuildPart() as p:
        with BuildSketch(Plane.XY.offset(PLATE_Z)):
            with Locations((PLATE_CX, 0)):
                RectangleRounded(PLATE_LEN, P["plate_w"], P["corner_r"])
        extrude(amount=P["plate_t"])
        holes(CLAMP_HOLES, P["m3"])
        holes(DECK_LEGS, P["m3"])
        holes(SHELL_HOLES, P["m3"])
        holes(EYE_HOLES, P["m3"])
        holes(IMU_HOLES, P["m3_tap"])
        # sarhoş teker: iki yarık
        with Locations(*[(P["caster_x"], s * P["caster_hole_spacing"] / 2) for s in (1, -1)]):
            Box(P["m3"], P["m3"] + 12, 40, mode=Mode.SUBTRACT)
        # pil yuvasını tutan iki cırt bant için dört yarık
        with Locations(*[(x, s * 41.5) for x in (-56, -30) for s in (1, -1)]):
            Box(20, 4, 40, mode=Mode.SUBTRACT)
        # ortadan motor kabloları üst kata geçsin
        with Locations((-8, 0)):
            Box(5, 24, 40, mode=Mode.SUBTRACT)
    return p.part


# --------------------------------------------------------------------------- motor kelepçesi
def clamp(side=1):
    """N20 için tünel: motoru içeriden it, dişli kutusu ön duvara dayanır, mil delikten çıkar.
    İki ayaktan alttan M3 ile plakaya vidalanır."""
    w, h = P["n20_w"] + 0.3, P["n20_h"] + 0.3
    wall = 2.0
    y0, y1 = YB - CLAMP_LEN, YB
    zc = R  # motor ekseni
    body_h = zc + h / 2 + wall - TOP
    with BuildPart() as p:
        with Locations((0, side * (y0 + y1) / 2, TOP + body_h / 2)):
            Box(w + 2 * wall, CLAMP_LEN, body_h)
        for sx in (1, -1):  # ayaklar
            with Locations((sx * 14.0, side * (y0 + y1) / 2, TOP + 1.5)):
                Box(10, CLAMP_LEN, 3)
        # tünel: ön duvar 1.5 mm kalır
        tunnel_len = CLAMP_LEN - 1.5
        with Locations((0, side * (y0 + tunnel_len / 2 - 1), zc)):
            Box(w, tunnel_len + 2, h, mode=Mode.SUBTRACT)
        # mil ve dişli kutusu göbeği için delik
        with Locations(Pos(0, side * (YB - 0.75), zc) * Rot(90, 0, 0)):
            Cylinder(2.6, 4, mode=Mode.SUBTRACT)
        # üstten kablo bağı yarığı
        with Locations((0, side * (y0 + 5), zc + h / 2 + wall / 2)):
            Box(w + 2 * wall + 2, 3, wall + 0.2, mode=Mode.SUBTRACT)
        holes([pt for pt in CLAMP_HOLES if pt[1] * side > 0], P["m3_tap"], z=TOP)
    return p.part


# --------------------------------------------------------------------------- tekerlek
def rim(side=1):
    """Jant: 5 yuvarlak boşluklu disk, D delikli göbek, radyal M3 setuskur. Lastik ayrıca TPU."""
    w = P["wheel_w"]
    rr = P["rim_d"] / 2
    with BuildPart() as p:
        Cylinder(rr, w)
        Cylinder(rr - 2.2, w - 3.6, mode=Mode.SUBTRACT)  # lastik oluğu (iki dudak arası)
        Cylinder(rr - 6, w, mode=Mode.SUBTRACT)  # içi boş
        with Locations((0, 0, w / 2 - 1.75)):
            Cylinder(rr - 5, 3.5)  # dış yüzdeki disk
        for i in range(5):
            a = i * 2 * math.pi / 5 + math.pi / 2
            with Locations((13.0 * math.cos(a), 13.0 * math.sin(a), w / 2 - 2)):
                Cylinder(5.4, 6, mode=Mode.SUBTRACT)
        Cylinder(6.5, w)  # göbek
        Cylinder(P["shaft_d"] / 2 + 0.1, w + 2, mode=Mode.SUBTRACT)
    part = p.part
    flat = P["shaft_flat"] - P["shaft_d"] / 2 + 0.05  # merkezden D düzlüğüne
    part = part + (Pos(flat + 1.5, 0, 0) * Box(3, 5, w) & Cylinder(P["shaft_d"] / 2 + 0.3, w))
    # setuskur: D düzlüğüne bassın, iç yüze 4 mm
    part = part - Pos(5, 0, -w / 2 + 4) * Rot(0, 90, 0) * Cylinder(P["m3_tap"] / 2, 10)
    return Pos(0, side * WHEEL_Y, R) * Rot(side * -90, 0, 0) * part


def tire(side=1):
    w = P["wheel_w"] - 4.0
    ro = P["wheel_d"] / 2
    ri = P["rim_d"] / 2 - 2.2 + 0.2  # oluğa 0.2 mm sıkı geçer
    with BuildPart() as p:
        Cylinder(ro, w)
        Cylinder(ri, w, mode=Mode.SUBTRACT)
        outer = [e for e in p.edges() if e.geom_type == GeomType.CIRCLE and e.radius > ro - 0.1]
        fillet(outer, 2.5)
    t = p.part
    for i in range(28):
        a = i * 2 * math.pi / 28
        t = t - Pos(ro * math.cos(a), ro * math.sin(a), 0) * Rot(0, 0, math.degrees(a)) * Box(1.6, 1.6, w + 2)
    return Pos(0, side * WHEEL_Y, R) * Rot(side * -90, 0, 0) * t


# --------------------------------------------------------------------------- üst kat
def deck():
    """Pilin üstünden geçen kat: breadboard + ESP32 + TB6612 buraya. Dört bacağı alttan M3 ile."""
    z0, t = P["deck_z"], P["deck_t"]
    with BuildPart() as p:
        with BuildSketch(Plane.XY.offset(z0)):
            with Locations((-28, 0)):
                RectangleRounded(96, 100, 12)
        extrude(amount=t)
        for x, y in DECK_LEGS:
            with Locations((x, y, (TOP + z0) / 2)):
                Cylinder(3.5, z0 - TOP)
        holes(DECK_LEGS, P["m3_tap"], z=TOP, h=20)
        with Locations((-8, 0, z0)):  # kablo geçişi
            Box(8, 30, 20, mode=Mode.SUBTRACT)
        with Locations((-45, 40, z0), (-45, -40, z0)):  # hafifletme
            Box(30, 5, 20, mode=Mode.SUBTRACT)
    return p.part


# --------------------------------------------------------------------------- kabuk
def shell():
    """Yuvarlak hatlı kapak: önde iki göz (HC-SR04), altında gülümseme, yanlarda teker yuvası,
    arkada USB deliği, üstte anahtar ve anten. Tabanı açık, 3 iç kulakla plakaya vidalanır."""
    L, W, H = PLATE_LEN - 1, P["plate_w"] - 1, P["shell_h"]
    wall = P["shell_wall"]

    def rounded_block(l, w, h, r_plan, r_top, z0):
        with BuildPart() as b:
            with BuildSketch(Plane.XY.offset(z0)):
                with Locations((PLATE_CX, 0)):
                    RectangleRounded(l, w, r_plan)
            extrude(amount=h)
            fillet(b.edges().group_by(Axis.Z)[-1], r_top)
        return b.part

    outer = rounded_block(L, W, H, P["corner_r"] - 0.5, P["shell_r"], TOP)
    inner = rounded_block(L - 2 * wall, W - 2 * wall, H - wall + 1, P["corner_r"] - 0.5 - wall, P["shell_r"] - wall, TOP - 1)
    body = outer - inner

    front = PLATE_CX + L / 2
    for s in (1, -1):  # gözler
        body = body - Pos(front - 3, s * P["eye_gap"] / 2, P["eye_z"]) * Rot(0, 90, 0) * Cylinder(P["eye_d"] / 2, 20)
    smile = Pos(front, 0, P["eye_z"] - 12) * Rot(0, 90, 0) * Torus(7, 1.2)  # gülümseme
    smile = smile - Pos(front, 0, P["eye_z"] - 12 + 10) * Box(10, 30, 20)
    body = body - smile
    for s in (1, -1):  # yanaklar: sığ çukurlar
        body = body - Pos(front + 2.8, s * 27, P["eye_z"] - 11) * Sphere(3.6)
    for s in (1, -1):  # teker yuvaları: kelepçe ve mil buradan çıkar
        body = body - Pos(0, s * W / 2, TOP + 8) * Box(28, 12, 16)
        body = body - Pos(0, s * W / 2, TOP + 16) * Rot(90, 0, 0) * Cylinder(14, 12)
    back = PLATE_CX - L / 2
    body = body - Pos(back, 0, P["deck_z"] + 20) * Box(12, 14, 9)  # USB kablosu ESP32'ye
    body = body - Pos(-52, 34, TOP + H) * Box(19.4, 13.2, 10)  # KCD1 anahtar (üstte, arkada)
    body = body - Pos(40, 0, TOP + H) * Cylinder(4, 10)  # anten / LED penceresi
    for x, y in SHELL_HOLES:  # iç vida kulakları
        ear = Pos(x, y, TOP + 3) * Box(12, 12, 6) - Pos(x, y, TOP + 3) * Cylinder(P["m3_tap"] / 2, 10)
        body = body + (ear & outer)
    return body


def antenna():
    """Kabuğun üstündeki küçük top: beyaz/şeffaf PETG bas, altına LED gelsin."""
    z = TOP + P["shell_h"]
    top = Pos(40, 0, z + 5) * Sphere(6) + Pos(40, 0, z - 2) * Cylinder(3.8, 8)
    return top - Pos(40, 0, z - 4) * Cylinder(2.6, 12)


def eye_mount():
    """HC-SR04'ü dikey tutar; dönüştürücüler kabuktaki gözlerden dışarı bakar."""
    t = 3.0
    x = FRONT - 0.5 - P["shell_wall"] - 2.5  # tutucu plakanın ön yüzü
    hz = P["eye_z"] - TOP + 13
    with BuildPart() as p:
        with Locations((x - t / 2, 0, TOP + hz / 2)):
            Box(t, 50, hz)
        with Locations(((x + 54) / 2, 0, TOP + 1.5)):
            Box(x - 54, 50, 3)
        for s in (1, -1):
            with Locations(Pos(x - t / 2, s * P["eye_gap"] / 2, P["eye_z"]) * Rot(0, 90, 0)):
                Cylinder(8.6, 10, mode=Mode.SUBTRACT)
        holes(EYE_HOLES, P["m3"], z=TOP)
    return p.part


def caster_spacer(h):
    with BuildPart() as p:
        with Locations((P["caster_x"], 0, PLATE_Z - h / 2)):
            Box(26, P["caster_hole_spacing"] + 18, h)
        with Locations(*[(P["caster_x"], s * P["caster_hole_spacing"] / 2, PLATE_Z - h / 2) for s in (1, -1)]):
            Box(P["m3"], P["m3"] + 12, h + 2, mode=Mode.SUBTRACT)
    return p.part


# --------------------------------------------------------------------------- dışa aktar
def to_print(part, rot=(0, 0, 0)):
    """Baskı yönüne çevir ve tablaya oturt (z min = 0, xy merkez)."""
    s = Rot(*rot) * part
    bb = s.bounding_box()
    return Pos(-(bb.min.X + bb.max.X) / 2, -(bb.min.Y + bb.max.Y) / 2, -bb.min.Z) * s


def main():
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "print").mkdir(exist_ok=True)
    parts = {
        # ad: (parça, baskı yönü)
        "sasi": (plate(), (0, 0, 0)),
        "motor-kelepcesi-sol": (clamp(1), (0, 0, 0)),
        "motor-kelepcesi-sag": (clamp(-1), (0, 0, 0)),
        "jant-sol": (rim(1), (-90, 0, 0)),
        "jant-sag": (rim(-1), (90, 0, 0)),
        "lastik-sol": (tire(1), (90, 0, 0)),
        "lastik-sag": (tire(-1), (-90, 0, 0)),
        "ust-kat": (deck(), (180, 0, 0)),
        "kabuk": (shell(), (180, 0, 0)),
        "anten": (antenna(), (0, 0, 0)),
        "goz-tutucu": (eye_mount(), (0, 0, 0)),
        "sarhos-ara-2mm": (caster_spacer(2), (0, 0, 0)),
    }
    info = {}
    for name, (part, rot) in parts.items():
        export_stl(part, str(OUT / f"{name}.stl"), tolerance=0.1, angular_tolerance=0.3)
        export_stl(to_print(part, rot), str(OUT / "print" / f"{name}.stl"), tolerance=0.03, angular_tolerance=0.15)
        bb = part.bounding_box()
        info[name] = dict(size=[round(bb.size.X, 1), round(bb.size.Y, 1), round(bb.size.Z, 1)], cm3=round(part.volume / 1000, 1))
        print(f"{name:20s} {bb.size.X:6.1f} × {bb.size.Y:6.1f} × {bb.size.Z:6.1f} mm  {part.volume / 1000:6.1f} cm³")
    geo = dict(R=R, plateZ=PLATE_Z, top=TOP, yb=YB, wheelY=WHEEL_Y, front=FRONT, back=BACK, plateCx=PLATE_CX, **P)
    (OUT / "porti.json").write_text(json.dumps(dict(parts=info, geo=geo), indent=1))
    print(f"aks {R:.1f} · plaka altı {PLATE_Z:.1f} mm (sarhoş teker yüksekliği) · iz {2 * WHEEL_Y:.1f} · boy {PLATE_LEN:.0f}")


if __name__ == "__main__":
    main()
