bl_info = {
    "name": "Export for InStudio",
    "author": "InStudio",
    "version": (1, 0, 0),
    "blender": (3, 6, 0),
    "location": "File > Export > InStudio (.instudio)",
    "description": "Exports the product with its groups, lights, cameras, world HDRI, background and colour look as one file for InStudio.",
    "category": "Import-Export",
}

# How to install: Blender > Edit > Preferences > Add-ons > Install (or "Install from Disk" in
# Blender 4.2+), choose this .py file, then tick "Export for InStudio".
# How to use: File > Export > InStudio (.instudio). Send the .instudio file to whoever builds the
# presentation; they open it in InStudio and get the product exactly as designed.

import json
import math
import os
import shutil
import tempfile
import zipfile

import bpy
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
from bpy_extras.io_utils import ExportHelper

GENERATOR = "InStudio Blender add-on 1.0.0"


def _active_world_output(tree):
    outputs = [n for n in tree.nodes if n.type == 'OUTPUT_WORLD']
    for node in outputs:
        if getattr(node, "is_active_output", False):
            return node
    return outputs[0] if outputs else None


def _linked_node(socket):
    return socket.links[0].from_node if socket.is_linked and socket.links else None


def read_world(scene, warnings):
    """What the world contributes: an HDRI (image, strength, rotation) or a plain colour."""
    world = scene.world
    result = {"image": None, "strength": 1.0, "rotation_z": 0.0, "color": [0.05, 0.05, 0.05]}
    if world is None:
        return result
    # Blender 6 drops World.use_nodes (nodes are always on); older versions may have them off.
    if world.node_tree is None or not getattr(world, "use_nodes", True):
        result["color"] = [float(c) for c in world.color[:3]]
        return result

    tree = world.node_tree
    output = _active_world_output(tree)
    background = _linked_node(output.inputs['Surface']) if output else None
    if background is None or background.type != 'BACKGROUND':
        background = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
    if background is None:
        warnings.append("The world has no Background node, so InStudio will use its studio lighting.")
        return result

    result["strength"] = float(background.inputs['Strength'].default_value)
    color_input = background.inputs['Color']
    source = _linked_node(color_input)
    if source is None:
        result["color"] = [float(c) for c in color_input.default_value[:3]]
    elif source.type == 'TEX_ENVIRONMENT' and source.image is not None:
        result["image"] = source.image
        mapping = _linked_node(source.inputs['Vector'])
        if mapping is not None and mapping.type == 'MAPPING':
            rotation = mapping.inputs['Rotation'].default_value if 'Rotation' in mapping.inputs else getattr(mapping, "rotation", (0, 0, 0))
            result["rotation_z"] = float(rotation[2])
    elif source.type == 'TEX_SKY':
        warnings.append("Blender's Sky Texture can't be exported. Use an HDRI image in the world for InStudio, or it will use its studio lighting.")
    else:
        warnings.append("The world colour comes from nodes InStudio can't read. Use an Environment Texture with an .hdr/.exr image.")
    return result


def save_world_image(image, folder, warnings):
    """Copies the world HDRI into the bundle folder. Returns its file name, or None."""
    fmt = (image.file_format or "").upper()
    path = bpy.path.abspath(image.filepath, library=image.library) if image.filepath else ""
    extension = os.path.splitext(path)[1].lower().lstrip(".")
    if fmt == 'HDR':
        extension = "hdr"
    elif fmt == 'OPEN_EXR':
        extension = "exr"
    if extension not in ("hdr", "exr"):
        warnings.append(f"The world image '{image.name}' isn't an .hdr or .exr file, so InStudio will use its studio lighting.")
        return None
    name = f"environment.{extension}"
    target = os.path.join(folder, name)
    if image.packed_file is not None:
        with open(target, "wb") as f:
            f.write(image.packed_file.data)
        return name
    if path and os.path.isfile(path):
        shutil.copyfile(path, target)
        return name
    warnings.append(f"The world image file '{path or image.name}' couldn't be found. Pack it (File > External Data > Pack Resources) and export again.")
    return None


def export_glb(path, selected_only):
    """Runs Blender's glTF exporter with the options InStudio needs, skipping any this Blender lacks."""
    wanted = {
        "filepath": path,
        "export_format": 'GLB',
        "use_selection": selected_only,
        "export_cameras": True,
        "export_lights": True,          # "Punctual Lights": point, spot and sun
        "export_extras": True,          # custom properties
        "export_apply": True,           # apply modifiers
        "export_animations": True,
        "export_yup": True,
        "export_hierarchy_full_collections": True,  # Blender 4.2+: collections become groups
        # "Compatible" light units match how InStudio renders lights, so they look as they do here.
        "export_import_convert_lighting_mode": 'COMPAT',
    }
    available = set(bpy.ops.export_scene.gltf.get_rna_type().properties.keys())
    bpy.ops.export_scene.gltf(**{k: v for k, v in wanted.items() if k in available})
    return "compat" if "export_import_convert_lighting_mode" in available else "spec"


class EXPORT_OT_instudio(Operator, ExportHelper):
    """Export the product with its lighting, HDRI, background, cameras and groups for InStudio"""

    bl_idname = "export_scene.instudio"
    bl_label = "Export for InStudio"
    bl_options = {'PRESET'}

    filename_ext = ".instudio"
    filter_glob: StringProperty(default="*.instudio", options={'HIDDEN'})

    selected_only: BoolProperty(
        name="Selected objects only",
        description="Export only the selected objects (the world, lights and cameras you select still come along)",
        default=False,
    )
    include_world: BoolProperty(
        name="Include world (HDRI and background)",
        description="Bring the world HDRI, its strength and rotation, and the background into InStudio",
        default=True,
    )

    def execute(self, context):
        scene = context.scene
        warnings = []
        folder = tempfile.mkdtemp(prefix="instudio-")
        try:
            glb_path = os.path.join(folder, "scene.glb")
            light_units = export_glb(glb_path, self.selected_only)
            if not os.path.isfile(glb_path):
                self.report({'ERROR'}, "Blender's glTF exporter didn't produce a file.")
                return {'CANCELLED'}

            world = read_world(scene, warnings) if self.include_world else None
            env_file = save_world_image(world["image"], folder, warnings) if world and world["image"] else None

            if scene.render.film_transparent:
                background = {"mode": "transparent"}
            elif env_file:
                background = {"mode": "environment"}
            elif world:
                background = {"mode": "color", "color": world["color"]}
            else:
                background = None

            info = {
                "version": 1,
                "generator": GENERATOR,
                "blender": bpy.app.version_string,
                "environment": {
                    "file": env_file,
                    "strength": world["strength"] if world else 1.0,
                    "rotationZ": world["rotation_z"] if world else 0.0,
                } if env_file else None,
                "background": background,
                "exposure": float(scene.view_settings.exposure),
                "viewTransform": scene.view_settings.view_transform,
                "camera": scene.camera.name if scene.camera else None,
                "unitScale": float(scene.unit_settings.scale_length),
                "lightUnits": light_units,
            }
            with open(os.path.join(folder, "instudio.json"), "w", encoding="utf-8") as f:
                json.dump(info, f, indent=2)

            with zipfile.ZipFile(self.filepath, "w", zipfile.ZIP_DEFLATED) as bundle:
                bundle.write(os.path.join(folder, "instudio.json"), "instudio.json")
                bundle.write(glb_path, "scene.glb")
                if env_file:
                    bundle.write(os.path.join(folder, env_file), env_file)
        except Exception as error:  # report anything to the user instead of a traceback
            self.report({'ERROR'}, f"Export for InStudio failed: {error}")
            return {'CANCELLED'}
        finally:
            shutil.rmtree(folder, ignore_errors=True)

        for warning in warnings:
            self.report({'WARNING'}, warning)
        size_mb = os.path.getsize(self.filepath) / (1024 * 1024)
        self.report({'INFO'}, f"Exported {os.path.basename(self.filepath)} ({size_mb:.1f} MB) for InStudio.")
        return {'FINISHED'}


def menu_func_export(self, context):
    self.layout.operator(EXPORT_OT_instudio.bl_idname, text="InStudio (.instudio)")


def register():
    bpy.utils.register_class(EXPORT_OT_instudio)
    bpy.types.TOPBAR_MT_file_export.append(menu_func_export)


def unregister():
    bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
    bpy.utils.unregister_class(EXPORT_OT_instudio)


if __name__ == "__main__":
    register()
