<!-- Thank you for contributing to Pyodide! All improvements are welcome,
     so don't be afraid to make a PR. -->

<!-- [IMPORTANT] Note on CI failures:
     Currently, we are having issues with selenium-based tests.
     Don't panic if the CI fails on your PR because of timeouts.
     It's probably not your fault. We will investigate :) -->

### Description
The PR is to avoid `importScripts` error when loading pyodide in a module type worker. Currently `loadScripts`  uses `importScripts` to import dependency  which throws an error when the worker is of type `module`.

### Checklists

<!-- Note on checklists:
     If you think some of these steps are not necessary for your PR,
     just remove those checkboxes, or mark them as checked. Otherwise,
     if some checkboxes are left unmarked, we might assume that your PR
     is not ready to be merged and we might keep you waiting -->

- [x] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [x] Add / update tests
- [ ] Add new / update outdated documentation

Use ESM import in module type webworker. <!-- Thank you for contributing to Pyodide! All improvements are welcome,
     so don't be afraid to make a PR. -->

<!-- [IMPORTANT] Note on CI failures:
     Currently, we are having issues with selenium-based tests.
     Don't panic if the CI fails on your PR because of timeouts.
     It's probably not your fault. We will investigate :) -->

### Description
The PR is to avoid `importScripts` error when loading pyodide in a module type worker. Currently `loadScripts`  uses `importScripts` to import dependency  which throws an error when the worker is of type `module`.

### Checklists

<!-- Note on checklists:
     If you think some of these steps are not necessary for your PR,
     just remove those checkboxes, or mark them as checked. Otherwise,
     if some checkboxes are left unmarked, we might assume that your PR
     is not ready to be merged and we might keep you waiting -->

- [x] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [x] Add / update tests
- [ ] Add new / update outdated documentation

/**
 * The Emscripten Module.
 *
 * @private
 */
export let Module: any = {};
Module.noImageDecoding = true;
Module.noAudioDecoding = true;
Module.noWasmDecoding = false; // we preload wasm using the built in plugin now
Module.preloadedWasm = {};
Module.preRun = [];

export let API: any = {};
Module.API = API;
export let Hiwire: any = {};
Module.hiwire = Hiwire;

// Put things that are exposed only for testing purposes here.
export let Tests: any = {};
API.tests = Tests;

/**
 *
 * @param stdin
 * @param stdout
 * @param stderr
 * @private
 */
export function setStandardStreams(
  stdin?: () => string,
  stdout?: (a: string) => void,
  stderr?: (a: string) => void
) {
  // For stdout and stderr, emscripten provides convenient wrappers that save us the trouble of converting the bytes into a string
  if (stdout) {
    Module.print = stdout;
  }

  if (stderr) {
    Module.printErr = stderr;
  }

  // For stdin, we have to deal with the low level API ourselves
  if (stdin) {
    Module.preRun.push(function () {
      Module.FS.init(createStdinWrapper(stdin), null, null);
    });
  }
}

function createStdinWrapper(stdin: () => string) {
  // When called, it asks the user for one whole line of input (stdin)
  // Then, it passes the individual bytes of the input to emscripten, one after another.
  // And finally, it terminates it with null.
  const encoder = new TextEncoder();
  let input = new Uint8Array(0);
  let inputIndex = -1; // -1 means that we just returned null
  function stdinWrapper() {
    try {
      if (inputIndex === -1) {
        let text = stdin();
        if (text === undefined || text === null) {
          return null;
        }
        if (typeof text !== "string") {
          throw new TypeError(
            `Expected stdin to return string, null, or undefined, got type ${typeof text}.`
          );
        }
        if (!text.endsWith("\n")) {
          text += "\n";
        }
        input = encoder.encode(text);
        inputIndex = 0;
      }

      if (inputIndex < input.length) {
        let character = input[inputIndex];
        inputIndex++;
        return character;
      } else {
        inputIndex = -1;
        return null;
      }
    } catch (e) {
      // emscripten will catch this and set an IOError which is unhelpful for
      // debugging.
      console.error("Error thrown in stdin:");
      console.error(e);
      throw e;
    }
  }
  return stdinWrapper;
}

/**
 * Make the home directory inside the virtual file system,
 * then change the working directory to it.
 *
 * @param path
 * @private
 */
export function setHomeDirectory(path: string) {
  Module.preRun.push(function () {
    const fallbackPath = "/";
    try {
      Module.FS.mkdirTree(path);
    } catch (e) {
      console.error(`Error occurred while making a home directory '${path}':`);
      console.error(e);
      console.error(`Using '${fallbackPath}' for a home directory instead`);
      path = fallbackPath;
    }
    Module.ENV.HOME = path;
    Module.FS.chdir(path);
  });
}

/**
 * The Emscripten Module.
 *
 * @private
 */
export let Module: any = {};
Module.noImageDecoding = true;
Module.noAudioDecoding = true;
Module.noWasmDecoding = false; // we preload wasm using the built in plugin now
Module.preloadedWasm = {};
Module.preRun = [];

export let API: any = {};
Module.API = API;
export let Hiwire: any = {};
Module.hiwire = Hiwire;

// Put things that are exposed only for testing purposes here.
export let Tests: any = {};
API.tests = Tests;

/**
 *
 * @param stdin
 * @param stdout
 * @param stderr
 * @private
 */
export function setStandardStreams(
  stdin?: () => string,
  stdout?: (a: string) => void,
  stderr?: (a: string) => void
) {
  // For stdout and stderr, emscripten provides convenient wrappers that save us the trouble of converting the bytes into a string
  if (stdout) {
    Module.print = stdout;
  }

  if (stderr) {
    Module.printErr = stderr;
  }

  // For stdin, we have to deal with the low level API ourselves
  if (stdin) {
    Module.preRun.push(function () {
      Module.FS.init(createStdinWrapper(stdin), null, null);
    });
  }
}

function createStdinWrapper(stdin: () => string) {
  // When called, it asks the user for one whole line of input (stdin)
  // Then, it passes the individual bytes of the input to emscripten, one after another.
  // And finally, it terminates it with null.
  const encoder = new TextEncoder();
  let input = new Uint8Array(0);
  let inputIndex = -1; // -1 means that we just returned null
  function stdinWrapper() {
    try {
      if (inputIndex === -1) {
        let text = stdin();
        if (text === undefined || text === null) {
          return null;
        }
        if (typeof text !== "string") {
          throw new TypeError(
            `Expected stdin to return string, null, or undefined, got type ${typeof text}.`
          );
        }
        if (!text.endsWith("\n")) {
          text += "\n";
        }
        input = encoder.encode(text);
        inputIndex = 0;
      }

      if (inputIndex < input.length) {
        let character = input[inputIndex];
        inputIndex++;
        return character;
      } else {
        inputIndex = -1;
        return null;
      }
    } catch (e) {
      // emscripten will catch this and set an IOError which is unhelpful for
      // debugging.
      console.error("Error thrown in stdin:");
      console.error(e);
      throw e;
    }
  }
  return stdinWrapper;
}

/**
 * Make the home directory inside the virtual file system,
 * then change the working directory to it.
 *
 * @param path
 * @private
 */
export function setHomeDirectory(path: string) {
  Module.preRun.push(function () {
    const fallbackPath = "/";
    try {
      Module.FS.mkdirTree(path);
    } catch (e) {
      console.error(`Error occurred while making a home directory '${path}':`);
      console.error(e);
      console.error(`Using '${fallbackPath}' for a home directory instead`);
      path = fallbackPath;
    }
    Module.ENV.HOME = path;
    Module.FS.chdir(path);
  });
}
