// Copyright 2018-2026 the Deno authors. MIT license.

// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file ban-types no-this-alias

(function () {
const { core, primordials } = __bootstrap;
const { WasiContext } = core.ops;
const { statSync } = core.loadExtScript("ext:deno_node/fs.ts");
const { exit } = core.loadExtScript("ext:deno_os/30_os.js");
const {
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_ARG_VALUE,
  ERR_WASI_ALREADY_STARTED,
  ERR_WASI_NOT_STARTED,
} = core.loadExtScript("ext:deno_node/internal/errors.ts");

let warningEmitted = false;
function emitExperimentalWarning() {
  if (warningEmitted) return;
  warningEmitted = true;
  // Match Node's "WASI is an experimental feature ..." warning. The
  // node_compat test runner asserts on this exact message via
  // common.expectWarning('ExperimentalWarning', ...).
  // deno-lint-ignore no-explicit-any
  const proc = (globalThis as any).process;
  if (proc && typeof proc.emitWarning === "function") {
    proc.emitWarning(
      "WASI is an experimental feature and might change at any time",
      "ExperimentalWarning",
    );
  }
}

const {
  ArrayPrototypeMap,
  ArrayPrototypePush,
  ArrayIsArray,
  Error,
  NumberIsInteger,
  ObjectEntries,
  ObjectPrototypeIsPrototypeOf,
  ObjectPrototypeToString,
  SafeArrayIterator,
  String,
  TypeError,
  Uint8Array,
} = primordials;

// UVWASI error for path not found
class UVWASIError extends Error {
  code: string;
  constructor(code: string, message: string) {
    super(message);
    this.code = code;
    this.name = "Error";
  }
}

// Check if value is a WebAssembly.Memory, works across VM contexts
function isWasmMemory(value: unknown): boolean {
  // instanceof fails across VM contexts, use Object.prototype.toString
  return ObjectPrototypeToString(value) === "[object WebAssembly.Memory]";
}

// Custom TypeError with ERR_INVALID_ARG_TYPE code for memory validation.
// Node uses native THROW_ERR_INVALID_ARG_TYPE which bypasses the JS formatter.
function createMemoryTypeError(actual: unknown): TypeError {
  const received = actual === undefined
    ? "undefined"
    : actual === null
    ? "null"
    : `type ${typeof actual}`;
  const err = new TypeError(
    `The "instance.exports.memory" property must be a WebAssembly.Memory object. Received ${received}`,
  );
  (err as unknown as { code: string }).code = "ERR_INVALID_ARG_TYPE";
  return err;
}

class WASIProcExit {
  code: number;
  constructor(code: number) {
    this.code = code;
  }
}

function validateObject(
  value: unknown,
  name: string,
): asserts value is object {
  if (value === null || typeof value !== "object" || ArrayIsArray(value)) {
    throw new ERR_INVALID_ARG_TYPE(name, "object", value);
  }
}

function validateArray(
  value: unknown,
  name: string,
): asserts value is unknown[] {
  if (!ArrayIsArray(value)) {
    throw new ERR_INVALID_ARG_TYPE(name, "Array", value);
  }
}

function validateBoolean(
  value: unknown,
  name: string,
): asserts value is boolean {
  if (typeof value !== "boolean") {
    throw new ERR_INVALID_ARG_TYPE(name, "boolean", value);
  }
}

function validateInt32(
  value: unknown,
  name: string,
): asserts value is number {
  if (!NumberIsInteger(value)) {
    throw new ERR_INVALID_ARG_TYPE(name, "int32", value);
  }
}

function validateString(
  value: unknown,
  name: string,
): asserts value is string {
  if (typeof value !== "string") {
    throw new ERR_INVALID_ARG_TYPE(name, "string", value);
  }
}

// deno-lint-ignore no-explicit-any
type WasiOptions = any;

// WASI preopens use direct host filesystem access in the native ops. They do
// not read from the in-memory VFS that `deno compile` uses for embedded files.
class WASI {
  #ctx;
  #version: string;
  #started = false;
  #returnOnExit: boolean;
  #wasiImport;

  constructor(options?: WasiOptions) {
    emitExperimentalWarning();
    if (options === undefined) {
      throw new ERR_INVALID_ARG_TYPE("options.version", "string", undefined);
    }
    validateObject(options, "options");

    if (options.version === undefined) {
      throw new ERR_INVALID_ARG_TYPE("options.version", "string", undefined);
    }
    validateString(options.version, "options.version");
    if (options.version !== "preview1" && options.version !== "unstable") {
      throw new ERR_INVALID_ARG_VALUE(
        "options.version",
        options.version,
        'must be "preview1" or "unstable"',
      );
    }

    const argsValue = options.args ?? [];
    if (options.args !== undefined) {
      validateArray(options.args, "options.args");
    }
    const args = ArrayPrototypeMap(argsValue, (arg) => String(arg));

    const envObj = options.env ?? {};
    if (options.env !== undefined) {
      validateObject(options.env, "options.env");
    }
    const envPairs: [string, string][] = [];
    for (const entry of new SafeArrayIterator(ObjectEntries(envObj))) {
      const key = entry[0];
      const value = entry[1];
      ArrayPrototypePush(envPairs, [key, String(value)]);
    }

    if (options.preopens !== undefined) {
      validateObject(options.preopens, "options.preopens");
    }
    const preopens: [string, string][] = [];
    if (options.preopens) {
      for (
        const entry of new SafeArrayIterator(ObjectEntries(options.preopens))
      ) {
        const virtualPath = entry[0];
        const realPath = entry[1];
        const realPathString = String(realPath);
        try {
          statSync(realPathString);
        } catch {
          throw new UVWASIError(
            "UVWASI_ENOENT",
            `uvwasi_init: failed to open preopen "${realPathString}"`,
          );
        }
        ArrayPrototypePush(preopens, [String(virtualPath), realPathString]);
      }
    }

    if (options.returnOnExit !== undefined) {
      validateBoolean(options.returnOnExit, "options.returnOnExit");
    }

    if (options.stdin !== undefined) {
      validateInt32(options.stdin, "options.stdin");
    }
    if (options.stdout !== undefined) {
      validateInt32(options.stdout, "options.stdout");
    }
    if (options.stderr !== undefined) {
      validateInt32(options.stderr, "options.stderr");
    }

    const stdinFd = options.stdin ?? 0;
    const stdoutFd = options.stdout ?? 1;
    const stderrFd = options.stderr ?? 2;
    this.#returnOnExit = options.returnOnExit ?? true;
    this.#version = options.version;

    this.#ctx = new WasiContext(
      args,
      envPairs,
      preopens,
      stdinFd,
      stdoutFd,
      stderrFd,
      this.#returnOnExit,
    );

    // Build the wasiImport object. Each function delegates to the cppgc object
    // method, passing the wasm memory buffer.
    const ctx = this.#ctx;
    const self = this;

    this.#wasiImport = {
      args_get(argv: number, argvBuf: number) {
        return ctx.argsGet(argv, argvBuf, self.#getMemoryBuffer());
      },
      args_sizes_get(argc: number, argvBufSize: number) {
        return ctx.argsSizesGet(argc, argvBufSize, self.#getMemoryBuffer());
      },
      environ_get(environ: number, environBuf: number) {
        return ctx.environGet(environ, environBuf, self.#getMemoryBuffer());
      },
      environ_sizes_get(environCount: number, environBufSize: number) {
        return ctx.environSizesGet(
          environCount,
          environBufSize,
          self.#getMemoryBuffer(),
        );
      },
      clock_res_get(clockId: number, resolution: number) {
        return ctx.clockResGet(
          clockId,
          resolution,
          self.#getMemoryBuffer(),
        );
      },
      clock_time_get(clockId: number, precision: number, time: number) {
        return ctx.clockTimeGet(
          clockId,
          precision,
          time,
          self.#getMemoryBuffer(),
        );
      },
      random_get(bufPtr: number, bufLen: number) {
        return ctx.randomGet(bufPtr, bufLen, self.#getMemoryBuffer());
      },
      proc_exit(code: number) {
        const exitCode = ctx.procExit(code);
        if (self.#returnOnExit) {
          throw new WASIProcExit(exitCode);
        }
        exit(exitCode);
      },
      proc_raise(sig: number) {
        return ctx.procRaise(sig);
      },
      fd_write(
        fd: number,
        iovsPtr: number,
        iovsLen: number,
        nwrittenPtr: number,
      ) {
        return ctx.fdWrite(
          fd,
          iovsPtr,
          iovsLen,
          nwrittenPtr,
          self.#getMemoryBuffer(),
        );
      },
      fd_read(
        fd: number,
        iovsPtr: number,
        iovsLen: number,
        nreadPtr: number,
      ) {
        return ctx.fdRead(
          fd,
          iovsPtr,
          iovsLen,
          nreadPtr,
          self.#getMemoryBuffer(),
        );
      },
      fd_seek(
        fd: number,
        offset: number,
        whence: number,
        newoffsetPtr: number,
      ) {
        return ctx.fdSeek(
          fd,
          offset,
          whence,
          newoffsetPtr,
          self.#getMemoryBuffer(),
        );
      },
      fd_close(fd: number) {
        return ctx.fdClose(fd);
      },
      fd_fdstat_get(fd: number, fdstatPtr: number) {
        return ctx.fdFdstatGet(fd, fdstatPtr, self.#getMemoryBuffer());
      },
      fd_fdstat_set_flags(fd: number, flags: number) {
        return ctx.fdFdstatSetFlags(fd, flags);
      },
      fd_fdstat_set_rights(
        fd: number,
        fsRightsBase: number,
        fsRightsInheriting: number,
      ) {
        return ctx.fdFdstatSetRights(
          fd,
          fsRightsBase,
          fsRightsInheriting,
        );
      },
      fd_prestat_get(fd: number, prestatPtr: number) {
        return ctx.fdPrestatGet(fd, prestatPtr, self.#getMemoryBuffer());
      },
      fd_prestat_dir_name(fd: number, pathPtr: number, pathLen: number) {
        return ctx.fdPrestatDirName(
          fd,
          pathPtr,
          pathLen,
          self.#getMemoryBuffer(),
        );
      },
      fd_tell(fd: number, offsetPtr: number) {
        return ctx.fdTell(fd, offsetPtr, self.#getMemoryBuffer());
      },
      fd_sync(fd: number) {
        return ctx.fdSync(fd);
      },
      fd_datasync(fd: number) {
        return ctx.fdDatasync(fd);
      },
      fd_advise(fd: number, offset: number, len: number, advice: number) {
        return ctx.fdAdvise(fd, offset, len, advice);
      },
      fd_allocate(fd: number, offset: number, len: number) {
        return ctx.fdAllocate(fd, offset, len);
      },
      fd_filestat_get(fd: number, filestatPtr: number) {
        return ctx.fdFilestatGet(
          fd,
          filestatPtr,
          self.#getMemoryBuffer(),
        );
      },
      fd_filestat_set_size(fd: number, size: number) {
        return ctx.fdFilestatSetSize(fd, size);
      },
      fd_filestat_set_times(
        fd: number,
        atim: number,
        mtim: number,
        fstFlags: number,
      ) {
        return ctx.fdFilestatSetTimes(fd, atim, mtim, fstFlags);
      },
      fd_renumber(from: number, to: number) {
        return ctx.fdRenumber(from, to);
      },
      fd_readdir(
        fd: number,
        bufPtr: number,
        bufLen: number,
        cookie: number,
        bufusedPtr: number,
      ) {
        return ctx.fdReaddir(
          fd,
          bufPtr,
          bufLen,
          cookie,
          bufusedPtr,
          self.#getMemoryBuffer(),
        );
      },
      fd_pread(
        fd: number,
        iovsPtr: number,
        iovsLen: number,
        offset: number,
        nreadPtr: number,
      ) {
        return ctx.fdPread(
          fd,
          iovsPtr,
          iovsLen,
          offset,
          nreadPtr,
          self.#getMemoryBuffer(),
        );
      },
      fd_pwrite(
        fd: number,
        iovsPtr: number,
        iovsLen: number,
        offset: number,
        nwrittenPtr: number,
      ) {
        return ctx.fdPwrite(
          fd,
          iovsPtr,
          iovsLen,
          offset,
          nwrittenPtr,
          self.#getMemoryBuffer(),
        );
      },
      path_open(
        dirfd: number,
        dirflags: number,
        pathPtr: number,
        pathLen: number,
        oflags: number,
        fsRightsBase: number,
        fsRightsInheriting: number,
        fdflags: number,
        fdPtr: number,
      ) {
        return ctx.pathOpen(
          dirfd,
          dirflags,
          pathPtr,
          pathLen,
          oflags,
          fsRightsBase,
          fsRightsInheriting,
          fdflags,
          fdPtr,
          self.#getMemoryBuffer(),
        );
      },
      path_create_directory(
        dirfd: number,
        pathPtr: number,
        pathLen: number,
      ) {
        return ctx.pathCreateDirectory(
          dirfd,
          pathPtr,
          pathLen,
          self.#getMemoryBuffer(),
        );
      },
      path_remove_directory(
        dirfd: number,
        pathPtr: number,
        pathLen: number,
      ) {
        return ctx.pathRemoveDirectory(
          dirfd,
          pathPtr,
          pathLen,
          self.#getMemoryBuffer(),
        );
      },
      path_unlink_file(dirfd: number, pathPtr: number, pathLen: number) {
        return ctx.pathUnlinkFile(
          dirfd,
          pathPtr,
          pathLen,
          self.#getMemoryBuffer(),
        );
      },
      path_rename(
        oldDirfd: number,
        oldPathPtr: number,
        oldPathLen: number,
        newDirfd: number,
        newPathPtr: number,
        newPathLen: number,
      ) {
        return ctx.pathRename(
          oldDirfd,
          oldPathPtr,
          oldPathLen,
          newDirfd,
          newPathPtr,
          newPathLen,
          self.#getMemoryBuffer(),
        );
      },
      path_filestat_get(
        dirfd: number,
        flags: number,
        pathPtr: number,
        pathLen: number,
        filestatPtr: number,
      ) {
        return ctx.pathFilestatGet(
          dirfd,
          flags,
          pathPtr,
          pathLen,
          filestatPtr,
          self.#getMemoryBuffer(),
        );
      },
      path_readlink(
        dirfd: number,
        pathPtr: number,
        pathLen: number,
        bufPtr: number,
        bufLen: number,
        bufusedPtr: number,
      ) {
        return ctx.pathReadlink(
          dirfd,
          pathPtr,
          pathLen,
          bufPtr,
          bufLen,
          bufusedPtr,
          self.#getMemoryBuffer(),
        );
      },
      path_symlink(
        oldPathPtr: number,
        oldPathLen: number,
        dirfd: number,
        newPathPtr: number,
        newPathLen: number,
      ) {
        return ctx.pathSymlink(
          oldPathPtr,
          oldPathLen,
          dirfd,
          newPathPtr,
          newPathLen,
          self.#getMemoryBuffer(),
        );
      },
      path_link(
        oldDirfd: number,
        oldFlags: number,
        oldPathPtr: number,
        oldPathLen: number,
        newDirfd: number,
        newPathPtr: number,
        newPathLen: number,
      ) {
        return ctx.pathLink(
          oldDirfd,
          oldFlags,
          oldPathPtr,
          oldPathLen,
          newDirfd,
          newPathPtr,
          newPathLen,
          self.#getMemoryBuffer(),
        );
      },
      path_filestat_set_times(
        dirfd: number,
        flags: number,
        pathPtr: number,
        pathLen: number,
        atim: number,
        mtim: number,
        fstFlags: number,
      ) {
        return ctx.pathFilestatSetTimes(
          dirfd,
          flags,
          pathPtr,
          pathLen,
          atim,
          mtim,
          fstFlags,
          self.#getMemoryBuffer(),
        );
      },
      poll_oneoff(
        inPtr: number,
        outPtr: number,
        nsubscriptions: number,
        neventsPtr: number,
      ) {
        return ctx.pollOneoff(
          inPtr,
          outPtr,
          nsubscriptions,
          neventsPtr,
          self.#getMemoryBuffer(),
        );
      },
      sched_yield() {
        return ctx.schedYield();
      },
      sock_recv(
        fd: number,
        riDataPtr: number,
        riDataLen: number,
        riFlags: number,
        roDatalenPtr: number,
        roFlagsPtr: number,
      ) {
        return ctx.sockRecv(
          fd,
          riDataPtr,
          riDataLen,
          riFlags,
          roDatalenPtr,
          roFlagsPtr,
          self.#getMemoryBuffer(),
        );
      },
      sock_send(
        fd: number,
        siDataPtr: number,
        siDataLen: number,
        siFlags: number,
        soDatalenPtr: number,
      ) {
        return ctx.sockSend(
          fd,
          siDataPtr,
          siDataLen,
          siFlags,
          soDatalenPtr,
          self.#getMemoryBuffer(),
        );
      },
      sock_shutdown(fd: number, how: number) {
        return ctx.sockShutdown(fd, how);
      },
      sock_accept(fd: number, flags: number, fdPtr: number) {
        return ctx.sockAccept(fd, flags, fdPtr, self.#getMemoryBuffer());
      },
    };
  }

  #memory: WebAssembly.Memory | null = null;

  #getMemoryBuffer(): Uint8Array {
    if (!this.#memory) {
      throw new ERR_WASI_NOT_STARTED();
    }
    // deno-lint-ignore deno-internal/prefer-primordials -- WebAssembly.Memory.prototype.buffer getter; no primordial equivalent
    return new Uint8Array(this.#memory.buffer);
  }

  get wasiImport() {
    return this.#wasiImport;
  }

  getImportObject() {
    if (this.#version === "unstable") {
      return { wasi_unstable: this.#wasiImport };
    }
    return { wasi_snapshot_preview1: this.#wasiImport };
  }

  start(instance?: WebAssembly.Instance): number {
    if (this.#started) {
      throw new ERR_WASI_ALREADY_STARTED();
    }

    if (instance === undefined || instance === null) {
      throw new ERR_INVALID_ARG_TYPE("instance", "object", instance);
    }
    if (typeof instance !== "object") {
      throw new ERR_INVALID_ARG_TYPE("instance", "object", instance);
    }

    const exports = instance.exports;
    if (exports === null || typeof exports !== "object") {
      throw new ERR_INVALID_ARG_TYPE("instance.exports", "object", exports);
    }

    if (typeof exports._start !== "function") {
      throw new ERR_INVALID_ARG_TYPE(
        "instance.exports._start",
        "function",
        exports._start,
      );
    }

    if (exports._initialize !== undefined) {
      throw new ERR_INVALID_ARG_TYPE(
        "instance.exports._initialize",
        "undefined",
        exports._initialize,
      );
    }

    if (!isWasmMemory(exports.memory)) {
      throw createMemoryTypeError(exports.memory);
    }

    this.#memory = exports.memory as WebAssembly.Memory;
    this.#started = true;

    try {
      (exports._start as Function)();
    } catch (e) {
      if (ObjectPrototypeIsPrototypeOf(WASIProcExit.prototype, e)) {
        return e.code;
      }
      throw e;
    }

    return 0;
  }

  // Node.js exposes finalizeBindings() so worker threads spawned via
  // `wasi.thread-spawn` can bind a wasm instance that shares the main
  // thread's WASI state (the existing #ctx) to an external WebAssembly.Memory.
  // We don't manage thread bookkeeping ourselves - that lives in the user's
  // thread-spawn shim - but we must accept the call so the user's bindings
  // can call wasi_thread_start() in the worker.
  finalizeBindings(
    instance?: WebAssembly.Instance,
    options?: { memory?: WebAssembly.Memory },
  ): void {
    if (instance === undefined || instance === null) {
      throw new ERR_INVALID_ARG_TYPE("instance", "object", instance);
    }
    if (typeof instance !== "object") {
      throw new ERR_INVALID_ARG_TYPE("instance", "object", instance);
    }
    const exports = instance.exports;
    if (exports === null || typeof exports !== "object") {
      throw new ERR_INVALID_ARG_TYPE("instance.exports", "object", exports);
    }
    const memory = options?.memory ??
      (exports as { memory?: unknown }).memory;
    if (!isWasmMemory(memory)) {
      throw createMemoryTypeError(memory);
    }
    this.#memory = memory as WebAssembly.Memory;
    // finalizeBindings is intentionally idempotent: the same WASI instance
    // can be bound to multiple wasm instances across threads.
    //
    // Setting #started here is a one-way transition shared with start()
    // and initialize(): once finalizeBindings has been called on this
    // instance (typically from the thread-spawn worker path), a later
    // start()/initialize() call on the same WASI object will throw
    // ERR_WASI_ALREADY_STARTED. Node's wasi_thread_start path has the
    // same behavior - re-entering start/initialize on a bound WASI is
    // never valid, the spawning thread is expected to construct a fresh
    // WASI per wasm module.
    this.#started = true;
  }

  initialize(instance?: WebAssembly.Instance): void {
    if (this.#started) {
      throw new ERR_WASI_ALREADY_STARTED();
    }

    if (instance === undefined || instance === null) {
      throw new ERR_INVALID_ARG_TYPE("instance", "object", instance);
    }
    if (typeof instance !== "object") {
      throw new ERR_INVALID_ARG_TYPE("instance", "object", instance);
    }

    const exports = instance.exports;
    if (exports === null || typeof exports !== "object") {
      throw new ERR_INVALID_ARG_TYPE("instance.exports", "object", exports);
    }

    if (
      exports._initialize !== undefined &&
      typeof exports._initialize !== "function"
    ) {
      throw new ERR_INVALID_ARG_TYPE(
        "instance.exports._initialize",
        "function",
        exports._initialize,
      );
    }

    if (exports._start !== undefined) {
      throw new ERR_INVALID_ARG_TYPE(
        "instance.exports._start",
        "undefined",
        exports._start,
      );
    }

    if (!isWasmMemory(exports.memory)) {
      throw createMemoryTypeError(exports.memory);
    }

    this.#memory = exports.memory as WebAssembly.Memory;
    this.#started = true;

    if (typeof exports._initialize === "function") {
      (exports._initialize as Function)();
    }
  }
}

return {
  default: { WASI },
  WASI,
};
})();
