pyodideOjs = {
  const {
    PyodideEvaluator,
    PyodideEnvironmentManager,
    setupPython,
    startPyodideWorker,
    b64Decode,
    collapsePath,
  } = window._exercise_ojs_runtime;

  const statusContainer = document.getElementById("exercise-loading-status");
  const indicatorContainer = document.getElementById("exercise-loading-indicator");
  indicatorContainer.classList.remove("d-none");

  let statusText = document.createElement("div")
  statusText.classList = "exercise-loading-details";
  statusText = statusContainer.appendChild(statusText);
  statusText.textContent = `Initialise`;

  // Hoist indicator out from final slide when running under reveal
  const revealStatus = document.querySelector(".reveal .exercise-loading-indicator");
  if (revealStatus) {
    revealStatus.remove();
    document.querySelector(".reveal > .slides").appendChild(revealStatus);
  }

  // Make any reveal slides with live cells scrollable
  document.querySelectorAll(".reveal .exercise-cell").forEach((el) => {
    el.closest('section.slide').classList.add("scrollable");
  })

  // Pyodide supplemental data and options
  const dataContent = document.querySelector(`script[type=\"pyodide-data\"]`).textContent;
  const data = JSON.parse(b64Decode(dataContent));

  // Grab list of resources to be downloaded
  const filesContent = document.querySelector(`script[type=\"vfs-file\"]`).textContent;
  const files = JSON.parse(b64Decode(filesContent));

  let pyodidePromise = (async () => {
    statusText.textContent = `Downloading Pyodide`;
    const pyodide = await startPyodideWorker(data.options);

    statusText.textContent = `Downloading package: micropip`;
    await pyodide.loadPackage("micropip");
    const micropip = await pyodide.pyimport("micropip");
    await data.packages.pkgs.map((pkg) => () => {
      statusText.textContent = `Downloading package: ${pkg}`;
      return micropip.install(pkg);
    }).reduce((cur, next) => cur.then(next), Promise.resolve());
    await micropip.destroy();

    // Download and install resources
    await files.map((file) => async () => {
      const name = file.substring(file.lastIndexOf('/') + 1);
      statusText.textContent = `Downloading resource: ${name}`;
      const response = await fetch(file);
      if (!response.ok) {
        throw new Error(`Can't download \`${file}\`. Error ${response.status}: "${response.statusText}".`);
      }
      const data = await response.arrayBuffer();

      // Store URLs in the cwd without any subdirectory structure
      if (file.includes("://")) {
        file = name;
      }

      // Collapse higher directory structure
      file = collapsePath(file);

      // Create directory tree, ignoring "directory exists" VFS errors
      const parts = file.split('/').slice(0, -1);
      let path = '';
      while (parts.length > 0) {
        path += parts.shift() + '/';
        try {
          await pyodide.FS.mkdir(path);
        } catch (e) {
          if (e.name !== "ErrnoError") throw e;
          if (e.errno !== 20) {
            const errorTextPtr = await pyodide._module._strerror(e.errno);
            const errorText = await pyodide._module.UTF8ToString(errorTextPtr);
            throw new Error(`Filesystem Error ${e.errno} "${errorText}".`);
          }
        }
      }

      // Write this file to the VFS
      try {
        return await pyodide.FS.writeFile(file, new Uint8Array(data));
      } catch (e) {
        if (e.name !== "ErrnoError") throw e;
        const errorTextPtr = await pyodide._module._strerror(e.errno);
        const errorText = await pyodide._module.UTF8ToString(errorTextPtr);
        throw new Error(`Filesystem Error ${e.errno} "${errorText}".`);
      }
    }).reduce((cur, next) => cur.then(next), Promise.resolve());

    statusText.textContent = `Pyodide environment setup`;
    await setupPython(pyodide);

    statusText.remove();
    if (statusContainer.children.length == 0) {
      statusContainer.parentNode.remove();
    }
    return pyodide;
  })().catch((err) => {
    statusText.style.color = "var(--exercise-editor-hl-er, #AD0000)";
    statusText.textContent = err.message;
    //indicatorContainer.querySelector(".spinner-grow").classList.add("d-none");
    throw err;
  });

  // Keep track of initial OJS block render
  const renderedOjs = {};

  const process = async (context, inputs) => {
    const pyodide = await pyodidePromise;
    const evaluator = new PyodideEvaluator(pyodide, context);
    await evaluator.process(inputs);
    return evaluator.container;
  }

  return {
    pyodidePromise,
    renderedOjs,
    process,
  };
}
