bl_info = {
    "name": "BeamNG Light Validation",
    "author": "BeamNG",
    "version": (0, 1, 6),
    "blender": (4, 5, 0),
    "location": "View3D > Sidebar > BeamNG Light Validation",
    "description": "Validate Blender lights and emissive materials against BeamNG values",
    "category": "Lighting",
}

import json
import math
from pathlib import Path

import bpy
from bpy.props import BoolProperty, EnumProperty, FloatProperty, FloatVectorProperty, PointerProperty, StringProperty
from bpy.types import Light, Material, Operator, Panel, PropertyGroup


LM_PER_W = 683.0
COCONUT_LIGHT_RANGE = 5000.0
POINT_DEFAULT_RADIUS = 5.0
SPOT_DEFAULT_RANGE = 10.0
EMISSIVE_FALLBACK_NITS = 2000.0
_SYNCING_FROM_BLENDER = False
_MSGBUS_OWNER = object()
GAME_ROOT = Path(__file__).resolve().parents[3]
COOKIE_TEX_NODE = "BeamNG Cookie Texture"
COOKIE_COORD_NODE = "BeamNG Cookie Coordinates"


def _clamp(v: float, lo: float, hi: float) -> float:
    return max(lo, min(hi, v))


def kelvin_to_rgb_linear(kelvin: float) -> tuple[float, float, float]:
    k = _clamp(float(kelvin or 6500.0), 1667.0, 25000.0)
    if k <= 4000.0:
        x = -0.2661239e9 / (k ** 3) - 0.2343580e6 / (k ** 2) + 0.8776956e3 / k + 0.179910
        y = -1.1063814 * (x ** 3) - 1.34811020 * (x ** 2) + 2.18555832 * x - 0.20219683
    else:
        x = -3.0258469e9 / (k ** 3) + 2.1070379e6 / (k ** 2) + 0.2226347e3 / k + 0.240390
        y = 3.0817580 * (x ** 3) - 5.87338670 * (x ** 2) + 3.75112997 * x - 0.37001483

    X = x / y
    Y = 1.0
    Z = (1.0 - x - y) / y

    r = 3.2404542 * X + (-1.5371385) * Y + (-0.4985314) * Z
    g = (-0.9692660) * X + 1.8760108 * Y + 0.0415560 * Z
    b = 0.0556434 * X + (-0.2040259) * Y + 1.0572252 * Z

    r = max(0.0, r)
    g = max(0.0, g)
    b = max(0.0, b)
    m = max(r, g, b)
    if m > 0.0:
        r /= m
        g /= m
        b /= m
    return r, g, b


def point_brightness_from_lumens(lumens: float) -> float:
    return float(lumens) / (4.0 * math.pi * COCONUT_LIGHT_RANGE)


def spot_brightness_from_candela(candela: float) -> float:
    return float(candela) / COCONUT_LIGHT_RANGE


def lumens_to_blender_watts(lumens: float) -> float:
    return max(0.0, float(lumens)) / LM_PER_W


def candela_to_blender_watts(candela: float) -> float:
    return max(0.0, float(candela)) * 4.0 * math.pi / LM_PER_W


def get_active_material(context) -> Material | None:
    obj = context.object
    if obj and getattr(obj, "active_material", None):
        return obj.active_material
    return getattr(context, "material", None)


def ensure_principled(mat: Material):
    mat.use_nodes = True
    nt = mat.node_tree
    bsdf = find_principled(mat)
    if bsdf is None:
        bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled")
        bsdf.location = (0, 0)
    out = None
    for node in nt.nodes:
        if node.bl_idname == "ShaderNodeOutputMaterial":
            out = node
            break
    if out is None:
        out = nt.nodes.new("ShaderNodeOutputMaterial")
        out.location = (300, 0)
    if not any(link.from_node == bsdf and link.to_node == out for link in nt.links):
        nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    return bsdf


def find_principled(mat: Material):
    if not mat or not mat.use_nodes or not mat.node_tree:
        return None
    for node in mat.node_tree.nodes:
        if node.bl_idname == "ShaderNodeBsdfPrincipled":
            return node
    return None


def get_principled_input(bsdf, names: tuple[str, ...]):
    if bsdf is None:
        return None
    for name in names:
        socket = bsdf.inputs.get(name)
        if socket is not None:
            return socket
    return None


def set_principled_input(bsdf, names: tuple[str, ...], value):
    socket = get_principled_input(bsdf, names)
    if socket is not None:
        socket.default_value = value
        return True
    return False


def find_light_node(light: Light, bl_idname: str):
    if not light.node_tree:
        return None
    for node in light.node_tree.nodes:
        if node.bl_idname == bl_idname:
            return node
    return None


def resolve_cookie_path(cookie: str) -> Path | None:
    value = str(cookie or "").strip()
    if not value:
        return None
    path = Path(value)
    if path.is_absolute() and path.exists():
        return path
    if value.startswith("/"):
        path = GAME_ROOT / value.lstrip("/")
    else:
        path = GAME_ROOT / value
    return path if path.exists() else None


def remove_cookie_nodes(light: Light):
    if not light.node_tree:
        return
    nt = light.node_tree
    for node in list(nt.nodes):
        if node.name in {COOKIE_TEX_NODE, COOKIE_COORD_NODE}:
            nt.nodes.remove(node)

    emission = find_light_node(light, "ShaderNodeEmission")
    output = find_light_node(light, "ShaderNodeOutputLight")
    if emission and output and not any(link.from_node == emission and link.to_node == output for link in nt.links):
        nt.links.new(emission.outputs["Emission"], output.inputs["Surface"])


def apply_cookie_nodes(light: Light, cookie: str):
    cookie = str(cookie or "").strip()
    if not cookie:
        remove_cookie_nodes(light)
        return

    light.use_nodes = True
    nt = light.node_tree
    if not nt:
        return

    emission = find_light_node(light, "ShaderNodeEmission") or nt.nodes.new("ShaderNodeEmission")
    output = find_light_node(light, "ShaderNodeOutputLight") or nt.nodes.new("ShaderNodeOutputLight")
    tex = nt.nodes.get(COOKIE_TEX_NODE) or nt.nodes.new("ShaderNodeTexImage")
    tex.name = COOKIE_TEX_NODE
    tex.label = "BeamNG Cookie"
    tex.extension = "CLIP"
    tex.location = (-560, 120)

    coord = nt.nodes.get(COOKIE_COORD_NODE) or nt.nodes.new("ShaderNodeTexCoord")
    coord.name = COOKIE_COORD_NODE
    coord.label = "BeamNG Cookie Coordinates"
    coord.location = (-760, 120)

    path = resolve_cookie_path(cookie)
    if path:
        try:
            tex.image = bpy.data.images.load(str(path), check_existing=True)
        except Exception:
            tex.image = None
    else:
        tex.image = None

    emission.location = (-180, 0)
    output.location = (120, 0)
    if "Strength" in emission.inputs:
        emission.inputs["Strength"].default_value = 1.0

    for link in list(nt.links):
        if link.to_node == emission and link.to_socket == emission.inputs["Color"]:
            nt.links.remove(link)
        elif link.to_node == output and link.to_socket == output.inputs["Surface"]:
            nt.links.remove(link)

    if "UV" in coord.outputs and "Vector" in tex.inputs:
        nt.links.new(coord.outputs["UV"], tex.inputs["Vector"])
    nt.links.new(tex.outputs["Color"], emission.inputs["Color"])
    nt.links.new(emission.outputs["Emission"], output.inputs["Surface"])


def set_prop_if_changed(props, name: str, value, epsilon: float = 1e-6):
    current = getattr(props, name)
    if isinstance(value, tuple):
        if len(current) == len(value) and all(abs(float(a) - float(b)) <= epsilon for a, b in zip(current, value)):
            return
    elif isinstance(value, str):
        if current == value:
            return
    elif abs(float(current) - float(value)) <= epsilon:
        return
    setattr(props, name, value)


def update_light_from_beamng_props(self, _context):
    if _SYNCING_FROM_BLENDER:
        return
    light = self.id_data
    if isinstance(light, Light) and light.type in {"POINT", "SPOT"}:
        apply_beamng_light(light)


def update_material_from_beamng_props(self, _context):
    if _SYNCING_FROM_BLENDER:
        return
    mat = self.id_data
    if isinstance(mat, Material):
        apply_beamng_emissive(mat)


def update_cookie_from_beamng_props(self, _context):
    light = self.id_data
    if isinstance(light, Light):
        set_light_cookie(light, self.cookie)
        apply_cookie_nodes(light, self.cookie)


def set_light_cookie(light: Light, cookie_value):
    cookie = str(cookie_value or "").strip()
    targets = [light]
    targets.extend(obj for obj in bpy.data.objects if obj.type == "LIGHT" and obj.data == light)
    for target in targets:
        if cookie:
            target["cookie"] = cookie
        elif "cookie" in target:
            del target["cookie"]


class BeamNGLightProperties(PropertyGroup):
    use_temperature: BoolProperty(
        name="Use Kelvin",
        description="Use BeamNG editor Kelvin conversion for the light color",
        default=False,
        update=update_light_from_beamng_props,
    )
    temperature: FloatProperty(
        name="Temperature",
        description="BeamNG color temperature in Kelvin",
        default=6500.0,
        min=1667.0,
        max=25000.0,
        update=update_light_from_beamng_props,
    )
    color: FloatVectorProperty(
        name="Linear Color",
        description="BeamNG linear RGB color",
        subtype="COLOR",
        size=3,
        min=0.0,
        default=(1.0, 1.0, 1.0),
        update=update_light_from_beamng_props,
    )
    point_lumens: FloatProperty(
        name="Intensity",
        description="BeamNG PointLight intensity in lumens",
        default=5000.0,
        min=0.0,
        update=update_light_from_beamng_props,
    )
    spot_candela: FloatProperty(
        name="Intensity",
        description="BeamNG SpotLight intensity in candelas",
        default=5000.0,
        min=0.0,
        update=update_light_from_beamng_props,
    )
    radius: FloatProperty(
        name="Radius",
        description="BeamNG PointLight attenuation radius in meters",
        default=POINT_DEFAULT_RADIUS,
        min=0.0,
        unit="LENGTH",
        update=update_light_from_beamng_props,
    )
    range: FloatProperty(
        name="Range",
        description="BeamNG SpotLight attenuation range in meters",
        default=SPOT_DEFAULT_RANGE,
        min=0.0,
        unit="LENGTH",
        update=update_light_from_beamng_props,
    )
    inner_angle: FloatProperty(
        name="Inner Angle",
        description="BeamNG SpotLight inner full cone angle in degrees",
        default=40.0,
        min=0.0,
        max=179.999,
        update=update_light_from_beamng_props,
    )
    outer_angle: FloatProperty(
        name="Outer Angle",
        description="BeamNG SpotLight outer full cone angle in degrees",
        default=45.0,
        min=0.001,
        max=179.999,
        update=update_light_from_beamng_props,
    )
    shadow_size: FloatProperty(
        name="Blender Shadow Size",
        description="Blender soft shadow radius in meters; BeamNG radius/range is attenuation distance",
        default=0.0,
        min=0.0,
        unit="LENGTH",
        update=update_light_from_beamng_props,
    )
    use_blender_cutoff: BoolProperty(
        name="Preview Range In EEVEE",
        description="Set Blender EEVEE custom distance from BeamNG radius/range",
        default=True,
        update=update_light_from_beamng_props,
    )
    cookie: StringProperty(
        name="Cookie",
        description="BeamNG cookie texture path stored/exported as the light cookie field",
        subtype="FILE_PATH",
        default="",
        update=update_cookie_from_beamng_props,
    )


class BeamNGEmissiveProperties(PropertyGroup):
    factor: FloatVectorProperty(
        name="Emissive Factor",
        description="BeamNG emissiveFactor, linear RGB",
        subtype="COLOR",
        size=3,
        min=0.0,
        default=(1.0, 1.0, 1.0),
        update=update_material_from_beamng_props,
    )
    use_nits: BoolProperty(
        name="Use Nits",
        description="Use BeamNG emissiveIntensityNits; off uses BeamNG's legacy 2000 nits fallback",
        default=True,
        update=update_material_from_beamng_props,
    )
    nits: FloatProperty(
        name="Nits",
        description="BeamNG emissiveIntensityNits",
        default=4000.0,
        min=0.0,
        update=update_material_from_beamng_props,
    )


def apply_beamng_light(light: Light):
    props = light.beamng_lighting
    color = kelvin_to_rgb_linear(props.temperature) if props.use_temperature else tuple(props.color)

    light.color = color
    light.normalize = True
    light.energy = 0.0
    light.shadow_soft_size = props.shadow_size
    light.use_custom_distance = bool(props.use_blender_cutoff)

    if light.type == "POINT":
        light.energy = lumens_to_blender_watts(props.point_lumens)
        if props.use_blender_cutoff:
            light.cutoff_distance = props.radius
        light["beamng_class"] = "PointLight"
        light["beamng_intensity"] = props.point_lumens
        light["beamng_brightness"] = point_brightness_from_lumens(props.point_lumens)
        light["beamng_radius"] = props.radius
    elif light.type == "SPOT":
        outer = max(props.outer_angle, 0.001)
        inner = _clamp(props.inner_angle, 0.0, outer)
        light.energy = candela_to_blender_watts(props.spot_candela)
        light.spot_size = math.radians(outer)
        light.spot_blend = _clamp(1.0 - (inner / outer), 0.0, 1.0)
        light.use_soft_falloff = True
        if props.use_blender_cutoff:
            light.cutoff_distance = props.range
        light["beamng_class"] = "SpotLight"
        light["beamng_intensity"] = props.spot_candela
        light["beamng_brightness"] = spot_brightness_from_candela(props.spot_candela)
        light["beamng_range"] = props.range
        light["beamng_innerAngle"] = inner
        light["beamng_outerAngle"] = outer

    light["beamng_color"] = tuple(color)
    light["beamng_useColorTemperature"] = props.use_temperature
    set_light_cookie(light, props.cookie)
    apply_cookie_nodes(light, props.cookie)
    if props.use_temperature:
        light["beamng_temperature"] = props.temperature


def copy_blender_light_to_beamng(light: Light, obj=None):
    global _SYNCING_FROM_BLENDER
    props = light.beamng_lighting
    _SYNCING_FROM_BLENDER = True
    try:
        set_prop_if_changed(props, "color", tuple(light.color))
        set_prop_if_changed(props, "shadow_size", getattr(light, "shadow_soft_size", 0.0))
        cookie = light.get("cookie", "")
        if not cookie and obj is not None:
            cookie = obj.get("cookie", "")
        if cookie:
            set_prop_if_changed(props, "cookie", str(cookie))
            set_light_cookie(light, cookie)
            apply_cookie_nodes(light, cookie)
        if light.type == "POINT":
            set_prop_if_changed(props, "point_lumens", max(0.0, float(light.energy)) * LM_PER_W)
            if getattr(light, "use_custom_distance", False):
                set_prop_if_changed(props, "radius", float(getattr(light, "cutoff_distance", POINT_DEFAULT_RADIUS)))
        elif light.type == "SPOT":
            outer = math.degrees(float(light.spot_size))
            set_prop_if_changed(props, "spot_candela", max(0.0, float(light.energy)) * LM_PER_W / (4.0 * math.pi))
            set_prop_if_changed(props, "outer_angle", outer)
            set_prop_if_changed(props, "inner_angle", outer * (1.0 - _clamp(float(light.spot_blend), 0.0, 1.0)))
            if getattr(light, "use_custom_distance", False):
                set_prop_if_changed(props, "range", float(getattr(light, "cutoff_distance", SPOT_DEFAULT_RANGE)))
    finally:
        _SYNCING_FROM_BLENDER = False


def apply_beamng_emissive(mat: Material):
    props = mat.beamng_emissive
    bsdf = ensure_principled(mat)
    factor = tuple(float(v) for v in props.factor)
    strength = props.nits if props.use_nits else EMISSIVE_FALLBACK_NITS
    set_principled_input(bsdf, ("Emission Color", "Emission"), (factor[0], factor[1], factor[2], 1.0))
    set_principled_input(bsdf, ("Emission Strength",), max(0.0, strength))
    mat["beamng_emissive_enabled"] = True
    mat["beamng_emissiveFactor"] = factor
    mat["beamng_emissiveIntensityNits"] = props.nits if props.use_nits else -1.0


def copy_blender_emissive_to_beamng(mat: Material):
    global _SYNCING_FROM_BLENDER
    bsdf = find_principled(mat)
    emission_color = get_principled_input(bsdf, ("Emission Color", "Emission"))
    emission_strength = get_principled_input(bsdf, ("Emission Strength",))
    if emission_color is None or emission_strength is None:
        return

    props = mat.beamng_emissive
    color = tuple(float(v) for v in emission_color.default_value[:3])
    strength = max(0.0, float(emission_strength.default_value))
    _SYNCING_FROM_BLENDER = True
    try:
        set_prop_if_changed(props, "factor", color)
        if props.use_nits:
            set_prop_if_changed(props, "nits", strength)
    finally:
        _SYNCING_FROM_BLENDER = False


def sync_from_blender_handler(_scene, depsgraph):
    for update in depsgraph.updates:
        updated_id = update.id
        if isinstance(updated_id, Light) and updated_id.type in {"POINT", "SPOT"} and hasattr(updated_id, "beamng_lighting"):
            copy_blender_light_to_beamng(updated_id)
        elif isinstance(updated_id, Material) and hasattr(updated_id, "beamng_emissive"):
            copy_blender_emissive_to_beamng(updated_id)
        elif getattr(updated_id, "type", None) == "LIGHT":
            light = getattr(updated_id, "data", None)
            if isinstance(light, Light) and light.type in {"POINT", "SPOT"} and hasattr(light, "beamng_lighting"):
                copy_blender_light_to_beamng(light)


def sync_active_selection():
    obj = bpy.context.object
    if obj and obj.type == "LIGHT" and isinstance(obj.data, Light) and obj.data.type in {"POINT", "SPOT"} and hasattr(obj.data, "beamng_lighting"):
        copy_blender_light_to_beamng(obj.data, obj)
    mat = get_active_material(bpy.context)
    if mat and hasattr(mat, "beamng_emissive"):
        copy_blender_emissive_to_beamng(mat)


def light_to_beamng_dict(light: Light) -> dict:
    props = light.beamng_lighting
    color = kelvin_to_rgb_linear(props.temperature) if props.use_temperature else tuple(props.color)
    if light.type == "SPOT":
        outer = max(props.outer_angle, 0.001)
        inner = _clamp(props.inner_angle, 0.0, outer)
        result = {
            "class": "SpotLight",
            "color": [color[0], color[1], color[2], 1.0],
            "intensity": props.spot_candela,
            "brightness": spot_brightness_from_candela(props.spot_candela),
            "range": props.range,
            "innerAngle": inner,
            "outerAngle": outer,
            "useColorTemperature": "true" if props.use_temperature else "false",
        }
    else:
        result = {
            "class": "PointLight",
            "color": [color[0], color[1], color[2], 1.0],
            "intensity": props.point_lumens,
            "brightness": point_brightness_from_lumens(props.point_lumens),
            "radius": props.radius,
            "useColorTemperature": "true" if props.use_temperature else "false",
        }

    cookie = props.cookie.strip()
    if cookie:
        result["cookie"] = cookie
    return result


def emissive_to_beamng_dict(mat: Material) -> dict:
    props = mat.beamng_emissive
    return {
        "emissive": True,
        "emissiveFactor": [props.factor[0], props.factor[1], props.factor[2]],
        "emissiveIntensityNits": props.nits if props.use_nits else -1,
    }


class BEAMNG_OT_create_light(Operator):
    bl_idname = "beamng_light_validation.create_light"
    bl_label = "Create BeamNG Light"
    bl_options = {"REGISTER", "UNDO"}

    kind: EnumProperty(
        name="Kind",
        items=(
            ("POINT", "PointLight", "Create a BeamNG PointLight preview"),
            ("SPOT", "SpotLight", "Create a BeamNG SpotLight preview"),
        ),
        default="POINT",
    )

    def execute(self, context):
        name = "BeamNG_PointLight" if self.kind == "POINT" else "BeamNG_SpotLight"
        light = bpy.data.lights.new(name, self.kind)
        obj = bpy.data.objects.new(name, light)
        context.collection.objects.link(obj)
        obj.location = context.scene.cursor.location
        context.view_layer.objects.active = obj
        obj.select_set(True)
        apply_beamng_light(light)
        return {"FINISHED"}


class BEAMNG_OT_copy_light_json(Operator):
    bl_idname = "beamng_light_validation.copy_light_json"
    bl_label = "Copy Light JSON"
    bl_options = {"REGISTER"}

    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == "LIGHT" and context.object.data.type in {"POINT", "SPOT"}

    def execute(self, context):
        copy_blender_light_to_beamng(context.object.data, context.object)
        context.window_manager.clipboard = json.dumps(light_to_beamng_dict(context.object.data), separators=(",", ":"))
        self.report({"INFO"}, "Copied BeamNG light JSON to clipboard")
        return {"FINISHED"}


class BEAMNG_OT_copy_emissive_json(Operator):
    bl_idname = "beamng_light_validation.copy_emissive_json"
    bl_label = "Copy Emissive JSON"
    bl_options = {"REGISTER"}

    @classmethod
    def poll(cls, context):
        return get_active_material(context) is not None

    def execute(self, context):
        mat = get_active_material(context)
        copy_blender_emissive_to_beamng(mat)
        context.window_manager.clipboard = json.dumps(emissive_to_beamng_dict(mat), separators=(",", ":"))
        self.report({"INFO"}, "Copied BeamNG emissive JSON to clipboard")
        return {"FINISHED"}


class BEAMNG_PT_lighting_sidebar(Panel):
    bl_label = "BeamNG Light Validation"
    bl_idname = "BEAMNG_PT_lighting_sidebar"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "BeamNG"

    def draw(self, context):
        layout = self.layout

        row = layout.row(align=True)
        op = row.operator("beamng_light_validation.create_light", text="Point")
        op.kind = "POINT"
        op = row.operator("beamng_light_validation.create_light", text="Spot")
        op.kind = "SPOT"

        obj = context.object
        if obj and obj.type == "LIGHT" and obj.data.type in {"POINT", "SPOT"}:
            draw_light_properties(layout, obj.data, obj)
        else:
            layout.label(text="Select a point or spot light.")

        layout.separator()
        mat = get_active_material(context)
        if mat:
            draw_emissive_properties(layout, mat)
        else:
            layout.label(text="Select an object with a material.")


class BEAMNG_PT_light_data(Panel):
    bl_label = "BeamNG"
    bl_idname = "BEAMNG_PT_light_data"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "data"

    @classmethod
    def poll(cls, context):
        return context.light and context.light.type in {"POINT", "SPOT"}

    def draw(self, context):
        draw_light_properties(self.layout, context.light, context.object)


class BEAMNG_PT_material_data(Panel):
    bl_label = "BeamNG Emissive"
    bl_idname = "BEAMNG_PT_material_data"
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "material"

    @classmethod
    def poll(cls, context):
        return context.material is not None

    def draw(self, context):
        draw_emissive_properties(self.layout, context.material)


def draw_light_properties(layout, light: Light, obj=None):
    props = light.beamng_lighting
    layout.use_property_split = True
    layout.use_property_decorate = False

    box = layout.box()
    box.label(text="BeamNG Light Values")
    row = box.row(align=True)
    row.operator("beamng_light_validation.copy_light_json", text="Copy JSON")

    col = box.column()
    col.prop(props, "use_temperature")
    if props.use_temperature:
        col.prop(props, "temperature")
    else:
        col.prop(props, "color")

    if light.type == "POINT":
        col.prop(props, "point_lumens", text="Lumens")
        col.prop(props, "radius")
        brightness = point_brightness_from_lumens(props.point_lumens)
        col.label(text=f"BeamNG brightness: {brightness:.6g}")
    elif light.type == "SPOT":
        col.prop(props, "spot_candela", text="Candela")
        col.prop(props, "range")
        col.prop(props, "inner_angle")
        col.prop(props, "outer_angle")
        brightness = spot_brightness_from_candela(props.spot_candela)
        col.label(text=f"BeamNG brightness: {brightness:.6g}")

    col.separator()
    col.prop(props, "cookie")
    col.prop(props, "shadow_size")
    col.prop(props, "use_blender_cutoff")
    col.label(text=f"Blender energy: {light.energy:.6g} W")


def draw_emissive_properties(layout, mat: Material):
    props = mat.beamng_emissive
    layout.use_property_split = True
    layout.use_property_decorate = False

    box = layout.box()
    box.label(text="BeamNG Emissive Values")
    row = box.row(align=True)
    row.operator("beamng_light_validation.copy_emissive_json", text="Copy JSON")

    col = box.column()
    col.prop(props, "factor")
    col.prop(props, "use_nits")
    if props.use_nits:
        col.prop(props, "nits")
        col.label(text=f"Shader emissive: factor * {props.nits:.6g}")
    else:
        col.label(text=f"Legacy preview fallback: factor * {EMISSIVE_FALLBACK_NITS:.0f}")


classes = (
    BeamNGLightProperties,
    BeamNGEmissiveProperties,
    BEAMNG_OT_create_light,
    BEAMNG_OT_copy_light_json,
    BEAMNG_OT_copy_emissive_json,
    BEAMNG_PT_lighting_sidebar,
    BEAMNG_PT_light_data,
    BEAMNG_PT_material_data,
)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    Light.beamng_lighting = PointerProperty(type=BeamNGLightProperties)
    Material.beamng_emissive = PointerProperty(type=BeamNGEmissiveProperties)
    if sync_from_blender_handler not in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.append(sync_from_blender_handler)
    bpy.msgbus.subscribe_rna(
        key=(bpy.types.LayerObjects, "active"),
        owner=_MSGBUS_OWNER,
        args=(),
        notify=sync_active_selection,
    )


def unregister():
    bpy.msgbus.clear_by_owner(_MSGBUS_OWNER)
    if sync_from_blender_handler in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.remove(sync_from_blender_handler)
    if hasattr(Material, "beamng_emissive"):
        del Material.beamng_emissive
    if hasattr(Light, "beamng_lighting"):
        del Light.beamng_lighting
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()
