// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

const core = globalThis.Deno.core;
const ops = core.ops;
const primordials = globalThis.__bootstrap.primordials;
const {
  ArrayPrototypeFilter,
  Date,
  DatePrototype,
  Error,
  Function,
  MathTrunc,
  ObjectEntries,
  ObjectPrototypeIsPrototypeOf,
  ObjectValues,
  SymbolAsyncIterator,
  SymbolIterator,
  Uint32Array,
} = primordials;
import { read, readSync, write, writeSync } from "ext:deno_io/12_io.js";
import * as abortSignal from "ext:deno_web/03_abort_signal.js";
import {
  readableStreamForRid,
  ReadableStreamPrototype,
  writableStreamForRid,
} from "ext:deno_web/06_streams.js";
import { pathFromURL } from "ext:deno_web/00_infra.js";

function chmodSync(path, mode) {
  ops.op_chmod_sync(pathFromURL(path), mode);
}

async function chmod(path, mode) {
  await core.opAsync2("op_chmod_async", pathFromURL(path), mode);
}

function chownSync(
  path,
  uid,
  gid,
) {
  ops.op_chown_sync(pathFromURL(path), uid, gid);
}

async function chown(
  path,
  uid,
  gid,
) {
  await core.opAsync(
    "op_chown_async",
    pathFromURL(path),
    uid,
    gid,
  );
}

function copyFileSync(
  fromPath,
  toPath,
) {
  ops.op_copy_file_sync(
    pathFromURL(fromPath),
    pathFromURL(toPath),
  );
}

async function copyFile(
  fromPath,
  toPath,
) {
  await core.opAsync(
    "op_copy_file_async",
    pathFromURL(fromPath),
    pathFromURL(toPath),
  );
}

function cwd() {
  return ops.op_cwd();
}

function chdir(directory) {
  ops.op_chdir(pathFromURL(directory));
}

function makeTempDirSync(options = {}) {
  return ops.op_make_temp_dir_sync(options);
}

function makeTempDir(options = {}) {
  return core.opAsync("op_make_temp_dir_async", options);
}

function makeTempFileSync(options = {}) {
  return ops.op_make_temp_file_sync(options);
}

function makeTempFile(options = {}) {
  return core.opAsync("op_make_temp_file_async", options);
}

function mkdirArgs(path, options) {
  const args = { path: pathFromURL(path), recursive: false };
  if (options != null) {
    if (typeof options.recursive == "boolean") {
      args.recursive = options.recursive;
    }
    if (options.mode) {
      args.mode = options.mode;
    }
  }
  return args;
}

function mkdirSync(path, options) {
  ops.op_mkdir_sync(mkdirArgs(path, options));
}

async function mkdir(
  path,
  options,
) {
  await core.opAsync2("op_mkdir_async", mkdirArgs(path, options));
}

function readDirSync(path) {
  return ops.op_read_dir_sync(pathFromURL(path))[
    SymbolIterator
  ]();
}

function readDir(path) {
  const array = core.opAsync(
    "op_read_dir_async",
    pathFromURL(path),
  );
  return {
    async *[SymbolAsyncIterator]() {
      const dir = await array;
      for (let i = 0; i < dir.length; ++i) {
        yield dir[i];
      }
    },
  };
}

function readLinkSync(path) {
  return ops.op_read_link_sync(pathFromURL(path));
}

function readLink(path) {
  return core.opAsync("op_read_link_async", pathFromURL(path));
}

function realPathSync(path) {
  return ops.op_realpath_sync(pathFromURL(path));
}

function realPath(path) {
  return core.opAsync("op_realpath_async", pathFromURL(path));
}

function removeSync(
  path,
  options = {},
) {
  ops.op_remove_sync(
    pathFromURL(path),
    !!options.recursive,
  );
}

async function remove(
  path,
  options = {},
) {
  await core.opAsync(
    "op_remove_async",
    pathFromURL(path),
    !!options.recursive,
  );
}

function renameSync(oldpath, newpath) {
  ops.op_rename_sync(
    pathFromURL(oldpath),
    pathFromURL(newpath),
  );
}

async function rename(oldpath, newpath) {
  await core.opAsync(
    "op_rename_async",
    pathFromURL(oldpath),
    pathFromURL(newpath),
  );
}

// Extract the FsStat object from the encoded buffer.
// See `runtime/ops/fs.rs` for the encoder.
//
// This is not a general purpose decoder. There are 4 types:
//
// 1. date
//  offset += 4
//  1/0 | extra padding | high u32 | low u32
//  if date[0] == 1, new Date(u64) else null
//
// 2. bool
//  offset += 2
//  1/0 | extra padding
//
// 3. u64
//  offset += 2
//  high u32 | low u32
//
// 4. ?u64 converts a zero u64 value to JS null on Windows.
function createByteStruct(types) {
  // types can be "date", "bool" or "u64".
  let offset = 0;
  let str =
    'const unix = Deno.build.os === "darwin" || Deno.build.os === "linux"; return {';
  const typeEntries = ObjectEntries(types);
  for (let i = 0; i < typeEntries.length; ++i) {
    let { 0: name, 1: type } = typeEntries[i];

    const optional = type.startsWith("?");
    if (optional) type = type.slice(1);

    if (type == "u64") {
      if (!optional) {
        str += `${name}: view[${offset}] + view[${offset + 1}] * 2**32,`;
      } else {
        str += `${name}: (unix ? (view[${offset}] + view[${
          offset + 1
        }] * 2**32) : (view[${offset}] + view[${
          offset + 1
        }] * 2**32) || null),`;
      }
    } else if (type == "date") {
      str += `${name}: view[${offset}] === 0 ? null : new Date(view[${
        offset + 2
      }] + view[${offset + 3}] * 2**32),`;
      offset += 2;
    } else {
      str += `${name}: !!(view[${offset}] + view[${offset + 1}] * 2**32),`;
    }
    offset += 2;
  }
  str += "};";
  // ...so you don't like eval huh? don't worry, it only executes during snapshot :)
  return [new Function("view", str), new Uint32Array(offset)];
}

const { 0: statStruct, 1: statBuf } = createByteStruct({
  isFile: "bool",
  isDirectory: "bool",
  isSymlink: "bool",
  size: "u64",
  mtime: "date",
  atime: "date",
  birthtime: "date",
  dev: "u64",
  ino: "?u64",
  mode: "?u64",
  nlink: "?u64",
  uid: "?u64",
  gid: "?u64",
  rdev: "?u64",
  blksize: "?u64",
  blocks: "?u64",
});

function parseFileInfo(response) {
  const unix = core.build.os === "darwin" || core.build.os === "linux";
  return {
    isFile: response.isFile,
    isDirectory: response.isDirectory,
    isSymlink: response.isSymlink,
    size: response.size,
    mtime: response.mtimeSet !== null ? new Date(response.mtime) : null,
    atime: response.atimeSet !== null ? new Date(response.atime) : null,
    birthtime: response.birthtimeSet !== null
      ? new Date(response.birthtime)
      : null,
    dev: response.dev,
    ino: unix ? response.ino : null,
    mode: unix ? response.mode : null,
    nlink: unix ? response.nlink : null,
    uid: unix ? response.uid : null,
    gid: unix ? response.gid : null,
    rdev: unix ? response.rdev : null,
    blksize: unix ? response.blksize : null,
    blocks: unix ? response.blocks : null,
  };
}

function fstatSync(rid) {
  ops.op_fstat_sync(rid, statBuf);
  return statStruct(statBuf);
}

async function fstat(rid) {
  return parseFileInfo(await core.opAsync("op_fstat_async", rid));
}

async function lstat(path) {
  const res = await core.opAsync("op_stat_async", {
    path: pathFromURL(path),
    lstat: true,
  });
  return parseFileInfo(res);
}

function lstatSync(path) {
  ops.op_stat_sync(
    pathFromURL(path),
    true,
    statBuf,
  );
  return statStruct(statBuf);
}

async function stat(path) {
  const res = await core.opAsync("op_stat_async", {
    path: pathFromURL(path),
    lstat: false,
  });
  return parseFileInfo(res);
}

function statSync(path) {
  ops.op_stat_sync(
    pathFromURL(path),
    false,
    statBuf,
  );
  return statStruct(statBuf);
}

function coerceLen(len) {
  if (len == null || len < 0) {
    return 0;
  }

  return len;
}

function ftruncateSync(rid, len) {
  ops.op_ftruncate_sync(rid, coerceLen(len));
}

async function ftruncate(rid, len) {
  await core.opAsync2("op_ftruncate_async", rid, coerceLen(len));
}

function truncateSync(path, len) {
  ops.op_truncate_sync(path, coerceLen(len));
}

async function truncate(path, len) {
  await core.opAsync2("op_truncate_async", path, coerceLen(len));
}

function umask(mask) {
  return ops.op_umask(mask);
}

function linkSync(oldpath, newpath) {
  ops.op_link_sync(oldpath, newpath);
}

async function link(oldpath, newpath) {
  await core.opAsync2("op_link_async", oldpath, newpath);
}

function toUnixTimeFromEpoch(value) {
  if (ObjectPrototypeIsPrototypeOf(DatePrototype, value)) {
    const time = value.valueOf();
    const seconds = MathTrunc(time / 1e3);
    const nanoseconds = MathTrunc(time - (seconds * 1e3)) * 1e6;

    return [
      seconds,
      nanoseconds,
    ];
  }

  const seconds = value;
  const nanoseconds = 0;

  return [
    seconds,
    nanoseconds,
  ];
}

function futimeSync(
  rid,
  atime,
  mtime,
) {
  const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime);
  const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime);
  ops.op_futime_sync(rid, atimeSec, atimeNsec, mtimeSec, mtimeNsec);
}

async function futime(
  rid,
  atime,
  mtime,
) {
  const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime);
  const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime);
  await core.opAsync(
    "op_futime_async",
    rid,
    atimeSec,
    atimeNsec,
    mtimeSec,
    mtimeNsec,
  );
}

function utimeSync(
  path,
  atime,
  mtime,
) {
  const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime);
  const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime);
  ops.op_utime_sync(
    pathFromURL(path),
    atimeSec,
    atimeNsec,
    mtimeSec,
    mtimeNsec,
  );
}

async function utime(
  path,
  atime,
  mtime,
) {
  const { 0: atimeSec, 1: atimeNsec } = toUnixTimeFromEpoch(atime);
  const { 0: mtimeSec, 1: mtimeNsec } = toUnixTimeFromEpoch(mtime);
  await core.opAsync(
    "op_utime_async",
    pathFromURL(path),
    atimeSec,
    atimeNsec,
    mtimeSec,
    mtimeNsec,
  );
}

function symlinkSync(
  oldpath,
  newpath,
  options,
) {
  ops.op_symlink_sync(
    pathFromURL(oldpath),
    pathFromURL(newpath),
    options?.type,
  );
}

async function symlink(
  oldpath,
  newpath,
  options,
) {
  await core.opAsync(
    "op_symlink_async",
    pathFromURL(oldpath),
    pathFromURL(newpath),
    options?.type,
  );
}

function fdatasyncSync(rid) {
  ops.op_fdatasync_sync(rid);
}

async function fdatasync(rid) {
  await core.opAsync("op_fdatasync_async", rid);
}

function fsyncSync(rid) {
  ops.op_fsync_sync(rid);
}

async function fsync(rid) {
  await core.opAsync("op_fsync_async", rid);
}

function flockSync(rid, exclusive) {
  ops.op_flock_sync(rid, exclusive === true);
}

async function flock(rid, exclusive) {
  await core.opAsync2("op_flock_async", rid, exclusive === true);
}

function funlockSync(rid) {
  ops.op_funlock_sync(rid);
}

async function funlock(rid) {
  await core.opAsync("op_funlock_async", rid);
}

function seekSync(
  rid,
  offset,
  whence,
) {
  return ops.op_seek_sync({ rid, offset, whence });
}

function seek(
  rid,
  offset,
  whence,
) {
  return core.opAsync("op_seek_async", { rid, offset, whence });
}

function openSync(
  path,
  options,
) {
  if (options) checkOpenOptions(options);
  const mode = options?.mode;
  const rid = ops.op_open_sync(
    pathFromURL(path),
    options,
    mode,
  );

  return new FsFile(rid);
}

async function open(
  path,
  options,
) {
  if (options) checkOpenOptions(options);
  const mode = options?.mode;
  const rid = await core.opAsync(
    "op_open_async",
    pathFromURL(path),
    options,
    mode,
  );

  return new FsFile(rid);
}

function createSync(path) {
  return openSync(path, {
    read: true,
    write: true,
    truncate: true,
    create: true,
  });
}

function create(path) {
  return open(path, {
    read: true,
    write: true,
    truncate: true,
    create: true,
  });
}

class FsFile {
  #rid = 0;

  #readable;
  #writable;

  constructor(rid) {
    this.#rid = rid;
  }

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

  write(p) {
    return write(this.rid, p);
  }

  writeSync(p) {
    return writeSync(this.rid, p);
  }

  truncate(len) {
    return ftruncate(this.rid, len);
  }

  truncateSync(len) {
    return ftruncateSync(this.rid, len);
  }

  read(p) {
    return read(this.rid, p);
  }

  readSync(p) {
    return readSync(this.rid, p);
  }

  seek(offset, whence) {
    return seek(this.rid, offset, whence);
  }

  seekSync(offset, whence) {
    return seekSync(this.rid, offset, whence);
  }

  stat() {
    return fstat(this.rid);
  }

  statSync() {
    return fstatSync(this.rid);
  }

  close() {
    core.close(this.rid);
  }

  get readable() {
    if (this.#readable === undefined) {
      this.#readable = readableStreamForRid(this.rid);
    }
    return this.#readable;
  }

  get writable() {
    if (this.#writable === undefined) {
      this.#writable = writableStreamForRid(this.rid);
    }
    return this.#writable;
  }
}

function checkOpenOptions(options) {
  if (
    ArrayPrototypeFilter(
      ObjectValues(options),
      (val) => val === true,
    ).length === 0
  ) {
    throw new Error("OpenOptions requires at least one option to be true");
  }

  if (options.truncate && !options.write) {
    throw new Error("'truncate' option requires 'write' option");
  }

  const createOrCreateNewWithoutWriteOrAppend =
    (options.create || options.createNew) &&
    !(options.write || options.append);

  if (createOrCreateNewWithoutWriteOrAppend) {
    throw new Error(
      "'create' or 'createNew' options require 'write' or 'append' option",
    );
  }
}

const File = FsFile;

function readFileSync(path) {
  return ops.op_readfile_sync(pathFromURL(path));
}

async function readFile(path, options) {
  let cancelRid;
  let abortHandler;
  if (options?.signal) {
    options.signal.throwIfAborted();
    cancelRid = ops.op_cancel_handle();
    abortHandler = () => core.tryClose(cancelRid);
    options.signal[abortSignal.add](abortHandler);
  }

  try {
    const read = await core.opAsync(
      "op_readfile_async",
      pathFromURL(path),
      cancelRid,
    );
    return read;
  } finally {
    if (options?.signal) {
      options.signal[abortSignal.remove](abortHandler);

      // always throw the abort error when aborted
      options.signal.throwIfAborted();
    }
  }
}

function readTextFileSync(path) {
  return ops.op_readfile_text_sync(pathFromURL(path));
}

async function readTextFile(path, options) {
  let cancelRid;
  let abortHandler;
  if (options?.signal) {
    options.signal.throwIfAborted();
    cancelRid = ops.op_cancel_handle();
    abortHandler = () => core.tryClose(cancelRid);
    options.signal[abortSignal.add](abortHandler);
  }

  try {
    const read = await core.opAsync(
      "op_readfile_text_async",
      pathFromURL(path),
      cancelRid,
    );
    return read;
  } finally {
    if (options?.signal) {
      options.signal[abortSignal.remove](abortHandler);

      // always throw the abort error when aborted
      options.signal.throwIfAborted();
    }
  }
}

function writeFileSync(
  path,
  data,
  options = {},
) {
  options.signal?.throwIfAborted();
  ops.op_write_file_sync(
    pathFromURL(path),
    options.mode,
    options.append ?? false,
    options.create ?? true,
    options.createNew ?? false,
    data,
  );
}

async function writeFile(
  path,
  data,
  options = {},
) {
  let cancelRid;
  let abortHandler;
  if (options.signal) {
    options.signal.throwIfAborted();
    cancelRid = ops.op_cancel_handle();
    abortHandler = () => core.tryClose(cancelRid);
    options.signal[abortSignal.add](abortHandler);
  }
  try {
    if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) {
      const file = await open(path, {
        mode: options.mode,
        append: options.append ?? false,
        create: options.create ?? true,
        createNew: options.createNew ?? false,
        write: true,
      });
      await data.pipeTo(file.writable, {
        signal: options.signal,
      });
    } else {
      await core.opAsync(
        "op_write_file_async",
        pathFromURL(path),
        options.mode,
        options.append ?? false,
        options.create ?? true,
        options.createNew ?? false,
        data,
        cancelRid,
      );
    }
  } finally {
    if (options.signal) {
      options.signal[abortSignal.remove](abortHandler);

      // always throw the abort error when aborted
      options.signal.throwIfAborted();
    }
  }
}

function writeTextFileSync(
  path,
  data,
  options = {},
) {
  const encoder = new TextEncoder();
  return writeFileSync(path, encoder.encode(data), options);
}

function writeTextFile(
  path,
  data,
  options = {},
) {
  if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) {
    return writeFile(
      path,
      data.pipeThrough(new TextEncoderStream()),
      options,
    );
  } else {
    const encoder = new TextEncoder();
    return writeFile(path, encoder.encode(data), options);
  }
}

export {
  chdir,
  chmod,
  chmodSync,
  chown,
  chownSync,
  copyFile,
  copyFileSync,
  create,
  createSync,
  cwd,
  fdatasync,
  fdatasyncSync,
  File,
  flock,
  flockSync,
  FsFile,
  fstat,
  fstatSync,
  fsync,
  fsyncSync,
  ftruncate,
  ftruncateSync,
  funlock,
  funlockSync,
  futime,
  futimeSync,
  link,
  linkSync,
  lstat,
  lstatSync,
  makeTempDir,
  makeTempDirSync,
  makeTempFile,
  makeTempFileSync,
  mkdir,
  mkdirSync,
  open,
  openSync,
  readDir,
  readDirSync,
  readFile,
  readFileSync,
  readLink,
  readLinkSync,
  readTextFile,
  readTextFileSync,
  realPath,
  realPathSync,
  remove,
  removeSync,
  rename,
  renameSync,
  seek,
  seekSync,
  stat,
  statSync,
  symlink,
  symlinkSync,
  truncate,
  truncateSync,
  umask,
  utime,
  utimeSync,
  writeFile,
  writeFileSync,
  writeTextFile,
  writeTextFileSync,
};