import trimesh
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import os

OUTPUT_DIR = "/DATA/AppData/hermes/Projekte/3d"
os.makedirs(OUTPUT_DIR, exist_ok=True)
NAME = "my_part"

# ─── PARAMETERS ────────────────────────────────────────────
WIDTH = 40
DEPTH = 30
HEIGHT = 20
WALL = 2.0
# ───────────────────────────────────────────────────────────

# ─── HELPERS ───────────────────────────────────────────────
def make_box(w, d, h, center=(0, 0, 0)):
    m = trimesh.creation.box(extents=[w, d, h])
    m.apply_translation(center)
    return m

def make_cylinder(r, h, center=(0, 0, 0), axis='z', sections=64):
    m = trimesh.creation.cylinder(radius=r, height=h, sections=sections)
    if axis == 'x':
        m.apply_transform(trimesh.transformations.rotation_matrix(np.pi/2, [0,1,0]))
    elif axis == 'y':
        m.apply_transform(trimesh.transformations.rotation_matrix(np.pi/2, [1,0,0]))
    m.apply_translation(center)
    return m

def drill(mesh, x, y, diameter, axis='z'):
    idx = {'x':0, 'y':1, 'z':2}[axis]
    length = mesh.extents[idx] * 1.5
    cyl = make_cylinder(diameter/2, length, (x, y, 0), axis=axis)
    return mesh.difference(cyl)

def chamfer_inner_top(mesh, inner_l, inner_w, h=2.0):
    """Simple chamfer at inner top opening — subtracts a slightly larger box at the top edge.
    WARNING: This only works reliably on CLOSED solid meshes. On open-top boxes,
    the boolean may silently fail (volume unchanged, watertight=True, but no geometry change).
    For open shells, build the chamfer into the mesh at construction time instead.
    h = chamfer depth in mm (also the overhang in x and y)."""
    zt = mesh.bounds[1][2]
    chamfer_box = make_box(inner_l + 2*h, inner_w + 2*h, h,
                           center=(0, 0, zt - h/2))
    result = mesh.difference(chamfer_box)
    # Validate: check if volume actually changed
    if abs(result.volume - mesh.volume) < 0.01:
        print("WARN: chamfer_inner_top produced no geometry change — boolean failed silently")
    return result

def _chamfer_prism(l, w, h, setback, center=(0, 0, 0)):
    """Build a chamfer prism (truncated pyramid) that can be subtracted from a closed solid.
    Bottom face is l×w, top face is (l-2*setback)×(w-2*setback), height=h.
    45° chamfer when setback == h."""
    cb_l, cb_w = l, w
    ct_l, ct_w = l - 2*setback, w - 2*setback
    verts = np.array([
        [-cb_l/2, -cb_w/2, 0],
        [ cb_l/2, -cb_w/2, 0],
        [ cb_l/2,  cb_w/2, 0],
        [-cb_l/2,  cb_w/2, 0],
        [-ct_l/2, -ct_w/2, h],
        [ ct_l/2, -ct_w/2, h],
        [ ct_l/2,  ct_w/2, h],
        [-ct_l/2,  ct_w/2, h],
    ], dtype=np.float64)
    faces = np.array([
        [0,1,5], [0,5,4],
        [1,2,6], [1,6,5],
        [2,3,7], [2,7,6],
        [3,0,4], [3,4,7],
        [4,5,6], [4,6,7],
        [0,3,2], [0,2,1],
    ])
    m = trimesh.Trimesh(vertices=verts, faces=faces)
    m.apply_translation(center)
    return m

def open_top_box_chamfer45(w, d, h, wall, floor, chamfer_h=2.0):
    """Open-top box with a REAL 45° inner-top chamfer.
    Two-stage boolean: solid outer minus straight cavity minus chamfer prism.
    Only works reliably because step1 (outer - cavity) remains a closed solid."""
    outer  = make_box(w, d, h, center=(w/2, d/2, h/2))
    cav_l  = w - 2*wall
    cav_w  = d - 2*wall
    low_h  = h - floor - chamfer_h
    cavity = make_box(cav_l, cav_w, low_h,
                      center=(w/2, d/2, floor + low_h/2))
    step1  = outer.difference(cavity)
    prism  = _chamfer_prism(cav_l, cav_w, chamfer_h, setback=chamfer_h,
                            center=(w/2, d/2, h - chamfer_h))
    result = step1.difference(prism)
    # Verify chamfer actually reduced volume
    expected_loss = 2 * chamfer_h * (cav_l + cav_w) * chamfer_h / 2   # rough check
    if abs(result.volume - step1.volume) < 0.5:
        print("WARN: chamfer boolean produced negligible geometry change")
    return result
# ───────────────────────────────────────────────────────────

# ─── MODEL ─────────────────────────────────────────────────
result = make_box(WIDTH, DEPTH, HEIGHT)
# ───────────────────────────────────────────────────────────

bb = result.extents
warnings = []
if max(bb) > 180:
    warnings.append(f"Exceeds A1 Mini build volume: {bb[0]:.1f}x{bb[1]:.1f}x{bb[2]:.1f} mm")
if not result.is_watertight:
    warnings.append("Mesh not watertight — slicer may produce gaps")
if not result.is_winding_consistent:
    warnings.append("Inconsistent winding")

stl_path = f"{OUTPUT_DIR}/{NAME}.stl"
png_path = f"{OUTPUT_DIR}/{NAME}.png"
result.export(stl_path)

fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
ax.add_collection3d(Poly3DCollection(
    result.vertices[result.faces], alpha=0.85, edgecolor='k',
    linewidth=0.15, facecolor='#7aa6d8'))
v = result.vertices
ax.set_xlim(v[:,0].min(), v[:,0].max())
ax.set_ylim(v[:,1].min(), v[:,1].max())
ax.set_zlim(v[:,2].min(), v[:,2].max())
ax.set_box_aspect([np.ptp(v[:,i]) for i in range(3)])
ax.set_xlabel('X (mm)'); ax.set_ylabel('Y (mm)'); ax.set_zlabel('Z (mm)')
ax.set_title(f"{NAME} — {bb[0]:.1f}x{bb[1]:.1f}x{bb[2]:.1f} mm")
plt.tight_layout()
plt.savefig(png_path, dpi=110, bbox_inches='tight')
plt.close()

print(f"OK {NAME}")
print(f"   Size:       {bb[0]:.1f} x {bb[1]:.1f} x {bb[2]:.1f} mm")
print(f"   Volume:     {result.volume/1000:.1f} cm3")
print(f"   Watertight: {result.is_watertight}")
print(f"   STL:        {stl_path}")
for w in warnings:
    print(f"   WARN: {w}")
