(function attachPyroSceneController(root) {
  'use strict';

  function createSceneController(options = {}) {
    const requestFrame = options.requestFrame || root.requestAnimationFrame?.bind(root);
    const cancelFrame = options.cancelFrame || root.cancelAnimationFrame?.bind(root);
    if (typeof requestFrame !== 'function' || typeof cancelFrame !== 'function') throw new Error('Animation-Frame-API fehlt');

    const scene = {
      defs: null,
      overlayRoot: null,
      objectNodes: new Map(),
      frame: 0,
      pendingObjectIds: new Set(),
      pendingTransformIds: new Set(),
      pendingInspector: false,
      fullRenderCount: 0,
      interactionFrameCount: 0
    };

    function cancelInteractionRender() {
      if (scene.frame) cancelFrame(scene.frame);
      scene.frame = 0; scene.pendingObjectIds.clear(); scene.pendingTransformIds.clear(); scene.pendingInspector = false;
    }

    function beginFullRender() {
      cancelInteractionRender(); scene.objectNodes.clear(); scene.overlayRoot = null;
    }

    function registerObjectNode(id, node) {
      if (id && node) scene.objectNodes.set(id, node);
      return node;
    }

    function completeFullRender() {
      scene.fullRenderCount += 1;
      options.onFullRender?.({ total: scene.fullRenderCount });
    }

    function replaceObject(object) {
      const previous = scene.objectNodes.get(object?.id);
      if (!object || !previous?.isConnected) return false;
      const replacement = options.renderObject?.(object);
      if (!replacement) return false;
      previous.replaceWith(replacement); scene.objectNodes.set(object.id, replacement);
      options.refreshObject?.(object);
      return true;
    }

    function patchObjectTransform(object) {
      const node = scene.objectNodes.get(object?.id);
      if (!object || !node?.isConnected || typeof options.patchObjectTransform !== 'function') return false;
      return options.patchObjectTransform(object, node) !== false;
    }

    function renderInteractionFrame() {
      const execute = () => {
        scene.frame = 0;
        const objectIds = [...scene.pendingObjectIds]; const transformIds = [...scene.pendingTransformIds]; const inspector = scene.pendingInspector;
        scene.pendingObjectIds.clear(); scene.pendingTransformIds.clear(); scene.pendingInspector = false;
        const replacementObjects = [];
        objectIds.forEach((id) => {
          const object = options.getObject?.(id);
          if (object) replacementObjects.push(object, ...(options.getDependents?.(object.id) || []));
        });
        const transformObjects = [];
        transformIds.forEach((id) => {
          const object = options.getObject?.(id);
          if (!object) return;
          transformObjects.push(object);
          replacementObjects.push(...(options.getDependents?.(object.id) || []));
        });
        const uniqueReplacements = [...new Map(replacementObjects.map((object) => [object.id, object])).values()];
        const replacementIds = new Set(uniqueReplacements.map((object) => object.id));
        const uniqueTransforms = [...new Map(transformObjects.map((object) => [object.id, object])).values()]
          .filter((object) => !replacementIds.has(object.id));
        const runMeasured = (name, callback) => typeof options.measure === 'function' ? options.measure(name, callback) : callback();
        const transformFailed = uniqueTransforms.length
          ? runMeasured('render.transformPatches', () => uniqueTransforms.some((object) => !patchObjectTransform(object)))
          : false;
        const replacementFailed = !transformFailed && uniqueReplacements.length
          ? runMeasured('render.objectReplacements', () => uniqueReplacements.some((object) => !replaceObject(object)))
          : false;
        if (transformFailed || replacementFailed) {
          options.renderFallback?.();
        } else {
          options.renderOverlays?.();
          scene.interactionFrameCount += 1;
          options.onInteractionFrame?.({
            changed: uniqueTransforms.length + uniqueReplacements.length,
            transformed: uniqueTransforms.length,
            replaced: uniqueReplacements.length,
            total: scene.interactionFrameCount
          });
        }
        if (inspector === 'transform') runMeasured('render.transformInspector', () => options.renderTransformInspector?.());
        else if (inspector) runMeasured('render.inspector', () => options.renderInspector?.());
      };
      return typeof options.measure === 'function' ? options.measure('render.interaction', execute) : execute();
    }

    function flushInteractionRender() {
      if (!scene.frame && !scene.pendingObjectIds.size && !scene.pendingTransformIds.size && !scene.pendingInspector) return false;
      if (scene.frame) cancelFrame(scene.frame);
      scene.frame = 0;
      renderInteractionFrame();
      return true;
    }

    function scheduleInteractionRender(objects = [], { inspector = false } = {}) {
      objects.filter(Boolean).forEach((object) => scene.pendingObjectIds.add(typeof object === 'string' ? object : object.id));
      if (inspector === true || scene.pendingInspector !== true && inspector === 'transform') scene.pendingInspector = inspector;
      if (!scene.frame) scene.frame = requestFrame(renderInteractionFrame);
    }

    function scheduleTransformRender(objects = [], { inspector = false } = {}) {
      objects.filter(Boolean).forEach((object) => scene.pendingTransformIds.add(typeof object === 'string' ? object : object.id));
      if (inspector === true || scene.pendingInspector !== true && inspector === 'transform') scene.pendingInspector = inspector;
      if (!scene.frame) scene.frame = requestFrame(renderInteractionFrame);
    }

    return Object.assign(scene, {
      cancelInteractionRender,
      beginFullRender,
      registerObjectNode,
      completeFullRender,
      replaceObject,
      renderInteractionFrame,
      flushInteractionRender,
      scheduleInteractionRender,
      scheduleTransformRender,
      nodeFor: (id) => scene.objectNodes.get(id) || null
    });
  }

  const api = Object.freeze({ createSceneController });
  root.PyroSceneController = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);
