function render({ model, el }) {
  el.replaceChildren();

  const root = document.createElement("div");
  root.className = "topovec-composite-widget";

  const main = document.createElement("div");
  main.className = "topovec-composite-main";

  const viewerRoot = document.createElement("div");
  viewerRoot.className = "topovec-camera-viewer";
  viewerRoot.dataset.helpVisible = "true";

  const toolbar = document.createElement("div");
  toolbar.className = "topovec-camera-toolbar";

  const resetButton = document.createElement("button");
  resetButton.type = "button";
  resetButton.textContent = "Reset";

  const snapButton = document.createElement("button");
  snapButton.type = "button";
  snapButton.textContent = "Snap";

  const projectionButton = document.createElement("button");
  projectionButton.type = "button";
  projectionButton.className = "topovec-composite-projection-button";

  const status = document.createElement("div");
  status.className = "topovec-camera-status";

  toolbar.append(resetButton, snapButton, projectionButton, status);

  const viewport = document.createElement("div");
  viewport.className = "topovec-camera-viewport";

  const image = document.createElement("img");
  image.className = "topovec-camera-image";
  image.draggable = false;
  image.alt = "topovec render";

  const busyBadge = document.createElement("div");
  busyBadge.className = "topovec-camera-busy";
  busyBadge.textContent = "Rendering...";

  const help = document.createElement("div");
  help.className = "topovec-camera-help";

  viewport.append(image, busyBadge, help);
  viewerRoot.append(toolbar, viewport);

  const panel = document.createElement("div");
  panel.className = "topovec-composite-panel";

  const panelScroller = document.createElement("div");
  panelScroller.className = "topovec-composite-panel-scroll";
  panel.append(panelScroller);

  main.append(viewerRoot, panel);

  const summary = document.createElement("div");
  summary.className = "topovec-composite-summary";

  const summaryLabel = document.createElement("div");
  summaryLabel.className = "topovec-composite-summary-label";
  summaryLabel.textContent = "Reproduce render";

  const summaryText = document.createElement("textarea");
  summaryText.className = "topovec-composite-summary-text";
  summaryText.readOnly = true;
  summaryText.disabled = true;

  summary.append(summaryLabel, summaryText);
  root.append(main, summary);
  el.appendChild(root);

  const usesCustomTransport = () => true;

  let pendingEvents = [];
  let waitingForRender = false;
  let lastBusy = false;
  let wheelCommitTimer = null;
  let pointerId = null;
  let dragMode = null;
  let lastX = 0;
  let lastY = 0;
  let accumDx = 0;
  let accumDy = 0;
  let dragDirty = false;
  let frame = null;
  let openSection = "Camera";

  const sendMessage = (type, payload = {}) => {
    waitingForRender = true;
    model.send({ type, ...payload });
  };

  const updateImage = () => {
    const src = model.get("image_data");
    if (src && image.src !== src) {
      image.src = src;
    }
  };

  const updateText = () => {
    status.textContent = model.get("status_text") || "";
    help.textContent = model.get("controls_text") || "";
    const mode = model.get("projection_mode") || "Perspective";
    projectionButton.textContent = `Projection: ${mode}`;
    panelScroller
      .querySelectorAll(".topovec-composite-projection-button")
      .forEach((button) => {
        button.textContent = `Projection: ${mode}`;
      });
  };

  const updateGeometry = () => {
    const width = model.get("width") || 512;
    const height = model.get("height") || width;
    viewerRoot.style.maxWidth = "100%";
    viewport.style.aspectRatio = `${width} / ${height}`;
  };

  const updateToolbarVisibility = () => {
    viewerRoot.dataset.showToolbar = model.get("show_toolbar") ? "true" : "false";
  };

  const updateBusy = () => {
    const busy = model.get("busy") ? true : false;
    viewerRoot.dataset.busy = busy ? "true" : "false";
    if (!busy && (lastBusy || waitingForRender)) {
      waitingForRender = false;
      flushPendingEvents();
    }
    lastBusy = busy;
  };

  const updateSummary = () => {
    summaryText.rows = Math.max(1, Number(model.get("summary_rows") || 8));
    summaryText.value = model.get("summary_text") || "";
  };

  const clearScheduledCommit = () => {
    if (wheelCommitTimer !== null) {
      clearTimeout(wheelCommitTimer);
      wheelCommitTimer = null;
    }
  };

  const dropPendingCameraCommits = () => {
    pendingEvents = pendingEvents.filter(
      (event) => event.type !== "camera_commit"
    );
  };

  const normalizeEvent = (eventType, payload = {}) => {
    if (eventType === "reset") {
      return { type: "reset_camera" };
    }
    if (eventType === "snap") {
      return { type: "snap_camera" };
    }
    if (eventType === "toggle_perspective") {
      return { type: "toggle_perspective" };
    }
    if (eventType === "camera_commit") {
      return { type: "camera_commit" };
    }
    return {
      type: "camera_delta",
      event_type: eventType,
      dx: payload.dx ?? 0,
      dy: payload.dy ?? 0,
      wheel: payload.wheel ?? 0,
    };
  };

  const emitEvent = (event) => {
    waitingForRender = true;
    if (usesCustomTransport()) {
      model.send(event);
    }
  };

  const flushPendingEvents = () => {
    if (waitingForRender || model.get("busy")) {
      return;
    }
    const nextEvent = pendingEvents.shift();
    if (!nextEvent) {
      return;
    }
    emitEvent(nextEvent);
  };

  const queueEvent = (eventType, payload = {}) => {
    const event = normalizeEvent(eventType, payload);
    if (event.type === "camera_delta") {
      dropPendingCameraCommits();
    }
    if (event.type === "reset_camera" || event.type === "snap_camera") {
      clearScheduledCommit();
      pendingEvents = [event];
    } else {
      const tail = pendingEvents[pendingEvents.length - 1];
      if (
        tail &&
        tail.type === "camera_delta" &&
        event.type === "camera_delta" &&
        tail.event_type === event.event_type
      ) {
        tail.dx += event.dx;
        tail.dy += event.dy;
        tail.wheel += event.wheel;
      } else if (event.type === "camera_commit") {
        dropPendingCameraCommits();
        pendingEvents.push(event);
      } else {
        pendingEvents.push(event);
      }
    }
    flushPendingEvents();
  };

  const flushDrag = () => {
    frame = null;
    if (!dragMode) {
      return;
    }
    const rect = viewport.getBoundingClientRect();
    const width = Math.max(rect.width, 1);
    const height = Math.max(rect.height, 1);
    if (accumDx === 0 && accumDy === 0) {
      return;
    }
    queueEvent(dragMode, {
      dx: accumDx / width,
      dy: accumDy / height,
    });
    accumDx = 0;
    accumDy = 0;
  };

  const scheduleDragFlush = () => {
    if (frame === null) {
      frame = requestAnimationFrame(flushDrag);
    }
  };

  const queueCameraCommit = () => {
    dropPendingCameraCommits();
    queueEvent("camera_commit");
  };

  const scheduleWheelCommit = () => {
    clearScheduledCommit();
    wheelCommitTimer = setTimeout(() => {
      wheelCommitTimer = null;
      queueCameraCommit();
    }, 160);
  };

  const onReset = () => {
    clearScheduledCommit();
    queueEvent("reset");
  };

  const onSnap = () => {
    clearScheduledCommit();
    queueEvent("snap");
  };

  const onToggleProjection = () => {
    clearScheduledCommit();
    queueEvent("toggle_perspective");
  };

  const onContextMenu = (event) => event.preventDefault();

  const onPointerDown = (event) => {
    clearScheduledCommit();
    dropPendingCameraCommits();
    pointerId = event.pointerId;
    dragMode =
      event.button === 2 || event.shiftKey
        ? "pan"
        : "rotate";
    lastX = event.clientX;
    lastY = event.clientY;
    accumDx = 0;
    accumDy = 0;
    dragDirty = false;
    viewerRoot.dataset.dragMode = dragMode;
    viewport.setPointerCapture(pointerId);
    event.preventDefault();
  };

  const onPointerMove = (event) => {
    if (pointerId !== event.pointerId || dragMode === null) {
      return;
    }
    const deltaX = event.clientX - lastX;
    const deltaY = event.clientY - lastY;
    accumDx += deltaX;
    accumDy += deltaY;
    lastX = event.clientX;
    lastY = event.clientY;
    if (deltaX !== 0 || deltaY !== 0) {
      dragDirty = true;
    }
    scheduleDragFlush();
    event.preventDefault();
  };

  const finishPointer = (event) => {
    if (pointerId !== event.pointerId) {
      return;
    }
    if (frame !== null) {
      cancelAnimationFrame(frame);
      flushDrag();
    }
    const shouldCommit = dragDirty;
    if (viewport.hasPointerCapture(event.pointerId)) {
      viewport.releasePointerCapture(event.pointerId);
    }
    pointerId = null;
    dragMode = null;
    dragDirty = false;
    viewerRoot.dataset.dragMode = "";
    if (shouldCommit) {
      queueCameraCommit();
    }
    event.preventDefault();
  };

  const onWheel = (event) => {
    clearScheduledCommit();
    dropPendingCameraCommits();
    const primaryDelta =
      Math.abs(event.deltaX) > Math.abs(event.deltaY)
        ? event.deltaX
        : event.deltaY;
    const wheel = Math.max(-8, Math.min(8, primaryDelta / 120));
    if (event.shiftKey) {
      queueEvent("roll", { dx: wheel });
    } else {
      queueEvent("zoom", { wheel });
    }
    scheduleWheelCommit();
    event.preventDefault();
  };

  const onDoubleClick = (event) => {
    viewerRoot.dataset.helpVisible =
      viewerRoot.dataset.helpVisible === "false" ? "true" : "false";
    event.preventDefault();
  };

  const buildSection = (title, body) => {
    const details = document.createElement("details");
    details.className = "topovec-composite-section";
    details.dataset.sectionTitle = title;

    const summary = document.createElement("summary");
    summary.className = "topovec-composite-section-title";
    summary.textContent = title;

    const content = document.createElement("div");
    content.className = "topovec-composite-section-body";
    content.append(body);

    details.append(summary, content);
    return details;
  };

  const buildRowsContainer = () => {
    const rows = document.createElement("div");
    rows.className = "topovec-composite-rows";
    return rows;
  };

  const buildCompactRow = (labelText, control, { stacked = false } = {}) => {
    const row = document.createElement("div");
    row.className = "topovec-composite-row";
    if (stacked) {
      row.classList.add("topovec-composite-row-stack");
    }

    const label = document.createElement("div");
    label.className = "topovec-composite-row-label";
    label.textContent = labelText || "";

    const controlCell = document.createElement("div");
    controlCell.className = "topovec-composite-row-control";
    controlCell.append(control);

    row.append(label, controlCell);
    return row;
  };

  const sendPropertyChange = (prop, group, value) => {
    sendMessage("apply_property_change", {
      group_id: group.id,
      prop_index: prop.index,
      value,
      expected_field: prop.field,
      expected_title: prop.title,
    });
  };

  const buildCameraControls = () => {
    const rows = buildRowsContainer();

    const reset = document.createElement("button");
    reset.type = "button";
    reset.className = "topovec-composite-action";
    reset.textContent = "Reset";
    reset.addEventListener("click", () => sendMessage("reset_camera"));

    const snap = document.createElement("button");
    snap.type = "button";
    snap.className = "topovec-composite-action";
    snap.textContent = "Snap";
    snap.addEventListener("click", () => sendMessage("snap_camera"));

    const projection = document.createElement("button");
    projection.type = "button";
    projection.className =
      "topovec-composite-action topovec-composite-projection-button";
    projection.textContent =
      `Projection: ${model.get("projection_mode") || "Perspective"}`;
    projection.addEventListener("click", () =>
      sendMessage("toggle_perspective")
    );

    rows.append(reset, snap, projection);
    return rows;
  };

  const buildPropertyControl = (group, prop) => {
    if (prop.input_kind === "switch") {
      const input = document.createElement("input");
      input.type = "checkbox";
      input.className = "topovec-composite-checkbox";
      input.checked = prop.value ? true : false;
      input.addEventListener("change", () =>
        sendPropertyChange(prop, group, input.checked)
      );
      return buildCompactRow(prop.title || "", input);
    }

    if (prop.input_kind === "dropdown") {
      const select = document.createElement("select");
      select.className = "topovec-composite-select";
      const values = Array.isArray(prop.values) ? prop.values : [];
      values.forEach((value, index) => {
        const option = document.createElement("option");
        option.value = String(index);
        option.textContent = String(value);
        option.selected = index === Number(prop.selected_index || 0);
        select.append(option);
      });
      select.addEventListener("change", () => {
        const index = Number(select.value || 0);
        sendPropertyChange(prop, group, values[index]);
      });
      return buildCompactRow(prop.title || "", select);
    }

    const commitNumeric = (rawValue) => {
      const next = Number(rawValue);
      if (!Number.isFinite(next)) {
        return;
      }
      sendPropertyChange(prop, group, next);
    };

    if (prop.input_kind === "slider") {
      const rangeWrap = document.createElement("div");
      rangeWrap.className = "topovec-composite-range";

      const range = document.createElement("input");
      range.type = "range";
      range.className = "topovec-composite-range-input";
      range.min = String(prop.min);
      range.max = String(prop.max);
      range.step = String(prop.step ?? 0.001);
      range.value = String(prop.value);

      const number = document.createElement("input");
      number.type = "number";
      number.className = "topovec-composite-number";
      number.min = String(prop.min);
      number.max = String(prop.max);
      number.step = String(prop.step ?? 0.001);
      number.value = String(prop.value);

      range.addEventListener("input", () => {
        number.value = range.value;
      });
      range.addEventListener("change", () => commitNumeric(range.value));
      number.addEventListener("change", () => {
        if (number.value === "") {
          return;
        }
        range.value = number.value;
        commitNumeric(number.value);
      });

      rangeWrap.append(range, number);
      return buildCompactRow(
        prop.title || "",
        rangeWrap,
        { stacked: prop.row_kind === "stack" }
      );
    }

    const input = document.createElement("input");
    input.type = "number";
    input.className = "topovec-composite-number";
    input.step = String(prop.step ?? 0.001);
    input.value = String(prop.value);
    if (prop.value_in_bounds) {
      input.min = String(prop.min);
      input.max = String(prop.max);
    }
    input.addEventListener("change", () => {
      if (input.value === "") {
        return;
      }
      commitNumeric(input.value);
    });
    return buildCompactRow(prop.title || "", input);
  };

  const renderInspector = () => {
    const snapshot = model.get("properties_snapshot") || {};
    const groups = Array.isArray(snapshot.property_groups)
      ? snapshot.property_groups
      : [];
    const sectionTitles = ["Camera", ...groups.map((group) => group.title)];
    if (!sectionTitles.includes(openSection)) {
      openSection = sectionTitles.includes("Camera")
        ? "Camera"
        : sectionTitles[0] || "";
    }

    panelScroller.replaceChildren();

    if (model.get("show_scene_selector")) {
      const selectorBlock = document.createElement("div");
      selectorBlock.className = "topovec-composite-selector";

      const availableScenes = Array.isArray(snapshot.available_scenes)
        ? snapshot.available_scenes
        : [];
      const selectedScene = snapshot.selected_scene;

      if (availableScenes.length === 0) {
        const empty = document.createElement("div");
        empty.className = "topovec-composite-note";
        empty.textContent = "No compatible built-in scenes.";
        selectorBlock.append(buildCompactRow("Scene", empty));
      } else if (selectedScene == null) {
        const invalid = document.createElement("div");
        invalid.className = "topovec-composite-note";
        invalid.textContent =
          "Scene selector requires a built-in topovec.mgl.SCENES scene.";
        selectorBlock.append(buildCompactRow("Scene", invalid));
      } else {
        const select = document.createElement("select");
        select.className = "topovec-composite-select";
        availableScenes.forEach((sceneName) => {
          const option = document.createElement("option");
          option.value = String(sceneName);
          option.textContent = String(sceneName);
          option.selected = String(sceneName) === String(selectedScene);
          select.append(option);
        });
        select.addEventListener("change", () =>
          sendMessage("switch_scene", { scene_name: select.value })
        );
        selectorBlock.append(buildCompactRow("Scene", select));
      }

      panelScroller.append(selectorBlock);
    }

    const sections = [];
    sections.push(buildSection("Camera", buildCameraControls()));

    groups.forEach((group) => {
      const properties = Array.isArray(group.properties)
        ? group.properties
        : [];
      const body = document.createElement("div");
      if (properties.length === 0) {
        const note = document.createElement("div");
        note.className = "topovec-composite-note";
        note.textContent = "No editable properties.";
        body.append(note);
      } else {
        const rows = buildRowsContainer();
        properties.forEach((prop) => {
          rows.append(buildPropertyControl(group, prop));
        });
        body.append(rows);
      }
      sections.push(buildSection(group.title || group.id || "Properties", body));
    });

    sections.forEach((section) => {
      const title = section.dataset.sectionTitle || "";
      section.open = title === openSection;
      section.addEventListener("toggle", () => {
        if (!section.open) {
          if (openSection === title) {
            openSection = "";
          }
          return;
        }
        openSection = title;
        sections.forEach((other) => {
          if (other !== section) {
            other.open = false;
          }
        });
      });
      panelScroller.append(section);
    });

    updateText();
  };

  resetButton.addEventListener("click", onReset);
  snapButton.addEventListener("click", onSnap);
  projectionButton.addEventListener("click", onToggleProjection);
  viewport.addEventListener("contextmenu", onContextMenu);
  viewport.addEventListener("pointerdown", onPointerDown);
  viewport.addEventListener("pointermove", onPointerMove);
  viewport.addEventListener("pointerup", finishPointer);
  viewport.addEventListener("pointercancel", finishPointer);
  viewport.addEventListener("wheel", onWheel, { passive: false });
  viewport.addEventListener("dblclick", onDoubleClick);

  model.on("change:image_data", updateImage);
  model.on("change:status_text", updateText);
  model.on("change:controls_text", updateText);
  model.on("change:projection_mode", updateText);
  model.on("change:summary_text", updateSummary);
  model.on("change:busy", updateBusy);
  model.on("change:width", updateGeometry);
  model.on("change:height", updateGeometry);
  model.on("change:show_toolbar", updateToolbarVisibility);
  model.on("change:show_scene_selector", renderInspector);
  model.on("change:properties_snapshot", renderInspector);
  model.on("change:summary_rows", updateSummary);

  updateImage();
  updateText();
  updateBusy();
  updateGeometry();
  updateToolbarVisibility();
  updateSummary();
  renderInspector();

  return () => {
    if (frame !== null) {
      cancelAnimationFrame(frame);
    }
    clearScheduledCommit();
    resetButton.removeEventListener("click", onReset);
    snapButton.removeEventListener("click", onSnap);
    projectionButton.removeEventListener("click", onToggleProjection);
    viewport.removeEventListener("contextmenu", onContextMenu);
    viewport.removeEventListener("pointerdown", onPointerDown);
    viewport.removeEventListener("pointermove", onPointerMove);
    viewport.removeEventListener("pointerup", finishPointer);
    viewport.removeEventListener("pointercancel", finishPointer);
    viewport.removeEventListener("wheel", onWheel);
    viewport.removeEventListener("dblclick", onDoubleClick);
    model.off("change:image_data", updateImage);
    model.off("change:status_text", updateText);
    model.off("change:controls_text", updateText);
    model.off("change:projection_mode", updateText);
    model.off("change:summary_text", updateSummary);
    model.off("change:busy", updateBusy);
    model.off("change:width", updateGeometry);
    model.off("change:height", updateGeometry);
    model.off("change:show_toolbar", updateToolbarVisibility);
    model.off("change:show_scene_selector", renderInspector);
    model.off("change:properties_snapshot", renderInspector);
    model.off("change:summary_rows", updateSummary);
  };
}

export default { render };
