"""Marimo adapter for `topovec.mgl` scenes and headless image containers.

This module does not define rendering semantics. Instead it mirrors the
backend-agnostic scene/property model from :mod:`topovec.mgl` into anywidget
state so the same scenes can be reused in notebooks, headless scripts, and
other frontends.
"""

from __future__ import annotations

import base64
import copy
import functools
import io
from dataclasses import dataclass
from typing import Any

import numpy as np
from PIL import Image

from ..core import ListProperty, NumericProperty, Property, System, UpdateMask
from ..mgl.camera import Camera
from ..viewer import session as _viewer_session_core
from ..viewer.residency import SceneResidencyManager
from ._assets import (
    camera_viewport_css,
    camera_viewport_esm,
    director_field_inspect_css,
    director_field_inspect_esm,
)

DEFAULT_CONTROLS_TEXT = (
    "Drag: orbit | Shift+drag: pan | Wheel: zoom | "
    "Shift+wheel: roll"
)

PROJECTION_LABELS = ("Perspective", "Orthographic")
DEFAULT_NUMERIC_STEP = 0.001


@dataclass(slots=True)
class DirectorFieldView:
    """Bundle returned by :func:`inspect_view`.

    The bundle keeps the Python-side :class:`RenderSession` together with the
    marimo widget so advanced callers can inspect or control the live session
    without reassembling the UI manually.
    """

    session: "RenderSession"
    widget: Any
    layout: Any


@dataclass(slots=True)
class _SessionMessageOutcome:
    handled: bool
    publish_frame: bool = False
    publish_camera: bool = False
    publish_properties: bool = False
    publish_summary: bool = False


def serialize_camera(camera: Camera) -> dict[str, Any]:
    """Convert a live camera into a JSON-friendly backend-neutral snapshot."""
    return {
        "look_at": [float(v) for v in np.asarray(camera.look_at, dtype=float)],
        "look_direction": [
            float(v) for v in np.asarray(camera.look_direction, dtype=float)
        ],
        "look_up": [float(v) for v in np.asarray(camera.look_up, dtype=float)],
        "look_scale": float(camera.look_scale),
        "perspective": bool(camera.perspective_is_on),
    }


def restore_camera(camera: Camera, state: dict[str, Any]) -> Camera:
    """Apply a serialized snapshot back to an existing camera instance."""
    camera.look_at = np.asarray(state["look_at"], dtype=np.float32)
    camera.look_direction = np.asarray(state["look_direction"], dtype=np.float32)
    camera.look_up = np.asarray(state["look_up"], dtype=np.float32)
    camera.look_scale = float(state["look_scale"])
    camera.perspective_is_on = bool(state["perspective"])
    return camera.normalize()


def _normalize_camera_state(camera_state: dict[str, Any]) -> dict[str, Any]:
    camera = Camera(perspective=bool(camera_state["perspective"]))
    return serialize_camera(restore_camera(camera, camera_state))


def camera_state_signature(camera_state: dict[str, Any]) -> tuple[Any, ...]:
    """Create a rounded signature used to compare camera states."""
    state = _normalize_camera_state(camera_state)

    def rounded(values: list[float]) -> tuple[float, ...]:
        return tuple(round(float(v), 6) for v in values)

    return (
        rounded(state["look_at"]),
        rounded(state["look_direction"]),
        rounded(state["look_up"]),
        round(float(state["look_scale"]), 6),
        bool(state["perspective"]),
    )


def _format_python_float(value: float) -> str:
    return format(float(value), ".6g")


def _format_python_value(value: Any) -> str:
    value = _coerce_snapshot_value(value)
    if isinstance(value, bool):
        return "True" if value else "False"
    if isinstance(value, (int, np.integer)) and not isinstance(value, bool):
        return str(int(value))
    if isinstance(value, (float, np.floating)):
        return _format_python_float(float(value))
    if isinstance(value, str):
        return repr(value)
    if isinstance(value, list):
        return "[" + ", ".join(_format_python_value(item) for item in value) + "]"
    if isinstance(value, tuple):
        inner = ", ".join(_format_python_value(item) for item in value)
        if len(value) == 1:
            inner += ","
        return f"({inner})"
    if value is None:
        return "None"
    return repr(value)


def _projection_label(perspective: bool) -> str:
    return PROJECTION_LABELS[0] if perspective else PROJECTION_LABELS[1]


def format_camera_summary(camera: Camera, *, topovec_name: str = "tv") -> str:
    """Render the current camera as executable Python code."""
    theta, phi, alpha = camera.get_orientation_angles()
    look_at = ", ".join(
        _format_python_value(v)
        for v in np.asarray(camera.look_at, dtype=float)
    )
    return (
        f"{topovec_name}.mgl.Camera.from_angles("
        f"theta={_format_python_value(theta)}, "
        f"phi={_format_python_value(phi)}, "
        f"alpha={_format_python_value(alpha)}, "
        f"scale={_format_python_value(camera.look_scale)}, "
        f"perspective={camera.perspective_is_on}, "
        f"look_at=[{look_at}])"
    )


def pil_image_to_data_url(image: Image.Image, image_format: str = "PNG") -> str:
    """Encode a PIL image for direct use in browser widget payloads."""
    buffer = io.BytesIO()
    image.save(buffer, format=image_format)
    payload = base64.b64encode(buffer.getvalue()).decode("ascii")
    mime = f"image/{image_format.lower()}"
    return f"data:{mime};base64,{payload}"


def apply_camera_command(
    camera: Camera,
    *,
    event_type: str,
    dx: float = 0.0,
    dy: float = 0.0,
    wheel: float = 0.0,
    rotate_rate: float = 2 * np.pi,
    roll_rate: float = 0.02 * np.pi,
    pan_rate: float = 2.0,
    zoom_rate: float = 0.12,
) -> Camera:
    """Apply one normalized interaction event to a camera.

    This helper is shared by widget event handlers and other adapters that want
    to reuse the same interaction semantics without depending on marimo.
    """
    dx = float(dx)
    dy = float(dy)
    wheel = float(wheel)

    if event_type == "rotate":
        camera.rotate_field_of_view(x=-dx, y=dy, rate=rotate_rate)
    elif event_type == "pan":
        camera.move_field_of_view(x=-dx, y=dy, rate=pan_rate)
    elif event_type == "roll":
        camera.rotate_around(z=-1.0, rate=dx * roll_rate)
    elif event_type == "zoom":
        camera.look_scale *= float(np.exp(np.clip(wheel, -8.0, 8.0) * zoom_rate))
    elif event_type in {"", "noop"}:
        return camera
    else:
        raise ValueError(f"Unsupported camera event: {event_type!r}")
    return camera


def _coerce_snapshot_value(value: Any) -> Any:
    if isinstance(value, np.generic):
        return value.item()
    if isinstance(value, np.ndarray):
        return value.tolist()
    if isinstance(value, tuple):
        return [_coerce_snapshot_value(v) for v in value]
    if isinstance(value, list):
        return [_coerce_snapshot_value(v) for v in value]
    return value


def _copy_shared_value(value: Any) -> Any:
    return copy.deepcopy(_coerce_snapshot_value(value))


def _shared_values_equal(left: Any, right: Any) -> bool:
    if isinstance(left, np.ndarray) or isinstance(right, np.ndarray):
        return np.array_equal(np.asarray(left), np.asarray(right))
    try:
        return bool(left == right)
    except Exception:
        return False


def _coerce_director_field_state(state: Any) -> np.ndarray:
    array = np.asarray(state)
    if array.ndim < 4:
        raise ValueError(
            "`inspect` expects a director field with at least three spatial "
            f"dimensions and a trailing vector axis, got shape {array.shape!r}."
        )
    if array.shape[-1] != 3:
        raise ValueError(
            "`inspect` expects the last state axis to have length 3, "
            f"got shape {array.shape!r}."
        )
    return array


def _infer_cubic_system_from_state(state: np.ndarray) -> System:
    return System.cubic(size=tuple(int(v) for v in state.shape[:3]))


def _attach_live_refs(target: Any, **refs: Any) -> Any:
    for name, value in refs.items():
        try:
            setattr(target, name, value)
        except Exception:
            continue
    return target


def _set_widget_frame_payload(target: Any, payload: dict[str, Any]) -> None:
    target.image_data = str(payload["image_data"])
    target.status_text = str(payload["status_text"])
    target.projection_mode = str(payload["projection_mode"])
    target.camera_state = dict(payload["camera_state"])
    target.width = int(payload.get("width", getattr(target, "width", 512)))
    target.height = int(payload.get("height", getattr(target, "height", 512)))


def _apply_bound_session_message(
    session: "RenderSession",
    message_type: str,
    content: dict[str, Any],
) -> _SessionMessageOutcome:
    if message_type == "camera_delta":
        changed = session.apply_camera_delta(
            event_type=str(content.get("event_type") or ""),
            dx=float(content.get("dx") or 0.0),
            dy=float(content.get("dy") or 0.0),
            wheel=float(content.get("wheel") or 0.0),
        )
        return _SessionMessageOutcome(
            handled=True,
            publish_frame=changed,
            publish_camera=changed,
            publish_summary=changed,
        )

    if message_type == "camera_commit":
        changed = session.commit_camera_interaction()
        return _SessionMessageOutcome(
            handled=True,
            publish_frame=changed,
            publish_camera=changed,
            publish_summary=changed,
        )

    if message_type == "toggle_perspective":
        changed = session.apply_camera_button("toggle_perspective")
        return _SessionMessageOutcome(
            handled=True,
            publish_frame=changed,
            publish_camera=changed,
            publish_summary=changed,
        )

    if message_type == "reset_camera":
        changed = session.apply_camera_button("reset")
        return _SessionMessageOutcome(
            handled=True,
            publish_frame=changed,
            publish_camera=changed,
            publish_summary=changed,
        )

    if message_type == "snap_camera":
        changed = session.apply_camera_button("snap")
        return _SessionMessageOutcome(
            handled=True,
            publish_frame=changed,
            publish_camera=changed,
        )

    if message_type == "switch_scene":
        residency_manager = getattr(session, "_scene_residency_manager", None)
        if residency_manager is None:
            changed = session.switch_scene(str(content.get("scene_name") or ""))
        else:
            changed = residency_manager.switch_scene(str(content.get("scene_name") or ""))
        return _SessionMessageOutcome(
            handled=True,
            publish_frame=changed,
            publish_camera=changed,
            publish_properties=changed,
            publish_summary=changed,
        )

    if message_type == "apply_property_change":
        session.apply_property_input(
            str(content.get("group_id") or ""),
            int(content.get("prop_index") or 0),
            content.get("value"),
            expected_field=(
                None
                if content.get("expected_field") is None
                else str(content.get("expected_field"))
            ),
            expected_title=(
                None
                if content.get("expected_title") is None
                else str(content.get("expected_title"))
            ),
        )
        return _SessionMessageOutcome(
            handled=True,
            publish_frame=True,
            publish_camera=True,
            publish_properties=True,
            publish_summary=True,
        )

    return _SessionMessageOutcome(handled=False)


def _property_snapshot(prop: Property, index: int) -> dict[str, Any]:
    spec = dict(prop.spec())
    snapshot = {
        "index": int(index),
        "kind": str(spec["kind"]),
        "field": str(spec["field"]),
        "title": str(spec["title"]),
        "value": _coerce_snapshot_value(spec["value"]),
        "invalidates": bool(spec["invalidates"]),
        "param_key": spec.get("param_key"),
    }
    if isinstance(prop, NumericProperty):
        snapshot.update(
            {
                "min": float(spec["min"]),
                "max": float(spec["max"]),
                "count": int(spec["count"]),
            }
        )
    elif isinstance(prop, ListProperty):
        snapshot.update(
            {
                "values": [_coerce_snapshot_value(v) for v in spec["values"]],
                "selected_index": int(spec["selected_index"]),
            }
        )
    return snapshot


def _numeric_step(prop_snapshot: dict[str, Any]) -> float | None:
    count = int(prop_snapshot.get("count", 0))
    minimum = float(prop_snapshot.get("min", 0.0))
    maximum = float(prop_snapshot.get("max", 0.0))
    if count <= 1:
        return None
    delta = maximum - minimum
    if not np.isfinite(delta) or delta <= 0:
        return None
    step = delta / float(count - 1)
    if not np.isfinite(step) or step <= 0:
        return None
    return float(step)


def _default_numeric_step() -> float:
    return DEFAULT_NUMERIC_STEP


def _effective_numeric_step(prop_snapshot: dict[str, Any]) -> float:
    step = _numeric_step(prop_snapshot)
    if step is not None:
        return step
    return _default_numeric_step()


def _numeric_bounds(
    prop_snapshot: dict[str, Any],
) -> tuple[float, float, float, bool]:
    """Normalize numeric metadata and report whether the current value fits it."""
    minimum = float(prop_snapshot.get("min", 0.0))
    maximum = float(prop_snapshot.get("max", 0.0))
    value = float(prop_snapshot.get("value", 0.0))

    if np.isfinite(minimum) and np.isfinite(maximum) and minimum > maximum:
        minimum, maximum = maximum, minimum

    value_in_bounds = (
        np.isfinite(minimum)
        and np.isfinite(maximum)
        and minimum <= value <= maximum
    )
    return minimum, maximum, value, value_in_bounds


def _numeric_step_decimals(step: float | None) -> int | None:
    if step is None:
        return None
    step = float(abs(step))
    if not np.isfinite(step) or step <= 0:
        return None
    text = f"{step:.12f}".rstrip("0").rstrip(".")
    if "." not in text:
        return 0
    return len(text.split(".", 1)[1])


def _quantize_numeric_value(value: float, prop_snapshot: dict[str, Any]) -> float:
    """Snap numeric input to either count-based or fallback resolution."""
    step = _effective_numeric_step(prop_snapshot)
    minimum, maximum, _, value_in_bounds = _numeric_bounds(prop_snapshot)
    snapped = float(value)

    if value_in_bounds:
        snapped = minimum + round((snapped - minimum) / step) * step
        snapped = float(np.clip(snapped, minimum, maximum))
    else:
        snapped = round(snapped / step) * step

    decimals = _numeric_step_decimals(step)
    if decimals is not None:
        snapped = round(snapped, decimals)

    return float(snapped)


def _decorate_property_snapshot(prop_snapshot: dict[str, Any]) -> dict[str, Any]:
    snapshot = dict(prop_snapshot)
    kind = str(snapshot.get("kind", ""))

    if kind == "numeric":
        minimum, maximum, value, value_in_bounds = _numeric_bounds(snapshot)
        step = _effective_numeric_step(snapshot)
        snapshot["value"] = _quantize_numeric_value(value, snapshot)
        snapshot["step"] = step
        snapshot["value_in_bounds"] = bool(value_in_bounds)
        snapshot["input_kind"] = (
            "slider"
            if step is not None and maximum > minimum and value_in_bounds
            else "number"
        )
    elif kind == "boolean":
        snapshot["input_kind"] = "switch"
    elif kind == "list":
        snapshot["input_kind"] = "dropdown"
    else:
        snapshot["input_kind"] = "display"

    snapshot["row_kind"] = (
        "stack" if snapshot["input_kind"] == "slider" else "inline"
    )

    return snapshot


def _missing_marimo_stack_error(
    missing: tuple[str, ...] = ("anywidget", "traitlets"),
) -> ImportError:
    missing_text = ", ".join(missing)
    return ImportError(
        "topovec.marimo.camera_viewer requires the optional widget stack "
        f"({missing_text}) in the active uv environment. "
        "Run `uv sync --all-extras` or `uv sync --extra marimo --extra mgl`, "
        "then restart or re-run the notebook cell."
    )


def _missing_marimo_error() -> ImportError:
    return ImportError(
        "topovec.marimo.camera_viewer requires `marimo` in the active uv environment. "
        "Run `uv sync --all-extras` or `uv sync --extra marimo --extra mgl`."
    )


def _load_marimo():
    try:
        import marimo as mo
    except ModuleNotFoundError as exc:
        raise _missing_marimo_error() from exc
    return mo


def _load_widget_stack():
    missing = []
    try:
        import anywidget
    except ModuleNotFoundError:
        anywidget = None
        missing.append("anywidget")
    try:
        import traitlets
    except ModuleNotFoundError:
        traitlets = None
        missing.append("traitlets")
    if missing:
        raise _missing_marimo_stack_error(tuple(missing))
    return anywidget, traitlets


@functools.lru_cache(maxsize=1)
def _camera_viewport_widget_class():
    """Create and cache the anywidget-backed viewport class."""
    anywidget, traitlets = _load_widget_stack()

    class _CameraViewportWidget(anywidget.AnyWidget):
        _esm = camera_viewport_esm()
        _css = camera_viewport_css()

        image_data = traitlets.Unicode("").tag(sync=True)
        status_text = traitlets.Unicode("").tag(sync=True)
        controls_text = traitlets.Unicode(DEFAULT_CONTROLS_TEXT).tag(sync=True)
        projection_mode = traitlets.Unicode(PROJECTION_LABELS[0]).tag(sync=True)
        camera_state = traitlets.Dict(default_value={}).tag(sync=True)
        width = traitlets.Int(512).tag(sync=True)
        height = traitlets.Int(512).tag(sync=True)
        busy = traitlets.Bool(False).tag(sync=True)
        show_toolbar = traitlets.Bool(True).tag(sync=True)
        use_custom_transport = traitlets.Bool(False).tag(sync=True)
        event_id = traitlets.Int(0).tag(sync=True)
        event_type = traitlets.Unicode("").tag(sync=True)
        event_dx = traitlets.Float(0.0).tag(sync=True)
        event_dy = traitlets.Float(0.0).tag(sync=True)
        event_wheel = traitlets.Float(0.0).tag(sync=True)

        def __init__(
            self,
            image_container: Any,
            state: np.ndarray | None = None,
            *,
            controls_text: str = DEFAULT_CONTROLS_TEXT,
            rotate_rate: float = 2 * np.pi,
            pan_rate: float = 2.0,
            zoom_rate: float = 0.12,
            show_toolbar: bool = True,
            auto_apply_events: bool = True,
            use_custom_transport: bool = False,
        ):
            """Bind an image container and initialize the first rendered frame."""
            super().__init__()
            self._image_container = image_container
            self._camera = image_container.scene.camera
            self._rotate_rate = float(rotate_rate)
            self._pan_rate = float(pan_rate)
            self._zoom_rate = float(zoom_rate)
            self._auto_apply_events = bool(auto_apply_events)
            self._bound_session: RenderSession | None = None
            self._publish_camera_snapshot = None
            self._publish_properties_snapshot = None
            self._last_payload: dict[str, Any] | None = None
            self.controls_text = controls_text
            self.show_toolbar = bool(show_toolbar)
            self.use_custom_transport = bool(use_custom_transport)

            size = getattr(
                image_container,
                "size",
                getattr(image_container, "_size", (512, 512)),
            )
            self.width = int(size[0])
            self.height = int(size[1])

            if state is not None:
                self._image_container.upload(state)

            self._home_state = serialize_camera(self._camera)
            self._register_message_handler()
            self.set_frame_payload(self._render_frame_payload())

        def _register_message_handler(self) -> None:
            on_msg = getattr(self, "on_msg", None)
            if callable(on_msg):
                on_msg(self._handle_custom_message)

        def bind_session(
            self,
            session: RenderSession,
            *,
            publish_camera_snapshot: Any | None = None,
            publish_properties_snapshot: Any | None = None,
        ) -> "_CameraViewportWidget":
            self._bound_session = session
            self.use_custom_transport = True
            if publish_camera_snapshot is not None:
                self._publish_camera_snapshot = publish_camera_snapshot
            if publish_properties_snapshot is not None:
                self._publish_properties_snapshot = publish_properties_snapshot
            return self

        def _publish_camera(self) -> None:
            if self._publish_camera_snapshot is not None and self._bound_session is not None:
                self._publish_camera_snapshot(self._bound_session.camera_snapshot())

        def _publish_properties(self) -> None:
            if (
                self._publish_properties_snapshot is not None
                and self._bound_session is not None
            ):
                self._publish_properties_snapshot(
                    self._bound_session.properties_snapshot()
                )

        def _ack_last_payload(self) -> "_CameraViewportWidget":
            if self._last_payload is not None:
                return self.set_frame_payload(self._last_payload)
            if self._bound_session is not None:
                return self.set_frame_payload(
                    self._bound_session.frame_payload(
                        image_encoder=pil_image_to_data_url
                    )
                )
            return self.set_frame_payload(self._render_frame_payload())

        def _set_error_status(self, message: str) -> None:
            self.busy = True
            try:
                self.status_text = str(message)
            finally:
                self.busy = False

        def _render_frame_payload(self) -> dict[str, Any]:
            image = self._image_container.save()
            return {
                "image_data": pil_image_to_data_url(image),
                "status_text": format_camera_summary(self._camera),
                "projection_mode": _projection_label(self._camera.perspective_is_on),
                "camera_state": serialize_camera(self._camera),
                "width": int(self.width),
                "height": int(self.height),
            }

        def set_frame_payload(self, payload: dict[str, Any]) -> "_CameraViewportWidget":
            """Update browser-visible traits from a rendered session payload."""
            self._last_payload = dict(payload)
            self.busy = True
            try:
                _set_widget_frame_payload(self, payload)
            finally:
                self.busy = False
            return self

        def refresh(self) -> "_CameraViewportWidget":
            """Render the current scene/camera state back into the widget image."""
            return self.set_frame_payload(self._render_frame_payload())

        def set_render_state(self, state: np.ndarray) -> "_CameraViewportWidget":
            """Replace the rendered director/state field without resetting the camera."""
            self._image_container.upload(state)
            return self.refresh()

        def set_home(self) -> "_CameraViewportWidget":
            """Store the current camera pose as the reset target."""
            if self._bound_session is not None:
                self._bound_session.set_home()
            self._home_state = serialize_camera(self._camera)
            return self.refresh()

        def reset_camera(self) -> "_CameraViewportWidget":
            """Restore the saved home camera pose and redraw the frame."""
            if self._bound_session is not None:
                changed = self._bound_session.apply_camera_button("reset")
                if changed:
                    self.set_frame_payload(
                        self._bound_session.frame_payload(
                            image_encoder=pil_image_to_data_url
                        )
                    )
                    self._publish_camera()
                    return self
                return self._ack_last_payload()
            restore_camera(self._camera, self._home_state)
            return self.refresh()

        def set_camera_state(
            self, camera_state: dict[str, Any]
        ) -> "_CameraViewportWidget":
            """Replace the camera with a serialized state and redraw the frame."""
            restore_camera(self._camera, camera_state)
            return self.refresh()

        def _handle_session_message(self, message_type: str, content: dict[str, Any]) -> bool:
            if self._bound_session is None:
                return False
            outcome = _apply_bound_session_message(
                self._bound_session,
                message_type,
                content,
            )
            if not outcome.handled:
                return False
            if outcome.publish_frame:
                self.set_frame_payload(
                    self._bound_session.frame_payload(
                        image_encoder=pil_image_to_data_url
                    )
                )
            else:
                self._ack_last_payload()
            if outcome.publish_camera:
                self._publish_camera()
            if outcome.publish_properties:
                self._publish_properties()
            return True

        def _handle_custom_message(self, _widget, content, _buffers) -> None:
            if not isinstance(content, dict):
                return
            message_type = str(content.get("type") or "")
            if not message_type:
                return

            try:
                handled = self._handle_session_message(message_type, content)
                if not handled:
                    raise ValueError(f"Unsupported viewer message: {message_type!r}")
            except Exception as exc:
                self._set_error_status(f"Render failed: {exc}")

        @traitlets.observe("event_id")
        def _handle_event(self, change):
            """Consume one aggregated frontend event in standalone mode."""
            if not self._auto_apply_events:
                return
            if change["new"] == change["old"]:
                return

            try:
                if self.event_type == "reset":
                    restore_camera(self._camera, self._home_state)
                elif self.event_type == "snap":
                    self._camera.snap()
                elif self.event_type == "toggle_perspective":
                    self._camera.toggle_perspective()
                else:
                    apply_camera_command(
                        self._camera,
                        event_type=self.event_type,
                        dx=self.event_dx,
                        dy=self.event_dy,
                        wheel=self.event_wheel,
                        rotate_rate=self._rotate_rate,
                        pan_rate=self._pan_rate,
                        zoom_rate=self._zoom_rate,
                    )
                self.refresh()
            except Exception as exc:
                self.status_text = f"Render failed: {exc}"
                self.busy = False

    _CameraViewportWidget.__name__ = "CameraViewportWidget"
    _CameraViewportWidget.__qualname__ = "CameraViewportWidget"
    return _CameraViewportWidget


@functools.lru_cache(maxsize=1)
def _composite_director_field_widget_class():
    """Create and cache the self-contained anywidget inspector class."""
    anywidget, traitlets = _load_widget_stack()

    class _CompositeDirectorFieldWidget(anywidget.AnyWidget):
        _esm = director_field_inspect_esm()
        _css = director_field_inspect_css()

        image_data = traitlets.Unicode("").tag(sync=True)
        status_text = traitlets.Unicode("").tag(sync=True)
        controls_text = traitlets.Unicode(DEFAULT_CONTROLS_TEXT).tag(sync=True)
        projection_mode = traitlets.Unicode(PROJECTION_LABELS[0]).tag(sync=True)
        camera_state = traitlets.Dict(default_value={}).tag(sync=True)
        camera_snapshot = traitlets.Dict(default_value={}).tag(sync=True)
        properties_snapshot = traitlets.Dict(default_value={}).tag(sync=True)
        width = traitlets.Int(512).tag(sync=True)
        height = traitlets.Int(512).tag(sync=True)
        summary_rows = traitlets.Int(8).tag(sync=True)
        summary_text = traitlets.Unicode("").tag(sync=True)
        busy = traitlets.Bool(False).tag(sync=True)
        show_toolbar = traitlets.Bool(False).tag(sync=True)
        show_scene_selector = traitlets.Bool(True).tag(sync=True)

        def __init__(
            self,
            session: "RenderSession",
            *,
            controls_text: str = DEFAULT_CONTROLS_TEXT,
            show_scene_selector: bool = True,
            show_toolbar: bool = False,
            summary_rows: int = 8,
        ):
            super().__init__()
            self._bound_session = session
            self._image_container = session.image_container
            self._last_payload: dict[str, Any] | None = None
            self.controls_text = controls_text
            self.show_toolbar = bool(show_toolbar)
            self.show_scene_selector = bool(show_scene_selector)
            self.summary_rows = max(1, int(summary_rows))

            size = getattr(
                self._image_container,
                "size",
                getattr(self._image_container, "_size", (512, 512)),
            )
            self.width = int(size[0])
            self.height = int(size[1])

            self._register_message_handler()
            self.refresh()

        def _register_message_handler(self) -> None:
            on_msg = getattr(self, "on_msg", None)
            if callable(on_msg):
                on_msg(self._handle_custom_message)

        def set_frame_payload(
            self,
            payload: dict[str, Any],
        ) -> "_CompositeDirectorFieldWidget":
            self._last_payload = dict(payload)
            self.busy = True
            try:
                _set_widget_frame_payload(self, payload)
            finally:
                self.busy = False
            return self

        def set_camera_snapshot(
            self,
            snapshot: dict[str, Any],
        ) -> "_CompositeDirectorFieldWidget":
            self.camera_snapshot = dict(snapshot)
            return self

        def set_properties_snapshot(
            self,
            snapshot: dict[str, Any],
        ) -> "_CompositeDirectorFieldWidget":
            self.properties_snapshot = dict(snapshot)
            return self

        def set_summary_text(self, summary_text: str) -> "_CompositeDirectorFieldWidget":
            self.summary_text = str(summary_text)
            return self

        def refresh(self) -> "_CompositeDirectorFieldWidget":
            return self._publish_state(
                publish_frame=True,
                publish_camera=True,
                publish_properties=True,
                publish_summary=True,
            )

        def _publish_state(
            self,
            *,
            publish_frame: bool = False,
            publish_camera: bool = False,
            publish_properties: bool = False,
            publish_summary: bool = False,
        ) -> "_CompositeDirectorFieldWidget":
            if publish_frame:
                self.set_frame_payload(
                    self._bound_session.frame_payload(
                        image_encoder=pil_image_to_data_url
                    )
                )
            if publish_camera:
                self.set_camera_snapshot(self._bound_session.camera_snapshot())
            if publish_properties:
                self.set_properties_snapshot(self._bound_session.properties_snapshot())
            if publish_summary:
                self.set_summary_text(self._bound_session.render_snippet())
            return self

        def _set_error_status(self, message: str) -> None:
            self.busy = True
            try:
                self.status_text = str(message)
            finally:
                self.busy = False

        def _handle_custom_message(self, _widget, content, _buffers) -> None:
            if not isinstance(content, dict):
                return
            message_type = str(content.get("type") or "")
            if not message_type:
                return

            try:
                outcome = _apply_bound_session_message(
                    self._bound_session,
                    message_type,
                    content,
                )
                if not outcome.handled:
                    raise ValueError(
                        f"Unsupported composite viewer message: {message_type!r}"
                    )
                self._publish_state(
                    publish_frame=outcome.publish_frame,
                    publish_camera=outcome.publish_camera,
                    publish_properties=outcome.publish_properties,
                    publish_summary=outcome.publish_summary,
                )
            except Exception as exc:
                self._set_error_status(f"Render failed: {exc}")

    _CompositeDirectorFieldWidget.__name__ = "CompositeDirectorFieldWidget"
    _CompositeDirectorFieldWidget.__qualname__ = "CompositeDirectorFieldWidget"
    return _CompositeDirectorFieldWidget


def _load_builtin_scene_registry() -> dict[str, Any]:
    try:
        from ..mgl import SCENES
    except Exception:
        return {}
    return dict(SCENES)


serialize_camera = _viewer_session_core.serialize_camera
restore_camera = _viewer_session_core.restore_camera
camera_state_signature = _viewer_session_core.camera_state_signature
format_camera_summary = _viewer_session_core.format_camera_summary
apply_camera_command = _viewer_session_core.apply_camera_command
_coerce_director_field_state = _viewer_session_core._coerce_director_field_state
_infer_cubic_system_from_state = _viewer_session_core.infer_cubic_system_from_state
_property_snapshot = _viewer_session_core._property_snapshot
_effective_numeric_step = _viewer_session_core._effective_numeric_step
_quantize_numeric_value = _viewer_session_core._quantize_numeric_value
_decorate_property_snapshot = _viewer_session_core._decorate_property_snapshot


class RenderSession(_viewer_session_core.ViewerSession):
    def _builtin_scene_registry(self) -> dict[str, Any]:
        return _load_builtin_scene_registry()


def camera_viewer(
    image_container: Any,
    state: np.ndarray | None = None,
    *,
    controls_text: str = DEFAULT_CONTROLS_TEXT,
    rotate_rate: float = 2 * np.pi,
    pan_rate: float = 2.0,
    zoom_rate: float = 0.12,
):
    """Create a standalone marimo viewport for an existing image container.

    This is the lowest-level marimo primitive in the module. It exposes camera
    interaction but does not build the full property inspector UI.
    """
    mo = _load_marimo()
    widget = _camera_viewport_widget_class()(
        image_container=image_container,
        state=state,
        controls_text=controls_text,
        rotate_rate=rotate_rate,
        pan_rate=pan_rate,
        zoom_rate=zoom_rate,
        show_toolbar=True,
        auto_apply_events=True,
    )
    return mo.ui.anywidget(widget)

def inspect_view(
    state: np.ndarray,
    *,
    system: System | None = None,
    scene: str = "Layer",
    imgsize: int = 512,
    scalex: bool = False,
    camera_preset: int | None = None,
    show_scene_selector: bool = True,
    show_toolbar: bool = False,
    summary_rows: int = 8,
    controls_text: str = DEFAULT_CONTROLS_TEXT,
    rotate_rate: float = 2 * np.pi,
    roll_rate: float = 0.02 * np.pi,
    pan_rate: float = 2.0,
    zoom_rate: float = 0.12,
) -> DirectorFieldView:
    """Build a self-contained marimo inspector for a director field.

    The helper prepares a default `topovec.mgl` scene, uploads the state, and
    returns a live bundle whose widget mirrors the underlying
    :class:`RenderSession`.
    """
    import topovec.mgl as mgl

    mo = _load_marimo()
    director_state = _coerce_director_field_state(state)
    resolved_system = (
        _infer_cubic_system_from_state(director_state)
        if system is None
        else system
    )
    image_container = mgl.render_prepare(
        scenecls=scene,
        system=resolved_system,
        camera_preset=camera_preset,
        scalex=scalex,
        imgsize=imgsize,
    )
    scene_residency = SceneResidencyManager(
        image_container,
        scene_registry_loader=_load_builtin_scene_registry,
        session_factory=RenderSession,
        session_kwargs={
            "controls_text": controls_text,
            "rotate_rate": rotate_rate,
            "roll_rate": roll_rate,
            "pan_rate": pan_rate,
            "zoom_rate": zoom_rate,
        },
        max_live_scenes=1,
    )
    session = scene_residency.activate(
        system=resolved_system,
        state=director_state,
        preferred_scene_name=scene,
    )
    raw_widget = _composite_director_field_widget_class()(
        session,
        controls_text=controls_text,
        show_scene_selector=show_scene_selector,
        show_toolbar=show_toolbar,
        summary_rows=summary_rows,
    )
    widget = mo.ui.anywidget(raw_widget)
    view = DirectorFieldView(
        session=session,
        widget=widget,
        layout=widget,
    )
    _attach_live_refs(
        widget,
        _topovec_director_field_view=view,
        _topovec_scene_residency=scene_residency,
    )
    return view


def inspect(
    state: np.ndarray,
    *,
    system: System | None = None,
    scene: str = "Layer",
    imgsize: int = 512,
    scalex: bool = False,
    camera_preset: int | None = None,
    show_scene_selector: bool = True,
    show_toolbar: bool = False,
    summary_rows: int = 8,
    controls_text: str = DEFAULT_CONTROLS_TEXT,
    rotate_rate: float = 2 * np.pi,
    roll_rate: float = 0.02 * np.pi,
    pan_rate: float = 2.0,
    zoom_rate: float = 0.12,
) -> Any:
    """Return only the final marimo widget for the default inspector view."""
    view = inspect_view(
        state,
        system=system,
        scene=scene,
        imgsize=imgsize,
        scalex=scalex,
        camera_preset=camera_preset,
        show_scene_selector=show_scene_selector,
        show_toolbar=show_toolbar,
        summary_rows=summary_rows,
        controls_text=controls_text,
        rotate_rate=rotate_rate,
        roll_rate=roll_rate,
        pan_rate=pan_rate,
        zoom_rate=zoom_rate,
    )
    return _attach_live_refs(
        view.layout,
        _topovec_director_field_view=view,
    )


def CameraViewportWidget(*args, **kwargs):
    """Compatibility factory returning the standalone viewport widget."""
    return _camera_viewport_widget_class()(*args, **kwargs)
