/**
 * @license
 * Copyright 2018 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

addToLibrary({
  $NODERAWFS__deps: ['$ERRNO_CODES', '$FS', '$NODEFS', '$mmapAlloc', '$FS_modeStringToFlags', '$NODERAWFS_stream_funcs'],
  $NODERAWFS__postset: `
    if (!ENVIRONMENT_IS_NODE) {
      throw new Error("NODERAWFS is currently only supported on Node.js environment.")
    }
    var nodeTTY = require('node:tty');
    function _wrapNodeError(func) {
      return (...args) => {
        try {
          return func(...args)
        } catch (e) {
          // Hack for Deno which throws BadResource instead of EBADF:
          // https://github.com/emscripten-core/emscripten/issues/26239
          if (e.name == 'BadResource') {
            e.code = 'EBADF';
          }
          if (e.code) {
            throw new FS.ErrnoError(ERRNO_CODES[e.code]);
          }
          throw e;
        }
      }
    }
    function _wrapNodeStreamFunc(func, vfs_func) {
      return _wrapNodeError((stream, ...args) => {
        if (stream.stream_ops) {
          // this stream was created by some other FS. e.g: PIPEFS.
          return vfs_func(stream, ...args);
        }
        return func(stream, ...args);
      });
    }
    // Use this to reference our in-memory filesystem
    /** @suppress {partialAlias} */
    var VFS = {...FS};
    // Wrap the whole in-memory filesystem API with
    // our Node.js based functions
    for (const [key, value] of Object.entries(NODERAWFS)) {
      FS[key] = _wrapNodeError(value);
    }
    for (const [key, value] of Object.entries(NODERAWFS_stream_funcs)) {
      FS[key] = _wrapNodeStreamFunc(value, FS[key]);
    }`,
  $NODERAWFS: {
    lookup(parent, name) {
#if ASSERTIONS
      assert(parent)
      assert(parent.path)
#endif
      return FS.lookupPath(`${parent.path}/${name}`).node;
    },
    lookupPath(path, opts = {}) {
      if (opts.parent) {
        path = PATH.dirname(path);
      }
      var st = fs.lstatSync(path);
      var mode = NODEFS.getMode(path);
      return { path, node: { id: st.ino, mode, node_ops: NODERAWFS, path }};
    },
    createStandardStreams() {
      FS.createStream({ nfd: 0, position: 0, path: '/dev/stdin', flags: 0, seekable: false }, 0);
      var paths = [,'/dev/stdout', '/dev/stderr'];
      for (var i = 1; i < 3; i++) {
        FS.createStream({ nfd: i, position: 0, path: paths[i], flags: {{{ cDefs.O_TRUNC | cDefs.O_CREAT | cDefs.O_WRONLY }}}, seekable: false }, i);
      }
    },
    // generic function for all node creation
    cwd() { return process.cwd(); },
    chdir(...args) { process.chdir(...args); },
    mknod(path, mode) {
      if (FS.isDir(path)) {
        fs.mkdirSync(path, mode);
      } else {
        fs.writeFileSync(path, '', { mode: mode });
      }
    },
    mkdir(...args) { fs.mkdirSync(...args); },
    symlink(...args) { fs.symlinkSync(...args); },
    link(oldpath, newpath, flags) {
      // AT_SYMLINK_FOLLOW (0x400): dereference oldpath if it is a symlink,
      // since node's link(2) links to the symlink itself by default.
      if (flags & 0x400) {
        oldpath = fs.realpathSync(oldpath);
      }
      fs.linkSync(oldpath, newpath);
    },
    rename(...args) { fs.renameSync(...args); },
    rmdir(...args) { fs.rmdirSync(...args); },
    readdir(...args) { return ['.', '..'].concat(fs.readdirSync(...args)); },
    unlink(...args) { fs.unlinkSync(...args); },
    readlink(...args) { return fs.readlinkSync(...args); },
    stat(path, dontFollow) {
      var stat = dontFollow ? fs.lstatSync(path) : fs.statSync(path);
      if (NODEFS.isWindows) {
        // Windows does not report the 'x' permission bit, so propagate read
        // bits to execute bits.
        stat.mode |= (stat.mode & {{{ cDefs.S_IRUGO }}}) >> 2;
      }
      return stat;
    },
    fstat(fd) {
      var stream = FS.getStreamChecked(fd);
      // Virtual streams (pipes, sockets) have no backing node fd; defer to their
      // own getattr rather than node's fs.fstatSync.
      var getattr = stream.stream_ops?.getattr ?? stream.node.node_ops?.getattr;
      if (getattr) {
        return getattr(stream.stream_ops?.getattr ? stream : stream.node);
      }
      return fs.fstatSync(stream.nfd);
    },
    statfs(path) {
      // Node's fs.statfsSync API doesn't provide these attributes so include
      // some defaults.
      var defaults = {
        fsid: 42,
        flags: 2,
        namelen: 255,
      }
      return Object.assign(defaults, fs.statfsSync(path));
    },
    statfsStream(stream) {
      return FS.statfs(stream.path);
    },
    chmod(path, mode, dontFollow) {
      mode &= {{{ cDefs.S_IALLUGO }}};
      if (NODEFS.isWindows) {
        // Windows only supports S_IREAD / S_IWRITE (S_IRUSR / S_IWUSR)
        // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/chmod-wchmod
        mode &= {{{ cDefs.S_IRUSR | cDefs.S_IWUSR }}};
      }
      if (dontFollow && fs.lstatSync(path).isSymbolicLink()) {
        // Node (and indeed linux) does not support chmod on symlinks
        // https://nodejs.org/api/fs.html#fslchmodsyncpath-mode
        throw new FS.ErrnoError({{{ cDefs.EOPNOTSUPP }}});
      }
      fs.chmodSync(path, mode);
    },
    fchmod(fd, mode) {
      var stream = FS.getStreamChecked(fd);
      fs.fchmodSync(stream.nfd, mode);
    },
    chown(...args) { fs.chownSync(...args); },
    fchown(fd, owner, group) {
      var stream = FS.getStreamChecked(fd);
      fs.fchownSync(stream.nfd, owner, group);
    },
    truncate(path, len) {
      // See https://github.com/nodejs/node/issues/35632
      if (len < 0) {
        throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
      }
      return fs.truncateSync(path, len);
    },
    ftruncate(fd, len) {
      // See https://github.com/nodejs/node/issues/35632
      if (len < 0) {
        throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
      }
      var stream = FS.getStreamChecked(fd);
      fs.ftruncateSync(stream.nfd, len);
    },
    utime(path, atime, mtime, dontFollow) {
      // null here for atime or mtime means UTIME_OMIT was passed.  Since node
      // doesn't support this concept we need to first find the existing
      // timestamps in order to preserve them.
      if ((atime === null) || (mtime === null)) {
        var st = dontFollow ? fs.lstatSync(path) : fs.statSync(path);
        atime ||= st.atimeMs;
        mtime ||= st.mtimeMs;
      }
      if (dontFollow) {
        fs.lutimesSync(path, atime/1000, mtime/1000);
      } else {
        fs.utimesSync(path, atime/1000, mtime/1000);
      }
    },
    open(path, flags, mode) {
      flags = FS_modeStringToFlags(flags);
      var pathTruncated = path.split('/').map((s) => s.slice(0, 255)).join('/');
      var nfd = fs.openSync(pathTruncated, NODEFS.flagsForNode(flags), mode);
      var st = fs.fstatSync(nfd);
      if (flags & {{{ cDefs.O_DIRECTORY }}} && !st.isDirectory()) {
        fs.closeSync(nfd);
        throw new FS.ErrnoError(ERRNO_CODES.ENOTDIR);
      }
      var newMode = NODEFS.getMode(pathTruncated);
      var node = { id: st.ino, mode: newMode, node_ops: NODERAWFS, path }
      return FS.createStream({ nfd, position: 0, path, flags, node, seekable: true });
    },
    createStream(stream, fd) {
      // Call the original FS.createStream
      var rtn = VFS.createStream(stream, fd);
      // Detect PIPEFS streams and skip the refcnt/tty initialization in that case.
      if (!stream.stream_ops) {
        rtn.shared.refcnt ??= 0;
        rtn.shared.refcnt++;
        rtn.tty = nodeTTY.isatty(rtn.nfd);
      }
      return rtn;
    },
  },

  /**
   * These functions all take a stream as the first argument which
   * could either be a stream created by NODERAWFS itself, or, for example
   * one created by PIPEFS.  We wrap all these function in an extra check.
   */
  $NODERAWFS_stream_funcs: {
    close(stream) {
      VFS.closeStream(stream.fd);
      // Don't close stdin/stdout/stderr since they are used by node itself.
      if (--stream.shared.refcnt <= 0 && stream.nfd > 2) {
        // This stream is created by our Node.js filesystem, close the
        // native file descriptor when its reference count drops to 0.
        fs.closeSync(stream.nfd);
      }
    },
    llseek(stream, offset, whence) {
      var position = offset;
      if (whence === {{{ cDefs.SEEK_CUR }}}) {
        position += stream.position;
      } else if (whence === {{{ cDefs.SEEK_END }}}) {
        position += fs.fstatSync(stream.nfd).size;
      } else if (whence !== {{{ cDefs.SEEK_SET }}}) {
        throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
      }

      if (position < 0) {
        throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
      }
      stream.position = position;
      return position;
    },
    read(stream, buffer, offset, length, position) {
      var seeking = typeof position != 'undefined';
      if (!seeking && stream.seekable) position = stream.position;
      var bytesRead = fs.readSync(stream.nfd, buffer, offset, length, position);
      // update position marker when non-seeking
      if (!seeking) stream.position += bytesRead;
      return bytesRead;
    },
    write(stream, buffer, offset, length, position) {
      if (stream.flags & {{{ cDefs.O_APPEND }}}) {
        // seek to the end before writing in append mode
        FS.llseek(stream, 0, {{{ cDefs.SEEK_END }}});
      }
      var seeking = typeof position != 'undefined';
      if (!seeking && stream.seekable) position = stream.position;
      var bytesWritten = fs.writeSync(stream.nfd, buffer, offset, length, position);
      // update position marker when non-seeking
      if (!seeking) stream.position += bytesWritten;
      return bytesWritten;
    },
    mmap(stream, length, position, prot, flags) {
      if (!length) {
        throw new FS.ErrnoError({{{ cDefs.EINVAL }}});
      }
      var ptr = mmapAlloc(length);
      FS.read(stream, HEAP8, ptr, length, position);
      return { ptr, allocated: true };
    },
    msync(stream, buffer, offset, length, mmapFlags) {
      FS.write(stream, buffer, 0, length, offset);
      // should we check if bytesWritten and length are the same?
      return 0;
    },
    ioctl(stream, cmd, arg) {
      throw new FS.ErrnoError({{{ cDefs.ENOTTY }}});
    },
  },
});
