"""Actual Concept 28 Fusion mesh views and a uniformly scaled Material Lab derivative.
Blender -b -t 6 --python render.py -- VALIDATED_R2_HANDOFF OUTPUT
Manufacturing files are read only. No AI-generated engineering views.
"""
import bpy
import hashlib
import json
import math
import sys
from pathlib import Path
from mathutils import Matrix, Vector

source, output = map(Path, sys.argv[sys.argv.index('--') + 1:][:2])
output.mkdir(parents=True, exist_ok=True)
manifest = []
for path in sorted((source / 'meshes/parts').glob('*.stl')):
    n=path.stem
    if '_HW_' in n: continue
    key=('chalk' if 'Shell' in n else 'rim' if 'Elastomer_Rim' in n else 'orange' if 'Ember_PTT' in n else 'steel' if any(t in n for t in ['Steel_Play_Cap','Stainless_Grille','Volume_Up_Pewter','Volume_Down_Pewter']) else 'graphite' if 'Woven_Mesh' in n else 'speaker' if any(t in n for t in ['Scrim','Acoustic_Mesh','Acoustic_Membrane']) else 'lens' if 'Status_Lens' in n else 'bond' if 'Bond_Film' in n else 'clear' if 'Coupler' in n else 'foam' if any(t in n for t in ['Preload','Liner','Pad','Seat']) else 'carrier')
    manifest.append({'name':n,'material':key})
params = json.loads((source / 'source/preview-materials.json').read_text())
params['materials']['laser']={'rgba':[.12,.13,.12,1], 'metallic':.1,'roughness':.7}
params['materials']['speaker']={'rgba':[.055,.058,.055,1], 'metallic':0,'roughness':.88}
params['materials']['graphite']={'rgba':[.37,.38,.36,1], 'metallic':.75,'roughness':.36}
params['materials']['steel']={'rgba':[.68,.70,.69,1], 'metallic':.9,'roughness':.3}
offsets={}
for row in manifest:
    n=row['name'];v=[0,0,0]
    if '_01_' in n:v=[0,0,16]
    elif '_02_' in n:v=[0,0,-18]
    elif '_09_' in n:v=[0,0,0]
    elif '_16_' in n:v=[-13,0,0]
    elif '_10_' in n:v=[0,0,34]
    elif any('_'+str(i)+'_' in n for i in range(57,63)):v=[0,0,32+(int(n.split('_')[2])-57)*2]
    elif '_04_' in n:v=[0,0,25]
    elif any('_'+str(i)+'_' in n for i in [12,14]):v=[12,0,0]
    offsets[n]=v
bpy.ops.wm.read_factory_settings(use_empty=True)
objects = []
records = []
scale = 1.36 / 152.7
native_manifest=json.loads((source/'audit/Concept_28_R1_Fusion_manifest.json').read_text())
world_bounds=[body['bbox_mm'] for component in native_manifest['components'] if '_REF_' not in component['name'] for body in component['bodies']]
bounds=[[min(b[i] for b in world_bounds) for i in [0,2,4]],[max(b[i] for b in world_bounds) for i in [1,3,5]]]
center = [(a + b) / 2 for a, b in zip(*bounds)]
colors = {'shell': '#E3E0D6', 'bumper': '#7D786E', 'button': '#ADB3B0', 'ptt': '#CC4118', 'volume': '#ADB3B0'}

def material(key):
    spec = params['materials'][key]
    rgba = spec['rgba']
    rgb = [v / 12.92 if v <= .04045 else ((v + .055) / 1.055) ** 2.4 for v in rgba[:3]]
    m = bpy.data.materials.new(key)
    m.use_nodes = True
    m.diffuse_color = (*rgb, rgba[3])
    bs = m.node_tree.nodes.get('Principled BSDF')
    bs.inputs['Base Color'].default_value = m.diffuse_color
    bs.inputs['Metallic'].default_value = spec['metallic']
    bs.inputs['Roughness'].default_value = spec['roughness']
    if key == 'pewter' and 'Anisotropic IOR Level' in bs.inputs:
        bs.inputs['Anisotropic IOR Level'].default_value = .35
    return m

mats = {key: material(key) for key in params['materials']}
for row in manifest:
    path = source / 'meshes/parts' / (row['name'] + '.stl')
    bpy.ops.wm.stl_import(filepath=str(path))
    obj = bpy.context.object
    obj.name = row['name']
    obj.data.name = row['name']
    obj.data.materials.append(mats[row['material']])
    if 'Steel_Play_Cap' in obj.name:
        obj.data.materials.append(mats['laser'])
        for poly in obj.data.polygons:
            if abs(poly.center.z-.51)<.003: poly.material_index=1
    import re
    match = re.match(r'C(?:27_UC1|27_R2|28_R1)_(\d{2})_', row['name'])
    num = int(match.group(1)) if match else 0
    part = ('shell' if num in (1, 2) else 'bumper' if num == 9 else 'ptt' if num == 16
            else 'button' if num == 10 else 'volume' if num in (12, 14) else None)
    obj['labPart'] = part or ''
    obj['labOffset'] = [value * scale for value in offsets[row['name']]]
    obj.data.transform(Matrix.Translation(Vector([-v * scale for v in center])) @ Matrix.Scale(scale, 4))
    # Evaluate Smooth by Angle during GLB export so planar CAD normals stay flat.
    bpy.ops.object.shade_auto_smooth()
    objects.append(obj)
    records.append({'name': obj.name, 'source': 'meshes/parts/' + path.name,
                    'sha256': hashlib.sha256(path.read_bytes()).hexdigest(), 'part': part,
                    'material': row['material'], 'vertices': len(obj.data.vertices), 'faces': len(obj.data.polygons)})

lab = output / 'lab'
lab.mkdir(exist_ok=True)
bpy.ops.object.select_all(action='SELECT')
bpy.ops.export_scene.gltf(filepath=str(lab / 'model.glb'), export_format='GLB',
                         export_yup=False, export_extras=True, export_apply=True,
                         export_cameras=False, export_lights=False, export_animations=False,
                         export_draco_mesh_compression_enable=True,
                         export_draco_position_quantization=16, export_draco_normal_quantization=12)
model = {'concept': '28', 'revision': 'R1', 'coordinateSystem': 'Y up, Z front',
         'displayScale': scale, 'sourceBounds': bounds, 'sourceCenterMm': center,
         'colors': colors, 'objects': records,
         'assetSha256': hashlib.sha256((lab / 'model.glb').read_bytes()).hexdigest(),
         'bytes': (lab / 'model.glb').stat().st_size,
         'sourceAssembly': 'Walkie_One_28_R1_CASE.step',
         'sourceAssemblySha256': hashlib.sha256((source / 'cad/Walkie_One_28_R1_CASE.step').read_bytes()).hexdigest(),
         'visibleLightCenterMm': [0, 2], 'visibleLightLoweredMm': 3,
         'note': 'Actual Concept 28 R1 Fusion-exported finished-part meshes. The circular light is 3 mm lower; the original rear collector and internal LED/coupler are retained. Woven mesh uses two crossing rod layers as a simplified stock representation; it is purchased material, not individually manufactured rods. Electronics and bought-in screws are omitted from this lightweight finish viewer and supplied in the full reference STEP. Uniform display scale and centering only; exploded offsets are illustrative. Physical materials, force, electronics and durability remain to be qualified.',
         'unmeshedSourceRecords': []}
(lab / 'model.json').write_text(json.dumps(model, indent=2) + '\n')

# Render the same meshes in a Z-up studio, at real metre scale.
transform = Matrix.Rotation(math.pi / 2, 4, 'X')
for obj in objects:
    obj.data.transform(Matrix.Scale(.001 / scale, 4))
    obj.data.transform(transform)
    obj.location.z = .07635
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.samples = 32
scene.cycles.use_denoising = True
scene.render.resolution_x = 1100
scene.render.resolution_y = 1300
scene.render.resolution_percentage = 100
scene.view_settings.view_transform = 'AgX'
scene.view_settings.exposure = .25
scene.world = bpy.data.worlds.new('Studio')
scene.world.use_nodes = True
scene.world.node_tree.nodes['Background'].inputs['Color'].default_value = (.78, .78, .75, 1)
scene.world.node_tree.nodes['Background'].inputs['Strength'].default_value = .25
bpy.ops.mesh.primitive_plane_add(size=200, location=(0, 0, -.00005))
ground = bpy.context.object
ground.name = 'Studio ground'
gmat = bpy.data.materials.new('Warm paper')
gmat.use_nodes = True
gmat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (.57, .55, .49, 1)
gmat.node_tree.nodes['Principled BSDF'].inputs['Roughness'].default_value = .85
ground.data.materials.append(gmat)
target = Vector((0, 0, .07635))
for name, pos, power, size in [('Key', (-.3, -.35, .5), 2.5, .35), ('Fill', (.3, -.1, .25), 1, .3), ('Rim', (.1, .2, .35), 2, .25)]:
    data = bpy.data.lights.new(name, 'AREA')
    data.energy, data.shape, data.size = power, 'DISK', size
    light = bpy.data.objects.new(name, data)
    scene.collection.objects.link(light)
    light.location = pos
    light.rotation_euler = (target - light.location).to_track_quat('-Z', 'Y').to_euler()
data = bpy.data.cameras.new('Camera')
camera = bpy.data.objects.new('Camera', data)
scene.collection.objects.link(camera)
scene.camera = camera
data.type, data.ortho_scale = 'ORTHO', .195
views = [('front', (0, -.5, .07635)), ('front-right', (.19, -.5, .20)),
         ('right', (.5, 0, .07635)), ('rear', (0, .5, .07635)),
         ('left', (-.5, 0, .07635)), ('front-left', (-.19, -.5, .20)),
         ('exploded', (.25, -.5, .23))]
for name, pos in views:
    if name == 'exploded':
        for obj in objects:
            obj.location += transform.to_3x3() @ Vector([v * .001 for v in offsets[obj.name]])
        data.ortho_scale = .245
    camera.location = pos
    camera.rotation_euler = (target - camera.location).to_track_quat('-Z', 'Y').to_euler()
    scene.render.filepath = str(output / (name + '.png'))
    bpy.ops.render.render(write_still=True)
# Internal faces of the actual shells; isolate for a readable retention view.
for obj in objects:
    obj.location -= transform.to_3x3() @ Vector([v * .001 for v in offsets[obj.name]])
    obj.hide_render = True
data.ortho_scale = .19
for number, name, pos in [(1, 'front-internal', (.10, .5, .19)), (2, 'rear-internal', (-.10, -.5, .19))]:
    shell = next(o for o in objects if (o.name.startswith(f'C27_R2_{number:02d}_') or o.name.startswith(f'C28_R1_{number:02d}_')))
    shell.hide_render = False
    camera.location = pos
    camera.rotation_euler = (target - camera.location).to_track_quat('-Z', 'Y').to_euler()
    scene.render.filepath = str(output / (name + '.png'))
    bpy.ops.render.render(write_still=True)
    shell.hide_render = True
print('Concept 28 R1: nine actual CAD views and Material Lab exported.')
