#!/usr/bin/env python3
"""Generate final box parts: lid XY-only, base with Z limits."""
import struct, numpy as np, os, shutil

ORIG = "/app/projects/esp32-box/original"
OUT = "/app/projects/esp32-box/exports"
os.makedirs(OUT, exist_ok=True)

def load_stl(path):
    with open(path, 'rb') as f:
        f.read(80)
        n = struct.unpack('<I', f.read(4))[0]
        verts = np.zeros((n*3, 3), dtype=np.float64)
        for i in range(n):
            f.read(12)
            for j in range(3):
                verts[i*3+j] = np.frombuffer(f.read(12), dtype=np.float32)
            f.read(2)
    return verts, n

def save_stl(verts, path):
    n = len(verts)//3
    with open(path, 'wb') as f:
        f.write(b'\x00'*80)
        f.write(struct.pack('<I', n))
        for i in range(n):
            f.write(struct.pack('<fff', 0, 0, 0))
            for j in range(3):
                v = verts[i*3+j]
                f.write(struct.pack('<fff', float(v[0]), float(v[1]), float(v[2])))
            f.write(b'\x00\x00')

def info(p):
    with open(p, 'rb') as f:
        f.read(80)
        n = struct.unpack('<I', f.read(4))[0]
        x1=y1=z1=9e9; x2=y2=z2=-9e9
        for i in range(n):
            f.read(12)
            for j in range(3):
                x,y,z = struct.unpack('<fff', f.read(12))
                x1,x2 = min(x1,x), max(x2,x)
                y1,y2 = min(y1,y), max(y2,y)
                z1,z2 = min(z1,z), max(z2,z)
            f.read(2)
    return n, x2-x1, y2-y1, z2-z1

def stretch_xy(v, cx, cy, nw, nd):
    x0,x1 = v[:,0].min(), v[:,0].max()
    y0,y1 = v[:,1].min(), v[:,1].max()
    n = len(v)//3
    xs = (nw-2*cx) / (60-2*cx) if (60-2*cx) > 0 else 1
    ys = (nd-2*cy) / (60-2*cy) if (60-2*cy) > 0 else 1
    r = v.copy()
    for idx in range(n*3):
        x,y,z = v[idx]
        xn = x if x < x0+cx else (x+nw-60) if x > x1-cx else x0+cx + (x-x0-cx)*xs
        yn = y if y < y0+cy else (y+nd-60) if y > y1-cy else y0+cy + (y-y0-cy)*ys
        r[idx] = [xn, yn, z]
    return r

def stretch_xyz(v, cx, cy, ow, od, oh, nw, nd, nh, keep_bot, keep_top):
    x0,x1 = v[:,0].min(), v[:,0].max()
    y0,y1 = v[:,1].min(), v[:,1].max()
    z0,z1 = v[:,2].min(), v[:,2].max()
    n = len(v)//3
    xs = (nw-2*cx) / (ow-2*cx) if (ow-2*cx) > 0 else 1
    ys = (nd-2*cy) / (od-2*cy) if (od-2*cy) > 0 else 1
    mo = oh - keep_bot - keep_top
    mn = nh - keep_bot - keep_top
    zs = mn/mo if mo > 0 else 1
    r = v.copy()
    for idx in range(n*3):
        x,y,z = v[idx]
        xn = x if x < x0+cx else (x+nw-ow) if x > x1-cx else x0+cx + (x-x0-cx)*xs
        yn = y if y < y0+cy else (y+nd-od) if y > y1-cy else y0+cy + (y-y0-cy)*ys
        if z < z0 + keep_bot:
            zn = z
        elif z > z1 - keep_top:
            zn = z + nh - oh
        else:
            zn = z0 + keep_bot + (z - z0 - keep_bot) * zs
        r[idx] = [xn, yn, zn]
    return r

ORIG_DIR = "/app/projects/esp32-box/original"
OUT_DIR = "/app/projects/esp32-box/exports"

print("BASE: X=80, Y=70, Z stretch 10.14-15.2mm...")
v,n = load_stl(os.path.join(ORIG_DIR, "Box.stl"))
r = stretch_xyz(v, 20, 20, 60, 60, 17, 80, 70, 32, 10.14, 1.8)
save_stl(r, os.path.join(OUT_DIR, "base.stl"))
nw,xd,yd,zd = info(os.path.join(OUT_DIR, "base.stl"))
print("  %.1fx%.1fx%.1f (%d tri)" % (xd, yd, zd, nw))

print("LID: X=80, Y=70, Z=10 (original)...")
v,n = load_stl(os.path.join(ORIG_DIR, "Lid.stl"))
r = stretch_xy(v, 20, 20, 80, 70)
save_stl(r, os.path.join(OUT_DIR, "lid.stl"))
nw,xd,yd,zd = info(os.path.join(OUT_DIR, "lid.stl"))
print("  %.1fx%.1fx%.1f (%d tri)" % (xd, yd, zd, nw))

print("WINDOWS (custom)...")
# Simple Build123d window creation
from build123d import Box, Pos, Cylinder, export_stl

def make_window(width, height, thick, cutouts=None):
    panel = Box(width, thick, height)
    for side in [-1, 1]:
        tab = Pos(0, side*(thick/2-0.5), 0) * Box(width-2, 1.0, height+0.5)
        panel = panel + tab
    if cutouts:
        for cx, cy, cw, ch in cutouts:
            panel = panel - Pos(cx, 0, cy) * Box(cw, thick+0.4, ch)
    return panel

WIN_THICK = 4.8
WIN_H = 23.0

sx = make_window(36.0, WIN_H, WIN_THICK)
export_stl(sx, os.path.join(OUT_DIR, "window_smooth.stl"))
print("  window_smooth: 36x%.1fx%.1f" % (WIN_THICK, WIN_H))

kc = make_window(36.0, WIN_H, WIN_THICK, [(-9, 0, 17, WIN_THICK+0.5), (9, 0, 9, 9)])
export_stl(kc, os.path.join(OUT_DIR, "window_keystone_dc.stl"))
print("  window_keystone_dc: 36x%.1fx%.1f" % (WIN_THICK, WIN_H))

sy = make_window(26.0, WIN_H, WIN_THICK)
export_stl(sy, os.path.join(OUT_DIR, "window_smooth_y.stl"))
print("  window_smooth_y: 26x%.1fx%.1f" % (WIN_THICK, WIN_H))

usb = make_window(26.0, WIN_H, WIN_THICK, [(0, 0, 9, 5)])
export_stl(usb, os.path.join(OUT_DIR, "window_usb.stl"))
print("  window_usb: 26x%.1fx%.1f" % (WIN_THICK, WIN_H))

print("SCREW (original)...")
shutil.copy2(os.path.join(ORIG_DIR, "Screw.stl"), os.path.join(OUT_DIR, "screw.stl"))
nw,xd,yd,zd = info(os.path.join(OUT_DIR, "screw.stl"))
print("  %.1fx%.1fx%.1f (original)" % (xd, yd, zd))

print("\nDONE")
