from __future__ import annotations

import json
import math
import re
import sys
import zipfile
from collections import Counter
from pathlib import Path
from xml.etree import ElementTree as ET

ROOT = Path(__file__).resolve().parent
INPUT = ROOT / "30-3mf-conversion-input.3mf"
OUTPUT = ROOT / "30-3mf-conversion-output.stl"
REPORT = ROOT / "30-3mf-conversion-verification.json"
NS = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02"

TRIANGLES = [(0, 2, 1), (0, 3, 2), (4, 5, 6), (4, 6, 7), (0, 1, 5), (0, 5, 4), (1, 2, 6), (1, 6, 5), (2, 3, 7), (2, 7, 6), (3, 0, 4), (3, 4, 7)]
OBJECTS = [
    {
        "id": "1",
        "name": "meetblok-A",
        "vertices": [(0, 0, 0), (40, 0, 0), (40, 20, 0), (0, 20, 0), (0, 0, 10), (40, 0, 10), (40, 20, 10), (0, 20, 10)],
    },
    {
        "id": "2",
        "name": "meetblok-B",
        "vertices": [(50, 0, 0), (60, 0, 0), (60, 10, 0), (50, 10, 0), (50, 0, 10), (60, 0, 10), (60, 10, 10), (50, 10, 10)],
    },
]


def create_3mf() -> None:
    triangles = "".join(f'<triangle v1="{a}" v2="{b}" v3="{c}"/>' for a, b, c in TRIANGLES)
    objects = "".join(
        f'<object id="{item["id"]}" name="{item["name"]}" type="model"><mesh><vertices>'
        + "".join(f'<vertex x="{x}" y="{y}" z="{z}"/>' for x, y, z in item["vertices"])
        + f"</vertices><triangles>{triangles}</triangles></mesh></object>"
        for item in OBJECTS
    )
    build_items = "".join(f'<item objectid="{item["id"]}"/>' for item in OBJECTS)
    model = f'''<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xml:lang="nl-NL" xmlns="{NS}">
  <metadata name="Title">Gecontroleerde conversie met twee meetblokken</metadata>
  <resources>{objects}</resources>
  <build>{build_items}</build>
</model>'''
    content_types = '''<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/></Types>'''
    rels = '''<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>'''
    with zipfile.ZipFile(INPUT, "w", zipfile.ZIP_DEFLATED) as archive:
        for name, contents in (
            ("[Content_Types].xml", content_types),
            ("_rels/.rels", rels),
            ("3D/3dmodel.model", model),
        ):
            entry = zipfile.ZipInfo(name, date_time=(2026, 9, 6, 0, 0, 0))
            entry.compress_type = zipfile.ZIP_DEFLATED
            entry.create_system = 0
            archive.writestr(entry, contents.encode("utf-8"))


def normal(a, b, c):
    u = tuple(b[i] - a[i] for i in range(3))
    v = tuple(c[i] - a[i] for i in range(3))
    n = (u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0])
    length = math.sqrt(sum(value * value for value in n))
    return tuple(value / length for value in n)


def convert() -> dict:
    with zipfile.ZipFile(INPUT) as archive:
        names = sorted(archive.namelist())
        xml = archive.read("3D/3dmodel.model")
    root = ET.fromstring(xml)
    unit = root.attrib.get("unit")
    objects = root.findall(f".//{{{NS}}}object")
    build_items = root.findall(f".//{{{NS}}}build/{{{NS}}}item")
    if any("transform" in item.attrib for item in build_items):
        raise ValueError("This bounded fixture converter rejects 3MF build transforms instead of silently dropping them.")
    lines = ["solid converted_fixture"]
    triangle_counts = []
    object_names = []
    for object_node in objects:
        object_names.append(object_node.attrib.get("name"))
        vertices = [tuple(float(node.attrib[key]) for key in ("x", "y", "z")) for node in object_node.findall(f".//{{{NS}}}vertex")]
        triangles = [tuple(int(node.attrib[key]) for key in ("v1", "v2", "v3")) for node in object_node.findall(f".//{{{NS}}}triangle")]
        triangle_counts.append(len(triangles))
        for triangle in triangles:
            a, b, c = (vertices[index] for index in triangle)
            nx, ny, nz = normal(a, b, c)
            lines.extend([f"  facet normal {nx:.6f} {ny:.6f} {nz:.6f}", "    outer loop"])
            lines.extend(f"      vertex {x:g} {y:g} {z:g}" for x, y, z in (a, b, c))
            lines.extend(["    endloop", "  endfacet"])
    lines.append("endsolid converted_fixture")
    OUTPUT.write_text("\n".join(lines) + "\n", encoding="ascii")
    parsed = [tuple(map(float, match)) for match in re.findall(r"^\s*vertex\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)$", OUTPUT.read_text(encoding="ascii"), re.MULTILINE)]
    parsed_triangles = [tuple(parsed[index:index + 3]) for index in range(0, len(parsed), 3)]
    graph = {}
    edges = Counter()
    directed_edges = Counter()
    for triangle in parsed_triangles:
        for point in triangle:
            graph.setdefault(point, set()).update(other for other in triangle if other != point)
        for start, end in zip(triangle, triangle[1:] + triangle[:1]):
            edges[tuple(sorted((start, end)))] += 1
            directed_edges[(start, end)] += 1
    remaining = set(graph)
    components = []
    while remaining:
        pending = [remaining.pop()]
        component = set(pending)
        while pending:
            for neighbour in graph[pending.pop()]:
                if neighbour in remaining:
                    remaining.remove(neighbour)
                    component.add(neighbour)
                    pending.append(neighbour)
        components.append(component)
    components.sort(key=lambda component: min(point[0] for point in component))
    shell_dimensions = []
    for component in components:
        shell_dimensions.append([
            max(point[axis] for point in component) - min(point[axis] for point in component)
            for axis in range(3)
        ])
    minimum = [min(point[axis] for point in parsed) for axis in range(3)]
    maximum = [max(point[axis] for point in parsed) for axis in range(3)]
    boundary_edges = sum(count == 1 for count in edges.values())
    non_manifold_edges = sum(count > 2 for count in edges.values())
    winding_mismatches = sum(
        count != directed_edges[(end, start)]
        for (start, end), count in directed_edges.items()
        if start < end
    )
    signed_volume = sum(
        a[0] * (b[1] * c[2] - b[2] * c[1])
        + a[1] * (b[2] * c[0] - b[0] * c[2])
        + a[2] * (b[0] * c[1] - b[1] * c[0])
        for a, b, c in parsed_triangles
    ) / 6
    return {"packageEntries": names, "unitReadFrom3mf": unit, "objectCount": len(objects), "buildItemCount": len(build_items), "buildTransforms": 0, "objectNamesReadFrom3mf": object_names, "triangleCountPerObject": triangle_counts, "triangleCountOutput": len(parsed_triangles), "outputShellCount": len(components), "outputDimensions": [maximum[i] - minimum[i] for i in range(3)], "outputShellDimensions": shell_dimensions, "outputTopology": {"boundaryEdges": boundary_edges, "nonManifoldEdges": non_manifold_edges, "windingMismatches": winding_mismatches, "signedVolumeCoordinateUnitsCubed": signed_volume}, "metadataAbsentFromStl": ["millimeter unit", "3MF Title metadata", "object names"]}


create_3mf()
result = convert()
report = {"schemaVersion": 1, "checkedOn": "2026-09-06", "method": "Generated a minimal standards-shaped 3MF package with two positioned core-mesh objects, rejected build transforms by contract, exported both meshes to ASCII STL, and reparsed output shells, dimensions and topology. This validates only the controlled fixture, not arbitrary 3MF extensions or named software.", "input": INPUT.name, "output": OUTPUT.name, "result": result, "pass": result["unitReadFrom3mf"] == "millimeter" and result["objectCount"] == result["buildItemCount"] == result["outputShellCount"] == 2 and result["buildTransforms"] == 0 and result["triangleCountPerObject"] == [12, 12] and result["triangleCountOutput"] == 24 and result["outputDimensions"] == [60.0, 20.0, 10.0] and result["outputShellDimensions"] == [[40.0, 20.0, 10.0], [10.0, 10.0, 10.0]] and result["outputTopology"] == {"boundaryEdges": 0, "nonManifoldEdges": 0, "windingMismatches": 0, "signedVolumeCoordinateUnitsCubed": 9000.0}}
REPORT.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2))
sys.exit(0 if report["pass"] else 1)
