"""Turn a row of wings.csv into printable geometry + a Bambu 3MF."""
import os, sys, math, json, zipfile, hashlib
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import numpy as np, trimesh
from shapely.geometry import Point
from shapely.ops import unary_union
from shapely import affinity
import tagkit as tk

P = tk.P
BOOL = 'manifold'

# Minimal project settings.  BambuStudio only reads this when the model's
# Application metadata starts with "BambuStudio-"; without it the user gets an
# "invalid config" dialog on open.  Keys chosen to preset a two-filament X1C.
PROJECT_SETTINGS = json.dumps({
    "from": "project",
    "version": "01.10.00.00",
    "name": "project_settings",
    "printer_model": "Bambu Lab X1 Carbon",
    "printer_variant": "0.4",
    "nozzle_diameter": ["0.4"],
    "filament_type": ["PLA", "PLA"],
    "filament_colour": ["#9DA3A8", "#F5D420"],
    "filament_settings_id": ["Bambu PLA Basic @BBL X1C", "Bambu PLA Basic @BBL X1C"],
    "print_settings_id": "0.20mm Standard @BBL X1C",
    "printer_settings_id": "Bambu Lab X1 Carbon 0.4 nozzle",
}, indent=1)

# ------------------------------------------------------------------ helpers
def _diff(a, b):
    return trimesh.boolean.difference([a, b], engine=BOOL)

def _union(ms):
    ms = [m for m in ms if m is not None]
    if len(ms) == 1:
        return ms[0]
    return trimesh.boolean.union(ms, engine=BOOL)

def _cyl(x, y, d, z0, z1, seg=20):
    m = trimesh.creation.cylinder(radius=d / 2.0, height=z1 - z0, sections=seg)
    m.apply_translation([x, y, (z0 + z1) / 2.0])
    return m


def chamfered(geom2d, z0, z1, cham=0.3, steps=3, clip=None):
    """Extrude geom2d from z0..z1 with a chamfer broken into the top outer edge.

    The chamfer is cut with thin ring-shaped tools around the perimeter rather
    than by re-extruding the whole artwork-holed polygon once per step.  The
    rings are ~200 points each, so the face count barely moves -- re-extruding
    the artwork tripled it.
    """
    body = tk.extrude(geom2d, z0, z1)
    ref = clip if clip is not None else geom2d
    outer = ref.buffer(1.0, join_style=1, quad_segs=8)

    cutters = []
    for i in range(1, steps + 1):
        inset = cham * i / steps
        ring = outer.difference(ref.buffer(-inset, join_style=1, quad_segs=16))
        zb = z1 - cham + cham * (i - 1) / steps
        zt = z1 - cham + cham * i / steps
        m = tk.extrude(ring, zb, zt + (0.001 if i == steps else 0.0))
        if m is not None:
            cutters.append(m)
    if not cutters:
        return body
    return _diff(body, _union(cutters))


# ------------------------------------------------------------------ halves
def build_half(name, number, hole_corner, footer='', chamfer=True):
    """Returns (base_mesh, top_mesh) for one half of a tag."""
    art = tk.build_artwork(name, number, footer)
    out, hole, _ = tk.outline(hole_corner)
    body = out.difference(hole)
    top2d = body.difference(art)

    base = tk.extrude(body, 0.0, P.base_t)
    keys = _union([_cyl(x, y, P.key_d, -0.1, P.key_depth) for x, y in P.key_positions])
    base = _diff(base, keys)

    if chamfer:
        top = chamfered(top2d, P.base_t, P.half_t, cham=0.3, steps=3, clip=out)
    else:
        top = tk.extrude(top2d, P.base_t, P.half_t)
    return base, top


def build_solid(name, number, footer=''):
    """One-piece double-sided variant: artwork on both faces, no glue."""
    art = tk.build_artwork(name, number, footer)
    art_back = affinity.scale(art, -1, 1, origin=(0, 0))     # reads right from the back
    out = tk.rrect(P.size, P.size, P.corner_r)
    hl = Point(-(P.size/2 - P.hole_inset), -(P.size/2 - P.hole_inset)).buffer(P.hole_d/2, quad_segs=18)
    hr = Point(+(P.size/2 - P.hole_inset), -(P.size/2 - P.hole_inset)).buffer(P.hole_d/2, quad_segs=18)
    body = out.difference(hl).difference(hr)

    face = 0.6
    t = P.solo_t
    back_art = body.intersection(art_back)
    front_top = body.difference(art)

    bottom_colour = _union([
        tk.extrude(body.difference(art_back), 0.0, face),
        chamfered(front_top, t - face, t, cham=0.3, steps=3, clip=out),
    ])
    base_colour = _union([
        tk.extrude(back_art, 0.0, face),
        tk.extrude(body, face, t - face),
    ])
    return base_colour, bottom_colour


# ------------------------------------------------------------------ 3MF out
_MODEL_HEAD = ('<?xml version="1.0" encoding="UTF-8"?>\n'
 '<model unit="millimeter" xml:lang="en-US" '
 'xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" '
 'xmlns:BambuStudio="http://schemas.bambulab.com/package/2021">\n'
 ' <metadata name="Application">BambuStudio-01.10.00.00</metadata>\n'
 ' <metadata name="BambuStudio:3mfVersion">1</metadata>\n'
 ' <metadata name="Title">{title}</metadata>\n'
 ' <metadata name="Designer">Verdant Ride</metadata>\n'
 ' <resources>\n')


def _mesh_xml(mesh, oid):
    v = mesh.vertices
    f = mesh.faces
    rows = ['  <object id="%d" type="model">\n   <mesh>\n    <vertices>' % oid]
    rows.append(''.join('<vertex x="%.4f" y="%.4f" z="%.4f"/>' % tuple(p) for p in v))
    rows.append('</vertices>\n    <triangles>')
    rows.append(''.join('<triangle v1="%d" v2="%d" v3="%d"/>' % tuple(t) for t in f))
    rows.append('</triangles>\n   </mesh>\n  </object>\n')
    return ''.join(rows)


def write_3mf(path, objects, title, plate_name=''):
    """objects: list of (label, [(part_label, mesh, extruder), ...], (x, y))"""
    oid = 1
    res, items, cfg = [], [], []
    for label, parts, (px, py) in objects:
        part_ids = []
        for plabel, mesh, ext in parts:
            res.append(_mesh_xml(mesh, oid))
            part_ids.append((oid, plabel, ext))
            oid += 1
        gid = oid; oid += 1
        comp = ''.join('    <component objectid="%d" transform="1 0 0 0 1 0 0 0 1 0 0 0"/>\n' % i
                       for i, _, _ in part_ids)
        res.append('  <object id="%d" type="model">\n   <components>\n%s   </components>\n  </object>\n'
                   % (gid, comp))
        items.append('  <item objectid="%d" transform="1 0 0 0 1 0 0 0 1 %.4f %.4f 0" printable="1"/>\n'
                     % (gid, px, py))
        p = ['  <object id="%d">\n   <metadata key="name" value="%s"/>\n'
             '   <metadata key="extruder" value="1"/>\n' % (gid, label)]
        for i, plabel, ext in part_ids:
            p.append('   <part id="%d" subtype="normal_part">\n'
                     '    <metadata key="name" value="%s"/>\n'
                     '    <metadata key="matrix" value="1 0 0 0 1 0 0 0 1 0 0 0"/>\n'
                     '    <metadata key="extruder" value="%d"/>\n'
                     '   </part>\n' % (i, plabel, ext))
        p.append('  </object>\n')
        cfg.append((gid, ''.join(p)))

    model = _MODEL_HEAD.format(title=title) + ''.join(res) + ' </resources>\n <build>\n' \
        + ''.join(items) + ' </build>\n</model>\n'

    ms = ['<?xml version="1.0" encoding="UTF-8"?>\n<config>\n']
    ms += [c for _, c in cfg]
    ms.append('  <plate>\n   <metadata key="plater_id" value="1"/>\n'
              '   <metadata key="plater_name" value="%s"/>\n'
              '   <metadata key="locked" value="false"/>\n' % plate_name)
    for gid, _ in cfg:
        ms.append('   <model_instance>\n    <metadata key="object_id" value="%d"/>\n'
                  '    <metadata key="instance_id" value="0"/>\n   </model_instance>\n' % gid)
    ms.append('  </plate>\n</config>\n')

    ct = ('<?xml version="1.0" encoding="UTF-8"?>\n'
          '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">\n'
          ' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>\n'
          ' <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>\n'
          ' <Default Extension="png" ContentType="image/png"/>\n'
          ' <Default Extension="gcode" ContentType="text/x.gcode"/>\n</Types>\n')
    rels = ('<?xml version="1.0" encoding="UTF-8"?>\n'
            '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n'
            '<Relationship Target="/3D/3dmodel.model" Id="rel-1" '
            'Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>\n</Relationships>\n')

    with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as z:
        z.writestr('[Content_Types].xml', ct)
        z.writestr('_rels/.rels', rels)
        z.writestr('3D/3dmodel.model', model)
        z.writestr('Metadata/model_settings.config', ''.join(ms))
        z.writestr('Metadata/project_settings.config', PROJECT_SETTINGS)


# ---------------------------------------------------------------------------
#  Multi-plate projects
# ---------------------------------------------------------------------------
PLATE_W = 256.0          # X1 Carbon bed
PLATE_STRIDE = 307.2     # bed * 1.2, per PartPlate.cpp
PLATE_MARGIN = 23.0      # keep clear of the edges and the front wiper zone


def plate_slots(pitch=45.0, margin=PLATE_MARGIN):
    """Grid positions on one bed, centred."""
    n = int((PLATE_W - 2 * margin) // pitch) + 1
    span = (n - 1) * pitch
    x0 = (PLATE_W - span) / 2.0
    return n * n, [(x0 + c * pitch, x0 + r * pitch)
                   for r in range(n) for c in range(n)]


def _plate_origin(i, n_plates):
    cols = max(1, int(math.ceil(math.sqrt(n_plates))))
    return ((i % cols) * PLATE_STRIDE, -(i // cols) * PLATE_STRIDE)


def write_plates(path, plates, title):
    """plates: list of plates; each plate is a list of
       (label, [(part_label, mesh, extruder), ...], (x, y)) with x,y local to the bed."""
    oid = 1
    res, items, cfg = [], [], []
    plate_objs = []
    for pi, plate in enumerate(plates):
        ox, oy = _plate_origin(pi, len(plates))
        ids = []
        for label, parts, (px, py) in plate:
            part_ids = []
            for plabel, mesh, ext in parts:
                res.append(_mesh_xml(mesh, oid))
                part_ids.append((oid, plabel, ext)); oid += 1
            gid = oid; oid += 1
            comp = ''.join('    <component objectid="%d" transform="1 0 0 0 1 0 0 0 1 0 0 0"/>\n' % i
                           for i, _, _ in part_ids)
            res.append('  <object id="%d" type="model">\n   <components>\n%s   </components>\n  </object>\n'
                       % (gid, comp))
            items.append('  <item objectid="%d" transform="1 0 0 0 1 0 0 0 1 %.4f %.4f 0" printable="1"/>\n'
                         % (gid, ox + px, oy + py))
            p = ['  <object id="%d">\n   <metadata key="name" value="%s"/>\n'
                 '   <metadata key="extruder" value="1"/>\n' % (gid, label)]
            for i, plabel, ext in part_ids:
                p.append('   <part id="%d" subtype="normal_part">\n'
                         '    <metadata key="name" value="%s"/>\n'
                         '    <metadata key="matrix" value="1 0 0 0 1 0 0 0 1 0 0 0"/>\n'
                         '    <metadata key="extruder" value="%d"/>\n'
                         '   </part>\n' % (i, plabel, ext))
            p.append('  </object>\n')
            cfg.append(''.join(p)); ids.append(gid)
        plate_objs.append(ids)

    model = _MODEL_HEAD.format(title=title) + ''.join(res) + ' </resources>\n <build>\n' \
        + ''.join(items) + ' </build>\n</model>\n'

    ms = ['<?xml version="1.0" encoding="UTF-8"?>\n<config>\n'] + cfg
    for pi, ids in enumerate(plate_objs):
        # plater_id must be 1-based and contiguous or BambuStudio refuses the file
        ms.append('  <plate>\n   <metadata key="plater_id" value="%d"/>\n'
                  '   <metadata key="plater_name" value="Plate %d"/>\n'
                  '   <metadata key="locked" value="false"/>\n' % (pi + 1, pi + 1))
        for gid in ids:
            ms.append('   <model_instance>\n    <metadata key="object_id" value="%d"/>\n'
                      '    <metadata key="instance_id" value="0"/>\n   </model_instance>\n' % gid)
        ms.append('  </plate>\n')
    ms.append('</config>\n')

    ct = ('<?xml version="1.0" encoding="UTF-8"?>\n'
          '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">\n'
          ' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>\n'
          ' <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>\n'
          ' <Default Extension="png" ContentType="image/png"/>\n'
          ' <Default Extension="gcode" ContentType="text/x.gcode"/>\n</Types>\n')
    rels = ('<?xml version="1.0" encoding="UTF-8"?>\n'
            '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n'
            '<Relationship Target="/3D/3dmodel.model" Id="rel-1" '
            'Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>\n</Relationships>\n')

    with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as z:
        z.writestr('[Content_Types].xml', ct)
        z.writestr('_rels/.rels', rels)
        z.writestr('3D/3dmodel.model', model)
        z.writestr('Metadata/model_settings.config', ''.join(ms))
        z.writestr('Metadata/project_settings.config', PROJECT_SETTINGS)
