"""Original sample furniture, glTF round trip, named material mapping and Cycles renders.

Run with Blender --background --factory-startup --python build_study.py.
No external meshes, textures, HDRIs, or client files are used.
"""
from pathlib import Path
import bpy
import json
import math
from mathutils import Vector

OUT = Path(__file__).resolve().parent
OUT.mkdir(exist_ok=True, parents=True)
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

IDS = {
    'wood_oak_01': (0.42, 0.24, 0.095, 1),
    'metal_powder_graphite': (0.035, 0.045, 0.05, 1),
    'fabric_boucle_forest': (0.10, 0.21, 0.15, 1),
}
placeholders = {}
for name, color in IDS.items():
    mat = bpy.data.materials.new(name)
    mat.diffuse_color = color
    mat.use_nodes = True
    mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = color
    placeholders[name] = mat


def block(name, position, dimensions, material_id, radius=0.008, segments=4):
    bpy.ops.mesh.primitive_cube_add(size=1, location=position)
    ob = bpy.context.object
    ob.name = name
    ob.dimensions = dimensions
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    bevel = ob.modifiers.new('Soft manufactured edges', 'BEVEL')
    bevel.width = radius
    bevel.segments = segments
    bevel.limit_method = 'ANGLE'
    bpy.ops.object.modifier_apply(modifier=bevel.name)
    ob.data.materials.append(placeholders[material_id])
    for poly in ob.data.polygons:
        poly.use_smooth = True
    weighted = ob.modifiers.new('Weighted corner normals', 'WEIGHTED_NORMAL')
    weighted.keep_sharp = True
    bpy.ops.object.modifier_apply(modifier=weighted.name)
    return ob


# Slatted oak deck and lower support rails, dimensions in metres.
for i in range(6):
    block(f'Oak deck slat {i+1:02}', (0, -0.1825+i*0.073, 0.408),
          (1.42, 0.061, 0.036), 'wood_oak_01', 0.007)
for y in (-0.135, 0.135):
    block('Oak underside support', (0, y, 0.373), (1.25, 0.045, 0.039),
          'wood_oak_01', 0.006)
for x in (-0.585, 0.585):
    block('Graphite top crossbar', (x, 0, 0.348), (0.037, 0.356, 0.030),
          'metal_powder_graphite', 0.005)
    for y in (-0.16, 0.16):
        ob = block('Graphite leg', (x, y, 0.181), (0.033, 0.033, 0.342),
                   'metal_powder_graphite', 0.006)
        block('Floor glide', (x, y, 0.009), (0.035, 0.035, 0.018),
              'metal_powder_graphite', 0.006)

# Rounded upholstered pad leaves an oak tray-like seat section exposed.
block('Forest boucle cushion', (0.215, 0, 0.474), (0.925, 0.405, 0.095),
      'fabric_boucle_forest', 0.043, 10)
# A small piping seam follows the rounded rectangular outline.
curve = bpy.data.curves.new('Upholstery piping', 'CURVE')
curve.dimensions = '3D'
curve.bevel_depth = 0.002
curve.bevel_resolution = 3
spline = curve.splines.new('POLY')
points = []
for cx, cy, start in ((0.635,0.155,0),(-0.205,0.155,90),(-0.205,-0.155,180),(0.635,-0.155,270)):
    for j in range(13):
        a = math.radians(start+j*90/12)
        points.append((cx+0.037*math.cos(a),cy+0.037*math.sin(a),0.474,1))
spline.points.add(len(points)-1)
for p, co in zip(spline.points, points):
    p.co = co
spline.use_cyclic_u = True
seam = bpy.data.objects.new('Cushion stitched piping', curve)
bpy.context.collection.objects.link(seam)
seam.data.materials.append(placeholders['fabric_boucle_forest'])
bpy.context.view_layer.objects.active = seam
seam.select_set(True)
bpy.ops.object.convert(target='MESH')

# Export original geometry as glTF, then deliberately start fresh and reimport it.
bpy.ops.object.select_all(action='SELECT')
bpy.ops.export_scene.gltf(filepath=str(OUT/'original-bench.glb'), export_format='GLB',
                          use_selection=True, export_materials='EXPORT')
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.gltf(filepath=str(OUT/'original-bench.glb'))
model_objects = [o for o in bpy.context.scene.objects if o.type == 'MESH']


def cycle_material(identifier):
    mat = bpy.data.materials.new('cycles__'+identifier)
    mat.use_nodes = True
    n = mat.node_tree.nodes
    links = mat.node_tree.links
    bs = n.get('Principled BSDF')
    bs.inputs['Base Color'].default_value = IDS[identifier]
    tex = n.new('ShaderNodeTexCoord')
    noise = n.new('ShaderNodeTexNoise')
    links.new(tex.outputs['Generated'], noise.inputs['Vector'])
    bump = n.new('ShaderNodeBump')
    links.new(noise.outputs['Fac'], bump.inputs['Height'])
    links.new(bump.outputs['Normal'], bs.inputs['Normal'])
    if identifier.startswith('wood'):
        mapping = n.new('ShaderNodeVectorMath')
        mapping.operation = 'MULTIPLY'
        mapping.inputs[1].default_value = (2, 60, 18)
        links.new(tex.outputs['Generated'], mapping.inputs[0])
        links.new(mapping.outputs['Vector'], noise.inputs['Vector'])
        noise.inputs['Scale'].default_value = 2.7
        noise.inputs['Detail'].default_value = 3
        noise.inputs['Roughness'].default_value = 0.68
        ramp = n.new('ShaderNodeValToRGB')
        ramp.color_ramp.elements[0].position = 0.12
        ramp.color_ramp.elements[0].color = (0.20,0.085,0.025,1)
        ramp.color_ramp.elements[1].position = 0.90
        ramp.color_ramp.elements[1].color = (0.55,0.34,0.14,1)
        links.new(noise.outputs['Fac'], ramp.inputs['Fac'])
        links.new(ramp.outputs['Color'], bs.inputs['Base Color'])
        bs.inputs['Roughness'].default_value = 0.36
        bump.inputs['Strength'].default_value = 0.17
        bump.inputs['Distance'].default_value = 0.0007
    elif identifier.startswith('fabric'):
        noise.inputs['Scale'].default_value = 185
        noise.inputs['Detail'].default_value = 2
        bs.inputs['Roughness'].default_value = 0.87
        bs.inputs['Sheen Weight'].default_value = 0.28
        bump.inputs['Strength'].default_value = 0.6
        bump.inputs['Distance'].default_value = 0.0014
        ramp = n.new('ShaderNodeValToRGB')
        ramp.color_ramp.elements[0].color = (0.033,0.095,0.052,1)
        ramp.color_ramp.elements[1].color = (0.17,0.29,0.18,1)
        links.new(noise.outputs['Fac'], ramp.inputs['Fac'])
        links.new(ramp.outputs['Color'], bs.inputs['Base Color'])
    else:
        bs.inputs['Metallic'].default_value = 0.48
        bs.inputs['Roughness'].default_value = 0.33
        noise.inputs['Scale'].default_value = 220
        bump.inputs['Strength'].default_value = 0.10
        bump.inputs['Distance'].default_value = 0.0003
    return mat


registry = {key: cycle_material(key) for key in IDS}
mapping_log = []
for ob in model_objects:
    for slot in ob.material_slots:
        source_id = slot.material.name
        if source_id not in registry:
            raise RuntimeError('Unmapped glTF material ID: '+source_id)
        mapping_log.append({'object':ob.name,'source_material_id':source_id,
                            'cycles_material':registry[source_id].name})
        slot.material = registry[source_id]

scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.samples = 96
scene.cycles.use_denoising = True
scene.cycles.max_bounces = 8
scene.render.resolution_x = 1400
scene.render.resolution_y = 1000
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGBA'
scene.render.film_transparent = True
scene.view_settings.view_transform = 'AgX'
scene.view_settings.exposure = 0
scene.world = bpy.data.worlds.new('Studio world')
scene.world.use_nodes = True
scene.world.node_tree.nodes['Background'].inputs['Color'].default_value = (0.7,0.8,1.0,1)
scene.world.node_tree.nodes['Background'].inputs['Strength'].default_value = 0.20


def aim(ob, target):
    ob.rotation_euler = (Vector(target)-ob.location).to_track_quat('-Z','Y').to_euler()


for name, pos, power, size, color in (
    ('Key softbox', (0,-2,3), 450, 2.5, (1,0.91,0.80)),
    ('Fill softbox', (-2,1,1.7), 280, 2, (0.82,0.90,1)),
    ('Rim strip', (1.5,1.4,2.4), 450, 1.6, (1,1,1)),
):
    data = bpy.data.lights.new(name, 'AREA')
    data.energy, data.shape, data.size, data.color = power, 'DISK', size, color
    ob = bpy.data.objects.new(name, data)
    scene.collection.objects.link(ob)
    ob.location = pos
    aim(ob,(0,0,0.25))

bpy.ops.mesh.primitive_plane_add(size=200, location=(0,0,0))
floor = bpy.context.object
floor.name = 'Transparent shadow catcher'
floor.is_shadow_catcher = True

camera_data = bpy.data.cameras.new('Product orbit camera')
camera = bpy.data.objects.new('Product orbit camera',camera_data)
scene.collection.objects.link(camera)
scene.camera = camera
camera_data.type = 'ORTHO'
camera_data.ortho_scale = 1.95
angles = {'front':(2.5,-3,1.75),'reverse':(-2.7,2.8,1.6),'side':(-2.8,-3.8,1.25)}

stats = {'blender_version':bpy.app.version_string,'original_sample':True,
         'client_project':False,'source_format':'GLB','dimensions_m':[1.42,0.426,0.5215],
         'mesh_objects':len(model_objects),
         'triangles':sum(sum(len(p.vertices)-2 for p in o.data.polygons) for o in model_objects),
         'material_ids':list(IDS),'unmapped_material_ids':[],
         'renderer':'Cycles','samples':96,'resolution':[1400,1000],
         'denoising':True,'color_management':'AgX, exposure 0',
         'manual_compositing':False,'external_assets':False,'mapping':mapping_log,
         'cameras':angles}
(OUT/'study-settings.json').write_text(json.dumps(stats,indent=2)+'\n')
for name, position in angles.items():
    camera.location = position
    aim(camera,(0,0,0.265))
    scene.render.filepath = str(OUT/f'bench-{name}.png')
    if name == 'front':
        scene.render.filepath = '//bench-front.png'
        bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'bench-cycles-study.blend'))
        scene.render.filepath = str(OUT/f'bench-{name}.png')
    bpy.ops.render.render(write_still=True)
print('STUDY_COMPLETE',json.dumps({k:stats[k] for k in ('mesh_objects','triangles','material_ids','unmapped_material_ids')}))
