const FIFO = require('fast-fifo')
const EventEmitter = require('bare-events')
const path = require('bare-path')
const { fileURLToPath } = require('bare-url')
const { Readable, Writable } = require('bare-stream')
const binding = require('./binding')
const constants = require('./lib/constants')
const FileError = require('./lib/errors')

const isWindows = Bare.platform === 'win32'

exports.constants = constants

class FileRequest {
  static _free = []

  static borrow() {
    if (this._free.length > 0) return this._free.pop()
    return new FileRequest()
  }

  static return(req) {
    if (this._free.length < 32) this._free.push(req.reset())
    else req.destroy()
  }

  constructor() {
    this._reset()
    this._handle = binding.requestInit(this, this._onresult)
  }

  get handle() {
    return this._handle
  }

  retain(value) {
    this._retain = value // Tie the lifetime of `value` to the lifetime of `this`
  }

  reset() {
    if (this._handle === null) return this

    binding.requestReset(this._handle)

    this._reset()

    return this
  }

  destroy() {
    if (this._handle === null) return this

    binding.requestDestroy(this._handle)

    this._reset()
    this._handle = null

    return this
  }

  then(resolve, reject) {
    return this._promise.then(resolve, reject)
  }

  return() {
    if (this._handle === null) return this

    FileRequest.return(this)

    return this
  }

  [Symbol.dispose]() {
    this.return()
  }

  _reset() {
    const { promise, resolve, reject } = Promise.withResolvers()

    this._promise = promise
    this._resolve = resolve
    this._reject = reject
    this._retain = null
  }

  _onresult(err, status) {
    if (err) this._reject(err)
    else this._resolve(status)
  }
}

function ok(result, cb) {
  if (typeof result === 'function') {
    cb = result
    result = undefined
  }

  if (cb) cb(null, result)
  else return result
}

function fail(err, cb) {
  if (cb) cb(err)
  else throw err
}

function done(err, result, cb) {
  if (typeof result === 'function') {
    cb = result
    result = undefined
  }

  if (err) fail(err, cb)
  else return ok(result, cb)
}

async function open(filepath, flags = 'r', mode = 0o666, cb) {
  if (typeof flags === 'function') {
    cb = flags
    flags = 'r'
    mode = 0o666
  } else if (typeof mode === 'function') {
    cb = mode
    mode = 0o666
  }

  if (typeof flags === 'string') flags = toFlags(flags)
  if (typeof mode === 'string') mode = toMode(mode)

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let fd
  let err = null
  try {
    binding.open(req.handle, filepath, flags, mode)

    fd = await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'open',
      code: e.code,
      path: filepath
    })
  }

  return done(err, fd, cb)
}

function openSync(filepath, flags = 'r', mode = 0o666) {
  if (typeof flags === 'string') flags = toFlags(flags)
  if (typeof mode === 'string') mode = toMode(mode)

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    return binding.openSync(req.handle, filepath, flags, mode)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'open',
      code: e.code,
      path: filepath
    })
  }
}

async function close(fd, cb) {
  using req = FileRequest.borrow()

  let err = null
  try {
    binding.close(req.handle, fd)

    await req
  } catch (e) {
    err = new FileError(e.message, { operation: 'close', code: e.code, fd })
  }

  return done(err, cb)
}

function closeSync(fd) {
  using req = FileRequest.borrow()

  try {
    binding.closeSync(req.handle, fd)
  } catch (e) {
    throw new FileError(e.message, { operation: 'close', code: e.code, fd })
  }
}

async function access(filepath, mode = constants.F_OK, cb) {
  if (typeof mode === 'function') {
    cb = mode
    mode = constants.F_OK
  }

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.access(req.handle, filepath, mode)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'access',
      code: e.code,
      path: filepath
    })
  }

  return done(err, cb)
}

function accessSync(filepath, mode = constants.F_OK) {
  using req = FileRequest.borrow()

  filepath = toNamespacedPath(filepath)

  try {
    binding.accessSync(req.handle, filepath, mode)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'access',
      code: e.code,
      path: filepath
    })
  }
}

async function exists(filepath, cb) {
  let ok = true
  try {
    await access(filepath)
  } catch {
    ok = false
  }

  return done(null, ok, cb)
}

function existsSync(filepath) {
  try {
    accessSync(filepath)
  } catch {
    return false
  }

  return true
}

async function read(
  fd,
  buffer,
  offset = 0,
  len = buffer.byteLength - offset,
  pos = -1,
  cb
) {
  if (typeof offset === 'function') {
    cb = offset
    offset = 0
    len = buffer.byteLength
    pos = -1
  } else if (typeof len === 'function') {
    cb = len
    len = buffer.byteLength - offset
    pos = -1
  } else if (typeof pos === 'function') {
    cb = pos
    pos = -1
  }

  if (typeof pos !== 'number') pos = -1

  using req = FileRequest.borrow()

  let bytes
  let err = null
  try {
    binding.read(req.handle, fd, buffer, offset, len, pos)

    bytes = await req
  } catch (e) {
    err = new FileError(e.message, { operation: 'read', code: e.code, fd })
  }

  return done(err, bytes, cb)
}

function readSync(
  fd,
  buffer,
  offset = 0,
  len = buffer.byteLength - offset,
  pos = -1
) {
  using req = FileRequest.borrow()

  try {
    return binding.readSync(req.handle, fd, buffer, offset, len, pos)
  } catch (e) {
    throw new FileError(e.message, { operation: 'read', code: e.code, fd })
  }
}

async function readv(fd, buffers, pos = -1, cb) {
  if (typeof pos === 'function') {
    cb = pos
    pos = -1
  }

  if (typeof pos !== 'number') pos = -1

  using req = FileRequest.borrow()

  let bytes
  let err = null
  try {
    binding.readv(req.handle, fd, buffers, pos)

    bytes = await req
  } catch (e) {
    err = new FileError(e.message, { operation: 'readv', code: e.code, fd })
  }

  return done(err, bytes, cb)
}

function readvSync(fd, buffers, pos = -1) {
  if (typeof pos !== 'number') pos = -1

  using req = FileRequest.borrow()

  try {
    return binding.readvSync(req.handle, fd, buffers, pos)
  } catch (e) {
    throw new FileError(e.message, { operation: 'readv', code: e.code, fd })
  }
}

async function write(fd, data, offset = 0, len, pos = -1, cb) {
  if (typeof data === 'string') {
    let encoding = len
    cb = pos
    pos = offset

    if (typeof pos === 'function') {
      cb = pos
      pos = -1
      encoding = 'utf8'
    } else if (typeof encoding === 'function') {
      cb = encoding
      encoding = 'utf8'
    }

    if (typeof pos === 'string') {
      encoding = pos
      pos = -1
    }

    data = Buffer.from(data, encoding)
    offset = 0
    len = data.byteLength
  } else if (typeof offset === 'function') {
    cb = offset
    offset = 0
    len = data.byteLength
    pos = -1
  } else if (typeof len === 'function') {
    cb = len
    len = data.byteLength - offset
    pos = -1
  } else if (typeof pos === 'function') {
    cb = pos
    pos = -1
  }

  if (typeof len !== 'number') len = data.byteLength - offset
  if (typeof pos !== 'number') pos = -1

  using req = FileRequest.borrow()

  let bytes
  let err = null
  try {
    binding.write(req.handle, fd, data, offset, len, pos)

    bytes = await req
  } catch (e) {
    err = new FileError(e.message, { operation: 'write', code: e.code, fd })
  }

  return done(err, bytes, cb)
}

function writeSync(fd, data, offset = 0, len, pos = -1) {
  if (typeof data === 'string') {
    let encoding = len
    pos = offset

    if (typeof pos === 'string') {
      encoding = pos
      pos = -1
    }

    data = Buffer.from(data, encoding)
    offset = 0
    len = data.byteLength
  }

  if (typeof len !== 'number') len = data.byteLength - offset
  if (typeof pos !== 'number') pos = -1

  using req = FileRequest.borrow()

  try {
    return binding.writeSync(req.handle, fd, data, offset, len, pos)
  } catch (e) {
    throw new FileError(e.message, { operation: 'write', code: e.code, fd })
  }
}

async function writev(fd, buffers, pos = -1, cb) {
  if (typeof pos === 'function') {
    cb = pos
    pos = -1
  }

  if (typeof pos !== 'number') pos = -1

  using req = FileRequest.borrow()

  let bytes
  let err = null
  try {
    binding.writev(req.handle, fd, buffers, pos)

    bytes = await req
  } catch (e) {
    err = new FileError(e.message, { operation: 'writev', code: e.code, fd })
  }

  return done(err, bytes, cb)
}

function writevSync(fd, buffers, pos = -1) {
  if (typeof pos !== 'number') pos = -1

  using req = FileRequest.borrow()

  try {
    return binding.writevSync(req.handle, fd, buffers, pos)
  } catch (e) {
    throw new FileError(e.message, { operation: 'writev', code: e.code, fd })
  }
}

async function stat(filepath, cb) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let st
  let err = null
  try {
    binding.stat(req.handle, filepath)

    await req

    st = new Stats(...binding.requestResultStat(req.handle))
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'stat',
      code: e.code,
      path: filepath
    })
  }

  return done(err, st, cb)
}

function statSync(filepath) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.statSync(req.handle, filepath)

    return new Stats(...binding.requestResultStat(req.handle))
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'stat',
      code: e.code,
      path: filepath
    })
  }
}

async function lstat(filepath, cb) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let st
  let err = null
  try {
    binding.lstat(req.handle, filepath)

    await req

    st = new Stats(...binding.requestResultStat(req.handle))
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'lstat',
      code: e.code,
      path: filepath
    })
  }

  return done(err, st, cb)
}

function lstatSync(filepath) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.lstatSync(req.handle, filepath)

    return new Stats(...binding.requestResultStat(req.handle))
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'lstat',
      code: e.code,
      path: filepath
    })
  }
}

async function fstat(fd, cb) {
  using req = FileRequest.borrow()

  let st
  let err = null
  try {
    binding.fstat(req.handle, fd)

    await req

    st = new Stats(...binding.requestResultStat(req.handle))
  } catch (e) {
    err = new FileError(e.message, { operation: 'fstat', code: e.code, fd })
  }

  return done(err, st, cb)
}

function fstatSync(fd) {
  using req = FileRequest.borrow()

  try {
    binding.fstatSync(req.handle, fd)

    return new Stats(...binding.requestResultStat(req.handle))
  } catch (e) {
    throw new FileError(e.message, { operation: 'fstat', code: e.code, fd })
  }
}

async function ftruncate(fd, len = 0, cb) {
  if (typeof len === 'function') {
    cb = len
    len = 0
  }

  if (typeof len !== 'number') len = 0

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.ftruncate(req.handle, fd, len)

    await req
  } catch (e) {
    err = new FileError(e.message, { operation: 'ftruncate', code: e.code, fd })
  }

  return done(err, cb)
}

function ftruncateSync(fd, len = 0) {
  if (typeof len !== 'number') len = 0

  using req = FileRequest.borrow()

  try {
    binding.ftruncateSync(req.handle, fd, len)
  } catch (e) {
    throw new FileError(e.message, { operation: 'ftruncate', code: e.code, fd })
  }
}

async function chmod(filepath, mode, cb) {
  if (typeof mode === 'string') mode = toMode(mode)

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.chmod(req.handle, filepath, mode)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'chmod',
      code: e.code,
      path: filepath
    })
  }

  return done(err, cb)
}

function chmodSync(filepath, mode) {
  if (typeof mode === 'string') mode = toMode(mode)

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.chmodSync(req.handle, filepath, mode)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'chmod',
      code: e.code,
      path: filepath
    })
  }
}

async function fchmod(fd, mode, cb) {
  if (typeof mode === 'string') mode = toMode(mode)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.fchmod(req.handle, fd, mode)

    await req
  } catch (e) {
    err = new FileError(e.message, { operation: 'fchmod', code: e.code, fd })
  }

  return done(err, cb)
}

function fchmodSync(fd, mode) {
  if (typeof mode === 'string') mode = toMode(mode)

  using req = FileRequest.borrow()

  try {
    binding.fchmodSync(req.handle, fd, mode)
  } catch (e) {
    throw new FileError(e.message, { operation: 'fchmod', code: e.code, fd })
  }
}

async function utimes(filepath, atime, mtime, cb) {
  if (typeof atime !== 'number') atime = atime.getTime() / 1000
  if (typeof mtime !== 'number') mtime = mtime.getTime() / 1000

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.utimes(req.handle, filepath, atime, mtime)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'utimes',
      code: e.code,
      path: filepath
    })
  }

  return done(err, cb)
}

function utimesSync(filepath, atime, mtime) {
  if (typeof atime !== 'number') atime = atime.getTime() / 1000
  if (typeof mtime !== 'number') mtime = mtime.getTime() / 1000

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.utimesSync(req.handle, filepath, atime, mtime)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'utimes',
      code: e.code,
      path: filepath
    })
  }
}

async function mkdir(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = { mode: 0o777 }
  }

  if (typeof opts === 'number') opts = { mode: opts }
  else if (!opts) opts = {}

  const mode = typeof opts.mode === 'number' ? opts.mode : 0o777

  filepath = toNamespacedPath(filepath)

  if (opts.recursive) {
    try {
      await mkdir(filepath, { mode })
    } catch (err) {
      if (err.code !== 'ENOENT' && (await stat(filepath)).isDirectory()) {
        return ok(cb)
      }

      while (filepath.endsWith(path.sep)) filepath = filepath.slice(0, -1)
      const i = filepath.lastIndexOf(path.sep)
      if (i <= 0) return fail(err, cb)

      await mkdir(filepath.slice(0, i), { mode, recursive: true })

      try {
        await mkdir(filepath, { mode })
      } catch (err) {
        if ((await stat(filepath)).isDirectory()) {
          return ok(cb)
        }

        return fail(err, cb)
      }
    }

    return ok(cb)
  }

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.mkdir(req.handle, filepath, mode)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'mkdir',
      code: e.code,
      path: filepath
    })
  }

  return done(err, cb)
}

function mkdirSync(filepath, opts) {
  if (typeof opts === 'number') opts = { mode: opts }
  else if (!opts) opts = {}

  const mode = typeof opts.mode === 'number' ? opts.mode : 0o777

  filepath = toNamespacedPath(filepath)

  if (opts.recursive) {
    try {
      mkdirSync(filepath, { mode })
    } catch (err) {
      if (err.code !== 'ENOENT' && statSync(filepath).isDirectory()) {
        return
      }

      while (filepath.endsWith(path.sep)) filepath = filepath.slice(0, -1)
      const i = filepath.lastIndexOf(path.sep)
      if (i <= 0) throw err

      mkdirSync(filepath.slice(0, i), { mode, recursive: true })

      try {
        mkdirSync(filepath, { mode })
      } catch (err) {
        if (statSync(filepath).isDirectory()) {
          return
        }

        throw err
      }
    }

    return
  }

  using req = FileRequest.borrow()

  try {
    binding.mkdirSync(req.handle, filepath, mode)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'mkdir',
      code: e.code,
      path: filepath
    })
  }
}

async function rmdir(filepath, cb) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.rmdir(req.handle, filepath)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'rmdir',
      code: e.code,
      path: filepath
    })
  }

  return done(err, cb)
}

function rmdirSync(filepath) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.rmdirSync(req.handle, filepath)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'rmdir',
      code: e.code,
      path: filepath
    })
  }
}

async function rm(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (!opts) opts = {}

  filepath = toNamespacedPath(filepath)

  try {
    const st = await lstat(filepath)

    if (st.isDirectory()) {
      if (opts.recursive) {
        try {
          await rmdir(filepath)
        } catch (err) {
          if (err.code !== 'ENOTEMPTY') throw err

          const files = await readdir(filepath)

          for (const file of files) {
            await rm(filepath + path.sep + file, opts)
          }

          await rmdir(filepath)
        }
      } else {
        fail(
          new FileError('is a directory', {
            operation: 'rm',
            code: 'EISDIR',
            path: filepath
          }),
          cb
        )
      }
    } else {
      await unlink(filepath)
    }
  } catch (err) {
    if (err.code !== 'ENOENT' || !opts.force) fail(err, cb)
  }
}

function rmSync(filepath, opts) {
  if (!opts) opts = {}

  filepath = toNamespacedPath(filepath)

  try {
    const st = lstatSync(filepath)

    if (st.isDirectory()) {
      if (opts.recursive) {
        try {
          rmdirSync(filepath)
        } catch (err) {
          if (err.code !== 'ENOTEMPTY') throw err

          const files = readdirSync(filepath)

          for (const file of files) {
            rmSync(filepath + path.sep + file, opts)
          }

          rmdirSync(filepath)
        }
      } else {
        throw new FileError('is a directory', {
          operation: 'rm',
          code: 'EISDIR',
          path: filepath
        })
      }
    } else {
      unlinkSync(filepath)
    }
  } catch (err) {
    if (err.code !== 'ENOENT' || !opts.force) throw err
  }
}

async function unlink(filepath, cb) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.unlink(req.handle, filepath)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'unlink',
      code: e.code,
      path: filepath
    })
  }

  return done(err, cb)
}

function unlinkSync(filepath) {
  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.unlinkSync(req.handle, filepath)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'unlink',
      code: e.code,
      path: filepath
    })
  }
}

async function rename(src, dst, cb) {
  src = toNamespacedPath(src)
  dst = toNamespacedPath(dst)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.rename(req.handle, src, dst)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'rename',
      code: e.code,
      path: src,
      destination: dst
    })
  }

  return done(err, cb)
}

function renameSync(src, dst) {
  src = toNamespacedPath(src)
  dst = toNamespacedPath(dst)

  using req = FileRequest.borrow()

  try {
    binding.renameSync(req.handle, src, dst)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'rename',
      code: e.code,
      path: src,
      destination: dst
    })
  }
}

async function copyFile(src, dst, mode = 0, cb) {
  if (typeof mode === 'function') {
    cb = mode
    mode = 0
  }

  src = toNamespacedPath(src)
  dst = toNamespacedPath(dst)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.copyfile(req.handle, src, dst, mode)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'copyfile',
      code: e.code,
      path: src,
      destination: dst
    })
  }

  return done(err, cb)
}

function copyFileSync(src, dst, mode = 0) {
  src = toNamespacedPath(src)
  dst = toNamespacedPath(dst)

  using req = FileRequest.borrow()

  try {
    binding.copyfileSync(req.handle, src, dst, mode)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'copyfile',
      code: e.code,
      path: src,
      destination: dst
    })
  }
}

async function realpath(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { encoding = 'utf8' } = opts

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let res
  let err = null
  try {
    binding.realpath(req.handle, filepath)

    await req

    res = Buffer.from(binding.requestResultString(req.handle))

    if (encoding !== 'buffer') res = res.toString(encoding)
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'realpath',
      code: e.code,
      path: filepath
    })
  }

  return done(err, res, cb)
}

function realpathSync(filepath, opts) {
  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { encoding = 'utf8' } = opts

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.realpathSync(req.handle, filepath)

    let res = Buffer.from(binding.requestResultString(req.handle))

    if (encoding !== 'buffer') res = res.toString(encoding)

    return res
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'realpath',
      code: e.code,
      path: filepath
    })
  }
}

async function readlink(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { encoding = 'utf8' } = opts

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let res
  let err = null
  try {
    binding.readlink(req.handle, filepath)

    await req

    res = Buffer.from(binding.requestResultString(req.handle))

    if (encoding !== 'buffer') res = res.toString(encoding)
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'readlink',
      code: e.code,
      path: filepath
    })
  }

  return done(err, res, cb)
}

function readlinkSync(filepath, opts) {
  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { encoding = 'utf8' } = opts

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.readlinkSync(req.handle, filepath)

    let res = Buffer.from(binding.requestResultString(req.handle))

    if (encoding !== 'buffer') res = res.toString(encoding)

    return res
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'readlink',
      code: e.code,
      path: filepath
    })
  }
}

function normalizeSymlinkTarget(target, type, filepath) {
  if (isWindows) {
    if (type === 'junction') target = path.resolve(filepath, '..', target)

    if (path.isAbsolute(target)) return path.toNamespacedPath(target)

    return target.replace(/\//g, path.sep)
  }

  return target
}

async function symlink(target, filepath, type, cb) {
  if (typeof type === 'function') {
    cb = type
    type = null
  }

  filepath = toNamespacedPath(filepath)

  if (typeof type === 'string') {
    switch (type) {
      case 'file':
      default:
        type = 0
        break
      case 'dir':
        type = constants.UV_FS_SYMLINK_DIR
        break
      case 'junction':
        type = constants.UV_FS_SYMLINK_JUNCTION
        break
    }
  } else if (typeof type !== 'number') {
    if (isWindows) {
      target = path.resolve(filepath, '..', target)

      try {
        type = (await stat(target)).isDirectory()
          ? constants.UV_FS_SYMLINK_DIR
          : constants.UV_FS_SYMLINK_JUNCTION
      } catch {
        type = 0
      }
    } else {
      type = 0
    }
  }

  target = normalizeSymlinkTarget(target)

  using req = FileRequest.borrow()

  let err = null
  try {
    binding.symlink(req.handle, target, filepath, type)

    await req
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'symlink',
      code: e.code,
      path: target,
      destination: filepath
    })
  }

  return done(err, cb)
}

function symlinkSync(target, filepath, type) {
  filepath = toNamespacedPath(filepath)

  if (typeof type === 'string') {
    switch (type) {
      case 'file':
      default:
        type = 0
        break
      case 'dir':
        type = constants.UV_FS_SYMLINK_DIR
        break
      case 'junction':
        type = constants.UV_FS_SYMLINK_JUNCTION
        break
    }
  } else if (typeof type !== 'number') {
    if (isWindows) {
      target = path.resolve(filepath, '..', target)

      try {
        type = statSync(target).isDirectory()
          ? constants.UV_FS_SYMLINK_DIR
          : constants.UV_FS_SYMLINK_JUNCTION
      } catch {
        type = 0
      }
    } else {
      type = 0
    }
  }

  target = normalizeSymlinkTarget(target)

  using req = FileRequest.borrow()

  try {
    binding.symlinkSync(req.handle, target, filepath, type)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'symlink',
      code: e.code,
      path: target,
      destination: filepath
    })
  }
}

async function opendir(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  let dir
  let err = null
  try {
    binding.opendir(req.handle, filepath)

    await req

    dir = new Dir(filepath, binding.requestResultDir(req.handle), opts)
  } catch (e) {
    err = new FileError(e.message, {
      operation: 'opendir',
      code: e.code,
      path: filepath
    })
  }

  return done(err, dir, cb)
}

function opendirSync(filepath, opts) {
  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  filepath = toNamespacedPath(filepath)

  using req = FileRequest.borrow()

  try {
    binding.opendirSync(req.handle, filepath)

    return new Dir(filepath, binding.requestResultDir(req.handle), opts)
  } catch (e) {
    throw new FileError(e.message, {
      operation: 'opendir',
      code: e.code,
      path: filepath
    })
  }
}

async function readdir(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { withFileTypes = false } = opts

  filepath = toNamespacedPath(filepath)

  let result = []
  let err = null
  try {
    const dir = await opendir(filepath)

    for await (const entry of dir) {
      result.push(withFileTypes ? entry : entry.name)
    }
  } catch (e) {
    result = []
    err = e
  }

  return done(err, result, cb)
}

function readdirSync(filepath, opts) {
  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { withFileTypes = false } = opts

  filepath = toNamespacedPath(filepath)

  const dir = opendirSync(filepath, opts)
  const result = []

  for (const entry of dir) {
    result.push(withFileTypes ? entry : entry.name)
  }

  return result
}

async function readFile(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { encoding = 'buffer' } = opts

  let fd = -1
  try {
    fd = await open(filepath, opts.flag || 'r')

    const st = await fstat(fd)

    let buffer
    let len = 0

    if (st.size === 0) {
      const buffers = []

      while (true) {
        buffer = Buffer.allocUnsafe(8192)
        const r = await read(fd, buffer)
        len += r
        if (r === 0) break
        buffers.push(buffer.subarray(0, r))
      }

      buffer = Buffer.concat(buffers)
    } else {
      buffer = Buffer.allocUnsafe(st.size)

      while (true) {
        const r = await read(fd, len ? buffer.subarray(len) : buffer)
        len += r
        if (r === 0 || len === buffer.byteLength) break
      }

      if (len !== buffer.byteLength) buffer = buffer.subarray(0, len)
    }

    if (encoding !== 'buffer') buffer = buffer.toString(encoding)

    return ok(buffer, cb)
  } catch (err) {
    fail(err, cb)
  } finally {
    if (fd !== -1) await close(fd)
  }
}

function readFileSync(filepath, opts) {
  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  const { encoding = 'buffer' } = opts

  let fd = -1
  try {
    fd = openSync(filepath, opts.flag || 'r')

    const st = fstatSync(fd)

    let buffer
    let len = 0

    if (st.size === 0) {
      const buffers = []

      while (true) {
        buffer = Buffer.allocUnsafe(8192)
        const r = readSync(fd, buffer)
        len += r
        if (r === 0) break
        buffers.push(buffer.subarray(0, r))
      }

      buffer = Buffer.concat(buffers)
    } else {
      buffer = Buffer.allocUnsafe(st.size)

      while (true) {
        const r = readSync(fd, len ? buffer.subarray(len) : buffer)
        len += r
        if (r === 0 || len === buffer.byteLength) break
      }

      if (len !== buffer.byteLength) buffer = buffer.subarray(0, len)
    }

    if (encoding !== 'buffer') buffer = buffer.toString(encoding)

    return buffer
  } finally {
    if (fd !== -1) closeSync(fd)
  }
}

async function writeFile(filepath, data, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  if (typeof data === 'string') data = Buffer.from(data, opts.encoding)

  let fd = -1
  try {
    fd = await open(filepath, opts.flag || 'w', opts.mode || 0o666)

    let len = 0

    while (true) {
      len += await write(fd, len ? data.subarray(len) : data)
      if (len === data.byteLength) break
    }

    return ok(cb)
  } catch (err) {
    fail(err, cb)
  } finally {
    if (fd !== -1) await close(fd)
  }
}

function writeFileSync(filepath, data, opts) {
  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  if (typeof data === 'string') data = Buffer.from(data, opts.encoding)

  let fd = -1
  try {
    fd = openSync(filepath, opts.flag || 'w', opts.mode || 0o666)

    let len = 0

    while (true) {
      len += writeSync(fd, len ? data.subarray(len) : data)
      if (len === data.byteLength) break
    }
  } finally {
    if (fd !== -1) closeSync(fd)
  }
}

function appendFile(filepath, data, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  if (!opts.flag) opts = { ...opts, flag: 'a' }

  return writeFile(filepath, data, opts, cb)
}

function appendFileSync(filepath, data, opts) {
  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  if (!opts.flag) opts = { ...opts, flag: 'a' }

  return writeFileSync(filepath, data, opts)
}

function watch(filepath, opts, cb) {
  if (typeof opts === 'function') {
    cb = opts
    opts = {}
  }

  if (typeof opts === 'string') opts = { encoding: opts }
  else if (!opts) opts = {}

  filepath = toNamespacedPath(filepath)

  return new Watcher(filepath, opts, cb)
}

class Stats {
  constructor(
    dev,
    mode,
    nlink,
    uid,
    gid,
    rdev,
    blksize,
    ino,
    size,
    blocks,
    atimeMs,
    mtimeMs,
    ctimeMs,
    birthtimeMs
  ) {
    this.dev = dev
    this.mode = mode
    this.nlink = nlink
    this.uid = uid
    this.gid = gid
    this.rdev = rdev
    this.blksize = blksize
    this.ino = ino
    this.size = size
    this.blocks = blocks
    this.atimeMs = atimeMs
    this.mtimeMs = mtimeMs
    this.ctimeMs = ctimeMs
    this.birthtimeMs = birthtimeMs
    this.atime = new Date(atimeMs)
    this.mtime = new Date(mtimeMs)
    this.ctime = new Date(ctimeMs)
    this.birthtime = new Date(birthtimeMs)
  }

  isDirectory() {
    return (this.mode & constants.S_IFMT) === constants.S_IFDIR
  }

  isFile() {
    return (this.mode & constants.S_IFMT) === constants.S_IFREG
  }

  isBlockDevice() {
    return (this.mode & constants.S_IFMT) === constants.S_IFBLK
  }

  isCharacterDevice() {
    return (this.mode & constants.S_IFCHR) === constants.S_IFCHR
  }

  isFIFO() {
    return (this.mode & constants.S_IFMT) === constants.S_IFIFO
  }

  isSymbolicLink() {
    return (this.mode & constants.S_IFMT) === constants.S_IFLNK
  }

  isSocket() {
    return (this.mode & constants.S_IFMT) === constants.S_IFSOCK
  }
}

class Dir {
  constructor(path, handle, opts = {}) {
    const { encoding = 'utf8', bufferSize = 32 } = opts

    this.path = path

    this._encoding = encoding
    this._capacity = bufferSize
    this._buffer = new FIFO()
    this._ended = false
    this._handle = handle
  }

  async read(cb) {
    if (this._buffer.length) return ok(this._buffer.shift(), cb)
    if (this._ended) return ok(null, cb)

    using req = FileRequest.borrow()

    let entries
    let err = null
    try {
      req.retain(binding.readdir(req.handle, this._handle, this._capacity))

      await req

      entries = binding.requestResultDirents(req.handle)
    } catch (e) {
      err = new FileError(e.message, {
        operation: 'readdir',
        code: e.code,
        path: this.path
      })
    }

    if (err) return fail(err, cb)

    if (entries.length === 0) {
      this._ended = true

      return ok(null, cb)
    }

    for (const entry of entries) {
      let name = Buffer.from(entry.name)

      if (this._encoding !== 'buffer') name = name.toString(this._encoding)

      this._buffer.push(new Dirent(this.path, name, entry.type))
    }

    return ok(this._buffer.shift(), cb)
  }

  readSync() {
    if (this._buffer.length) return this._buffer.shift()
    if (this._ended) return null

    using req = FileRequest.borrow()

    let entries
    try {
      req.retain(binding.readdirSync(req.handle, this._handle, this._capacity))

      entries = binding.requestResultDirents(req.handle)
    } catch (e) {
      throw new FileError(e.message, {
        operation: 'readdir',
        code: e.code,
        path: this.path
      })
    }

    if (entries.length === 0) {
      this._ended = true

      return null
    }

    for (const entry of entries) {
      let name = Buffer.from(entry.name)

      if (this._encoding !== 'buffer') name = name.toString(this._encoding)

      this._buffer.push(new Dirent(this.path, name, entry.type))
    }

    return this._buffer.shift()
  }

  async close(cb) {
    using req = FileRequest.borrow()

    let err = null
    try {
      binding.closedir(req.handle, this._handle)

      await req
    } catch (e) {
      err = new FileError(e.message, {
        operation: 'closedir',
        code: e.code,
        path: this.path
      })
    }

    this._handle = null

    return done(err, cb)
  }

  closeSync() {
    using req = FileRequest.borrow()

    try {
      binding.closedirSync(req.handle, this._handle)
    } catch (e) {
      throw new FileError(e.message, {
        operation: 'closedir',
        code: e.code,
        path: this.path
      })
    }

    this._handle = null
  }

  [Symbol.dispose]() {
    this.closeSync()
  }

  async [Symbol.asyncDispose]() {
    await this.close()
  }

  *[Symbol.iterator]() {
    while (true) {
      const entry = this.readSync()
      if (entry === null) break
      yield entry
    }

    this.closeSync()
  }

  async *[Symbol.asyncIterator]() {
    while (true) {
      const entry = await this.read()
      if (entry === null) break
      yield entry
    }

    await this.close()
  }
}

class Dirent {
  constructor(path, name, type) {
    this.path = path
    this.name = name
    this.type = type
  }

  isFile() {
    return this.type === constants.UV_DIRENT_FILE
  }

  isDirectory() {
    return this.type === constants.UV_DIRENT_DIR
  }

  isSymbolicLink() {
    return this.type === constants.UV_DIRENT_LINK
  }

  isFIFO() {
    return this.type === constants.UV_DIRENT_FIFO
  }

  isSocket() {
    return this.type === constants.UV_DIRENT_SOCKET
  }

  isCharacterDevice() {
    return this.type === constants.UV_DIRENT_CHAR
  }

  isBlockDevice() {
    return this.type === constants.UV_DIRENT_BLOCK
  }
}

class FileReadStream extends Readable {
  constructor(path, opts = {}) {
    const { eagerOpen = true } = opts

    super({ eagerOpen, ...opts })

    this.path = path
    this.fd = typeof opts.fd === 'number' ? opts.fd : -1
    this.flags = opts.flags || 'r'
    this.mode = opts.mode || 0o666

    this._offset = opts.start || 0
    this._missing = 0

    if (opts.length) {
      this._missing = opts.length
    } else if (typeof opts.end === 'number') {
      this._missing = opts.end - this._offset + 1
    } else {
      this._missing = -1
    }
  }

  async _open(cb) {
    let err

    if (this.fd === -1) {
      err = null
      try {
        this.fd = await open(this.path, this.flags, this.mode)
      } catch (e) {
        err = e
      }

      if (err) return cb(err)
    }

    let st
    err = null
    try {
      st = await fstat(this.fd)
    } catch (e) {
      err = e
    }

    if (err) return cb(err)

    if (this._missing === -1) this._missing = st.size

    if (st.size < this._offset) {
      this._offset = st.size
      this._missing = 0
    } else if (st.size < this._offset + this._missing) {
      this._missing = st.size - this._offset
    }

    cb(null)
  }

  async _read(size) {
    if (this._missing <= 0) return this.push(null)

    const data = Buffer.allocUnsafe(Math.min(this._missing, size))

    let len
    let err = null
    try {
      len = await read(this.fd, data, 0, data.byteLength, this._offset)
    } catch (e) {
      err = e
    }

    if (err) return cb(err)

    if (len === 0) return this.push(null)

    if (this._missing < len) len = this._missing

    this.push(data.subarray(0, len))

    this._missing -= len
    this._offset += len
  }

  async _destroy(err, cb) {
    if (this.fd === -1) return cb(err)

    err = null
    try {
      await close(this.fd)
    } catch (e) {
      err = e
    }

    cb(err)
  }
}

class FileWriteStream extends Writable {
  constructor(path, opts = {}) {
    const { eagerOpen = true } = opts

    super({ eagerOpen, ...opts })

    this.path = path
    this.fd = typeof opts.fd === 'number' ? opts.fd : -1
    this.flags = opts.flags || 'w'
    this.mode = opts.mode || 0o666
  }

  async _open(cb) {
    if (this.fd !== -1) return cb(null)

    let err = null
    try {
      this.fd = await open(this.path, this.flags, this.mode)
    } catch (e) {
      err = e
    }

    cb(err)
  }

  async _writev(batch, cb) {
    let err = null
    try {
      await writev(
        this.fd,
        batch.map(({ chunk }) => chunk)
      )
    } catch (e) {
      err = e
    }

    cb(err)
  }

  async _destroy(err, cb) {
    if (this.fd === -1) return cb(err)

    err = null
    try {
      await close(this.fd)
    } catch (e) {
      err = e
    }

    cb(err)
  }
}

class Watcher extends EventEmitter {
  constructor(path, opts, onchange) {
    if (typeof opts === 'function') {
      onchange = opts
      opts = {}
    }

    if (!opts) opts = {}

    const { persistent = true, recursive = false, encoding = 'utf8' } = opts

    super()

    this._closed = false
    this._encoding = encoding
    this._handle = binding.watcherInit(
      path,
      recursive,
      this,
      this._onevent,
      this._onclose
    )

    if (!persistent) this.unref()

    if (onchange) this.on('change', onchange)
  }

  close() {
    if (this._closed) return
    this._closed = true

    binding.watcherClose(this._handle)
  }

  ref() {
    if (this._handle) binding.watcherRef(this._handle)
    return this
  }

  unref() {
    if (this._handle) binding.watcherUnref(this._handle)
    return this
  }

  [Symbol.asyncIterator]() {
    const buffer = []
    let done = false
    let error = null
    let next = null

    this.on('change', (eventType, filename) => {
      if (next) {
        next.resolve({ done: false, value: { eventType, filename } })
        next = null
      } else {
        buffer.push({ eventType, filename })
      }
    })
      .on('error', (err) => {
        done = true
        error = err

        if (next) {
          next.reject(error)
          next = null
        }
      })
      .on('close', () => {
        done = true

        if (next) {
          next.resolve({ done })
          next = null
        }
      })

    return {
      next: () =>
        new Promise((resolve, reject) => {
          if (error) return reject(error)

          if (buffer.length)
            return resolve({ done: false, value: buffer.shift() })

          if (done) return resolve({ done })

          next = { resolve, reject }
        })
    }
  }

  _onevent(err, events, filename) {
    if (err) {
      this.close()
      this.emit('error', err)
    } else {
      const path =
        this._encoding === 'buffer'
          ? Buffer.from(filename)
          : Buffer.from(filename).toString(this._encoding)

      if (events & binding.UV_RENAME) {
        this.emit('change', 'rename', path)
      }

      if (events & binding.UV_CHANGE) {
        this.emit('change', 'change', path)
      }
    }
  }

  _onclose() {
    this._handle = null

    this.emit('close')
  }
}

exports.access = access
exports.appendFile = appendFile
exports.chmod = chmod
// exports.chown = chown TODO
exports.close = close
exports.copyFile = copyFile
exports.exists = exists
exports.fchmod = fchmod
// exports.fchown = fchown TODO
// exports.fdatasync = fdatasync TODO
exports.fstat = fstat
// exports.fsync = fsync TODO
exports.ftruncate = ftruncate
// exports.futimes = futimes TODO
// exports.lchmod = lchmod TODO
// exports.lchown = lchown TODO
// exports.lutimes = lutimes TODO
// exports.link = link TODO
exports.lstat = lstat
exports.mkdir = mkdir
// exports.mkdtemp = mkdtemp TODO
exports.open = open
exports.opendir = opendir
exports.read = read
exports.readFile = readFile
exports.readdir = readdir
exports.readlink = readlink
exports.readv = readv
exports.realpath = realpath
exports.rename = rename
exports.rm = rm
exports.rmdir = rmdir
exports.stat = stat
// exports.statfs = statfs TODO
exports.symlink = symlink
// exports.truncate = truncate TODO
exports.unlink = unlink
exports.utimes = utimes
exports.watch = watch
exports.write = write
exports.writeFile = writeFile
exports.writev = writev

exports.accessSync = accessSync
exports.appendFileSync = appendFileSync
exports.chmodSync = chmodSync
// exports.chownSync = chownSync TODO
exports.closeSync = closeSync
exports.copyFileSync = copyFileSync
exports.existsSync = existsSync
exports.fchmodSync = fchmodSync
// exports.fchownSync = fchownSync TODO
// exports.fdatasyncSync = fdatasyncSync TODO
exports.fstatSync = fstatSync
// exports.fsyncSync = fsyncSync TODO
exports.ftruncateSync = ftruncateSync
// exports.futimesSync = futimesSync TODO
// exports.lchmodSync = lchmodSync TODO
// exports.lchownSync = lchownSync TODO
// exports.lutimesSync = lutimesSync TODO
// exports.linkSync = linkSync TODO
exports.lstatSync = lstatSync
exports.mkdirSync = mkdirSync
// exports.mkdtempSync = mkdtempSync TODO
exports.openSync = openSync
exports.opendirSync = opendirSync
exports.readFileSync = readFileSync
exports.readSync = readSync
exports.readdirSync = readdirSync
exports.readlinkSync = readlinkSync
exports.readvSync = readvSync
exports.realpathSync = realpathSync
exports.renameSync = renameSync
exports.rmSync = rmSync
exports.rmdirSync = rmdirSync
exports.statSync = statSync
// exports.statfsSync = statfsSync TODO
exports.symlinkSync = symlinkSync
// exports.truncateSync = truncateSync TODO
exports.unlinkSync = unlinkSync
exports.utimesSync = utimesSync
exports.writeFileSync = writeFileSync
exports.writeSync = writeSync
exports.writevSync = writevSync

exports.promises = require('./promises')

exports.Stats = Stats
exports.Dir = Dir
exports.Dirent = Dirent
exports.Watcher = Watcher

exports.ReadStream = FileReadStream

exports.createReadStream = function createReadStream(path, opts) {
  return new FileReadStream(path, opts)
}

exports.WriteStream = FileWriteStream

exports.createWriteStream = function createWriteStream(path, opts) {
  return new FileWriteStream(path, opts)
}

function toNamespacedPath(filepath) {
  if (typeof filepath !== 'string') {
    if (URL.isURL(filepath)) filepath = fileURLToPath(filepath)
    else filepath = filepath.toString()
  }

  return path.toNamespacedPath(filepath)
}

function toFlags(flags) {
  switch (flags) {
    case 'r':
      return constants.O_RDONLY
    case 'rs': // Fall through.
    case 'sr':
      return constants.O_RDONLY | constants.O_SYNC
    case 'r+':
      return constants.O_RDWR
    case 'rs+': // Fall through.
    case 'sr+':
      return constants.O_RDWR | constants.O_SYNC

    case 'w':
      return constants.O_TRUNC | constants.O_CREAT | constants.O_WRONLY
    case 'wx': // Fall through.
    case 'xw':
      return (
        constants.O_TRUNC |
        constants.O_CREAT |
        constants.O_WRONLY |
        constants.O_EXCL
      )

    case 'w+':
      return constants.O_TRUNC | constants.O_CREAT | constants.O_RDWR
    case 'wx+': // Fall through.
    case 'xw+':
      return (
        constants.O_TRUNC |
        constants.O_CREAT |
        constants.O_RDWR |
        constants.O_EXCL
      )

    case 'a':
      return constants.O_APPEND | constants.O_CREAT | constants.O_WRONLY
    case 'ax': // Fall through.
    case 'xa':
      return (
        constants.O_APPEND |
        constants.O_CREAT |
        constants.O_WRONLY |
        constants.O_EXCL
      )
    case 'as': // Fall through.
    case 'sa':
      return (
        constants.O_APPEND |
        constants.O_CREAT |
        constants.O_WRONLY |
        constants.O_SYNC
      )

    case 'a+':
      return constants.O_APPEND | constants.O_CREAT | constants.O_RDWR
    case 'ax+': // Fall through.
    case 'xa+':
      return (
        constants.O_APPEND |
        constants.O_CREAT |
        constants.O_RDWR |
        constants.O_EXCL
      )
    case 'as+': // Fall through.
    case 'sa+':
      return (
        constants.O_APPEND |
        constants.O_CREAT |
        constants.O_RDWR |
        constants.O_SYNC
      )
    default:
      return 0
  }
}

function toMode(mode) {
  return parseInt(mode, 8)
}
