"""Construit la scene originale Pli dans Blender 4.5.x LTS.

Lancer avec Blender --background --factory-startup --python-exit-code 1
--python generate_scene.py -- [--render-preview]. Aucun telechargement.
La commande remplace la scene en memoire : utiliser un processus dedie.
"""
from pathlib import Path
import argparse
import importlib.util
import json
import math
import sys

ROOT = Path(__file__).resolve().parent


def main():
    args_after = sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else []
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--render-preview', action='store_true', help='Rendu Cycles CPU 480 x 300, 16 echantillons.')
    args = parser.parse_args(args_after)
    module_spec = importlib.util.spec_from_file_location('prismora_validator', ROOT / 'validate_pack.py')
    validator = importlib.util.module_from_spec(module_spec)
    module_spec.loader.exec_module(validator)
    validation = validator.validate(ROOT)
    if not validation['ok']:
        raise RuntimeError('Pack incomplet ou modifie : ' + '; '.join(validation['errors']))
    spec = json.loads((ROOT / 'scene.json').read_text(encoding='utf-8'))
    try:
        import bpy
        from mathutils import Vector
    except ImportError as error:
        raise RuntimeError('Ce generateur doit etre lance par Blender 4.5.x LTS, pas par Python seul.') from error
    if bpy.app.version[:2] != (4, 5):
        raise RuntimeError('Version cible : Blender 4.5.x LTS. Version recue : ' + bpy.app.version_string)
    blend_path = ROOT / spec['outputs']['blend']
    receipt_path = ROOT / spec['outputs']['receipt']
    preview_path = ROOT / spec['outputs']['preview']
    for path in [blend_path, receipt_path] + ([preview_path] if args.render_preview else []):
        if path.exists():
            raise RuntimeError('Sortie deja presente : ' + path.name + '. Extraire le pack dans un nouveau dossier pour un nouvel essai.')
        path.parent.mkdir(parents=True, exist_ok=True)
    bpy.ops.wm.read_factory_settings(use_empty=True)
    scene = bpy.context.scene
    scene.name = 'RenderParcel_Pli_01'
    scene.render.engine = spec['engine']
    scene.cycles.device = 'CPU'
    scene.cycles.samples = spec['samples']
    scene.cycles.seed = spec['seed']
    scene.cycles.use_denoising = False
    scene.cycles.max_bounces = 8
    scene.render.resolution_x, scene.render.resolution_y = spec['resolution']
    scene.render.resolution_percentage = 100
    scene.render.image_settings.file_format = 'PNG'
    scene.render.image_settings.color_mode = 'RGB'
    scene.render.image_settings.color_depth = '8'
    scene.render.film_transparent = False
    scene.frame_start = scene.frame_end = spec['frame']
    scene.frame_set(spec['frame'])
    scene.view_settings.view_transform = 'AgX'
    scene.view_settings.exposure = 0
    scene.view_settings.gamma = 1
    world = bpy.data.worlds.new('Pli_Ambiance')
    world.use_nodes = True
    world.node_tree.nodes['Background'].inputs['Color'].default_value = (0.11, 0.06, 0.17, 1)
    world.node_tree.nodes['Background'].inputs['Strength'].default_value = 0.25
    scene.world = world

    def material(name, color, roughness, metallic=0):
        mat = bpy.data.materials.new(name)
        mat.use_nodes = True
        shader = mat.node_tree.nodes.get('Principled BSDF')
        shader.inputs['Base Color'].default_value = (*color, 1)
        shader.inputs['Roughness'].default_value = roughness
        shader.inputs['Metallic'].default_value = metallic
        return mat, shader

    ceramic, ceramic_bsdf = material('Pli_Ceramique_violette', (0.15, 0.055, 0.23), 0.24)
    noise = ceramic.node_tree.nodes.new('ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 130
    bump = ceramic.node_tree.nodes.new('ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.10
    bump.inputs['Distance'].default_value = 0.008
    ceramic.node_tree.links.new(noise.outputs['Fac'], bump.inputs['Height'])
    ceramic.node_tree.links.new(bump.outputs['Normal'], ceramic_bsdf.inputs['Normal'])
    copper, _ = material('Pli_Orange_satine', (0.72, 0.19, 0.055), 0.32, 0.5)
    cork, _ = material('Pli_Bouchon_prune', (0.055, 0.022, 0.075), 0.3)
    paper, paper_bsdf = material('Pli_Papier_imprime', (0.8, 0.73, 0.60), 0.65)
    stone, _ = material('Pli_Socle_argile', (0.45, 0.31, 0.28), 0.7)
    backdrop, _ = material('Pli_Fond_aubergine', (0.055, 0.023, 0.075), 0.85)

    def finish(obj, name, mat):
        obj.name = name
        obj.data.materials.append(mat)
        return obj

    bpy.ops.mesh.primitive_cube_add(size=1, location=(0.65, 0, 1.23))
    body = finish(bpy.context.object, 'Pli_Corps', ceramic)
    body.dimensions = (1.3, 0.76, 2.1)
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    bevel = body.modifiers.new('Pli_Aretes_douces', 'BEVEL')
    bevel.width, bevel.segments = 0.14, 5
    body.modifiers.new('Pli_Normales', 'WEIGHTED_NORMAL')

    bpy.ops.mesh.primitive_cylinder_add(vertices=96, radius=0.33, depth=0.32, location=(0.65, 0, 2.46))
    cap = finish(bpy.context.object, 'Pli_Bouchon', cork)
    cap_bevel = cap.modifiers.new('Pli_Bord_du_bouchon', 'BEVEL')
    cap_bevel.width, cap_bevel.segments = 0.035, 3
    bpy.ops.mesh.primitive_torus_add(major_segments=96, minor_segments=16, location=(0.65, 0, 2.3), major_radius=0.34, minor_radius=0.075)
    collar = finish(bpy.context.object, 'Pli_Collerette', copper)
    for polygon in collar.data.polygons:
        polygon.use_smooth = True
    bpy.ops.mesh.primitive_cylinder_add(vertices=96, radius=1.17, depth=0.18, location=(0.65, 0, 0.09))
    pedestal = finish(bpy.context.object, 'Pli_Socle', stone)
    pedestal_bevel = pedestal.modifiers.new('Pli_Bord_du_socle', 'BEVEL')
    pedestal_bevel.width, pedestal_bevel.segments = 0.04, 3
    bpy.ops.mesh.primitive_plane_add(size=200, location=(0, 0, 0))
    finish(bpy.context.object, 'Pli_Fond', backdrop)

    # Etiquette frontale originale. Coordonnees UV explicites, pas de police.
    mesh = bpy.data.meshes.new('Pli_Etiquette_Mesh')
    mesh.from_pydata([(0.22, -0.383, 0.65), (1.08, -0.383, 0.65), (1.08, -0.383, 1.75), (0.22, -0.383, 1.75)], [], [(0, 1, 2, 3)])
    mesh.update()
    label = bpy.data.objects.new('Pli_Etiquette', mesh)
    scene.collection.objects.link(label)
    finish(label, 'Pli_Etiquette', paper)
    uv = mesh.uv_layers.new(name='Pli_UV')
    coordinates = [(0, 0), (1, 0), (1, 1), (0, 1)]
    for index, loop in enumerate(mesh.loops):
        uv.data[index].uv = coordinates[loop.vertex_index]
    asset = spec['assets'][0]
    image = bpy.data.images.load(str(ROOT / asset['path']), check_existing=False)
    image.name = 'Pli_Motif_chevron'
    image.colorspace_settings.name = asset['colorSpace']
    # Le .blend est un niveau sous la racine. Garder le fichier externe + le pack.
    image.pack()
    image.filepath = '//../' + asset['path']
    image_node = paper.node_tree.nodes.new('ShaderNodeTexImage')
    image_node.image = image
    paper.node_tree.links.new(image_node.outputs['Color'], paper_bsdf.inputs['Base Color'])

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

    def area(name, location, energy, size, color):
        light_data = bpy.data.lights.new(name, 'AREA')
        light_data.energy, light_data.shape, light_data.size = energy, 'DISK', size
        light_data.color = color
        light = bpy.data.objects.new(name, light_data)
        scene.collection.objects.link(light)
        light.location = location
        aim(light, (0.65, 0, 1.2))

    area('Pli_Key', (-3.5, -4.5, 5.5), 700, 4.0, (1.0, 0.87, 0.76))
    area('Pli_Fill', (4, -1.5, 3), 260, 3.5, (0.72, 0.68, 1.0))
    area('Pli_Rim', (0.5, 3, 4.5), 550, 2.5, (1.0, 0.50, 0.25))
    camera_data = bpy.data.cameras.new('Pli_Camera')
    camera = bpy.data.objects.new('Pli_Camera', camera_data)
    scene.collection.objects.link(camera)
    camera.location = (4.0, -7.5, 3.6)
    aim(camera, (0.1, 0, 1.3))
    camera_data.type, camera_data.ortho_scale = 'ORTHO', 4.8
    camera_data.lens = 50
    scene.camera = camera
    scene['prismora_pack_version'] = spec['version']
    scene['prismora_purpose'] = 'Scene originale de transfert ; aucune mesure GPU'
    actual = set(scene.objects.keys())
    if actual != set(spec['expectedObjects']):
        raise RuntimeError('Inventaire d objets inattendu : ' + repr(sorted(actual)))
    scene.render.filepath = '//../05_rendus/pli_final.png'
    bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), check_existing=False)
    receipt = {'blenderVersion': bpy.app.version_string, 'engine': scene.render.engine, 'device': scene.cycles.device, 'blend': spec['outputs']['blend'], 'objects': sorted(actual), 'packedImages': [image.name], 'renderExecuted': False, 'gpuRenderExecuted': False}
    if args.render_preview:
        scene.render.resolution_x, scene.render.resolution_y = spec['preview']['resolution']
        scene.cycles.samples = spec['preview']['samples']
        scene.render.filepath = str(preview_path)
        bpy.ops.render.render(write_still=True)
        if not preview_path.is_file():
            raise RuntimeError('Blender a termine sans le PNG de controle attendu')
        receipt.update(renderExecuted=True, preview=spec['outputs']['preview'], previewResolution=spec['preview']['resolution'], previewSamples=spec['preview']['samples'])
    receipt_path.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
    print(json.dumps(receipt, ensure_ascii=False))


if __name__ == '__main__':
    main()
