(function (root) {
  'use strict';

  // Offline asset delivery for exported bundles.
  //
  // A bundle opened from file:// cannot fetch() a neighbouring file, and cannot
  // load an ES module from one either — both are fetched with CORS applied, and
  // a page loaded from disk has a null origin. What a browser DOES allow is
  // <script src="…">, so anything a bundle needs offline ships as a classic
  // script that registers its own bytes as base64 on window.__CREATR_MEDIA__
  // (see piece_export_media_registry_script()). Decode each entry once and hand
  // back a Blob URL, so every consumer sees an ordinary URL it already handles.
  //
  // This lives in its own file rather than inside aframe-model-runtime.js
  // because it is not an A-Frame concern: hand tracking needs it to reach the
  // MediaPipe wasm and model whatever engine the piece uses.
  var blobUrls = {};

  // Only worth using when the page cannot fetch its own neighbours. Served over
  // http(s) the real file is better: the browser streams and caches it, and it
  // is the file a person edits to make the piece their own. The registry is the
  // offline path, not the default one.
  function needsRegistry() {
    try {
      if (root.location && root.location.protocol === 'file:') return true;
      // A srcdoc iframe — how the immersive room mounts a piece — reports
      // "about:" as its protocol while inheriting the parent's base URL and,
      // with it, the parent's inability to fetch a neighbouring file. Asking
      // the protocol alone says "served" and the assets then fail exactly as
      // they did before any of this existed. baseURI is the honest signal.
      var base = root.document && root.document.baseURI;
      return typeof base === 'string' && base.indexOf('file:') === 0;
    } catch (error) {
      return false;
    }
  }

  // Shared so every consumer agrees on what "offline" means. sonic-controller
  // asks this before choosing its hand-tracking route.
  root.creatrBundleIsOffline = needsRegistry;

  // A request may name the asset as it was written in the markup
  // ("media/x.glb"), or fully resolved by the browser
  // ("file:///Users/.../a%20folder/media/x.glb"). The registry is keyed on the
  // bundle-relative path, so accept either and decode percent-escapes — a
  // bundle sitting in a folder with a space in its name is ordinary, not an
  // edge case.
  function registryKeyFor(source, registry) {
    var cut = source.search(/[?#]/);
    var path = cut >= 0 ? source.slice(0, cut) : source;
    path = path.replace(/^\.\//, '');
    if (registry[path]) return path;

    var decoded = path;
    try {
      decoded = decodeURIComponent(path);
    } catch (error) {
      // Leave it as-is: a malformed escape is not worth failing the lookup for.
    }
    if (registry[decoded]) return decoded;

    for (var key in registry) {
      if (!Object.prototype.hasOwnProperty.call(registry, key)) continue;
      var suffix = '/' + key;
      if (decoded.length > suffix.length && decoded.slice(-suffix.length) === suffix) return key;
    }
    return '';
  }

  function registeredBlobUrl(source, force) {
    if (!source) return '';
    if (!force && !needsRegistry()) return '';
    var registry = root.__CREATR_MEDIA__;
    if (!registry) return '';

    var key = registryKeyFor(source, registry);
    var entry = key ? registry[key] : null;
    if (!entry || !entry.base64) return '';
    if (blobUrls[key]) return blobUrls[key];

    try {
      var binary = root.atob(entry.base64);
      var bytes = new Uint8Array(binary.length);
      for (var i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
      blobUrls[key] = root.URL.createObjectURL(new Blob([bytes], { type: entry.mime || 'application/octet-stream' }));
      return blobUrls[key];
    } catch (error) {
      return '';
    }
  }

  root.creatrResolveBundledMedia = registeredBlobUrl;

  // Rewriting src attributes is not enough on its own. A-Frame's <a-asset-item>
  // starts its request from connectedCallback, synchronously, while the parser
  // is still building <a-assets> — before any observer callback (a microtask)
  // could run, and before the scene exists to be watched. So intercept one
  // level lower, where every consumer meets the network: whoever asks for a
  // registered path gets the in-memory copy, whether that is A-Frame's asset
  // system, THREE's GLTFLoader, MediaPipe's wasm loader, or the piece's own
  // code.
  //
  // Only on file:, and only for paths actually in the registry — every other
  // request goes to the real implementation untouched.
  function installRegistryTransport() {
    if (root.__creatrMediaTransportPatched) return;
    if (!needsRegistry() || !root.__CREATR_MEDIA__) return;
    root.__creatrMediaTransportPatched = true;

    if (typeof root.fetch === 'function') {
      var nativeFetch = root.fetch;
      root.fetch = function (input, init) {
        var url = typeof input === 'string' ? input : (input && input.url) || '';
        var blob = url ? registeredBlobUrl(url) : '';
        if (blob) return nativeFetch.call(root, blob, init);
        return nativeFetch.apply(root, arguments);
      };
    }

    var XHR = root.XMLHttpRequest;
    if (XHR && XHR.prototype && typeof XHR.prototype.open === 'function') {
      var nativeOpen = XHR.prototype.open;
      XHR.prototype.open = function (method, url) {
        var blob = typeof url === 'string' ? registeredBlobUrl(url) : '';
        if (blob) {
          var args = Array.prototype.slice.call(arguments);
          args[1] = blob;
          return nativeOpen.apply(this, args);
        }
        return nativeOpen.apply(this, arguments);
      };
    }
  }

  installRegistryTransport();
}(typeof window !== 'undefined' ? window : this));
