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

// Interfaces 100% copied from Go.
// Documentation liberally lifted from them too.
// Thank you! We love Go! <3

const core = globalThis.Deno.core;
const ops = core.ops;
const primordials = globalThis.__bootstrap.primordials;
import {
  readableStreamForRid,
  writableStreamForRid,
} from "ext:deno_web/06_streams.js";
const {
  Uint8Array,
  ArrayPrototypePush,
  MathMin,
  TypedArrayPrototypeSubarray,
  TypedArrayPrototypeSet,
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeGetByteLength,
} = primordials;

const DEFAULT_BUFFER_SIZE = 32 * 1024;
// Seek whence values.
// https://golang.org/pkg/io/#pkg-constants
const SeekMode = {
  0: "Start",
  1: "Current",
  2: "End",

  Start: 0,
  Current: 1,
  End: 2,
};

async function copy(
  src,
  dst,
  options,
) {
  let n = 0;
  const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
  const b = new Uint8Array(bufSize);
  let gotEOF = false;
  while (gotEOF === false) {
    const result = await src.read(b);
    if (result === null) {
      gotEOF = true;
    } else {
      let nwritten = 0;
      while (nwritten < result) {
        nwritten += await dst.write(
          TypedArrayPrototypeSubarray(b, nwritten, result),
        );
      }
      n += nwritten;
    }
  }
  return n;
}

async function* iter(
  r,
  options,
) {
  const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
  const b = new Uint8Array(bufSize);
  while (true) {
    const result = await r.read(b);
    if (result === null) {
      break;
    }

    yield TypedArrayPrototypeSubarray(b, 0, result);
  }
}

function* iterSync(
  r,
  options,
) {
  const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
  const b = new Uint8Array(bufSize);
  while (true) {
    const result = r.readSync(b);
    if (result === null) {
      break;
    }

    yield TypedArrayPrototypeSubarray(b, 0, result);
  }
}

function readSync(rid, buffer) {
  if (buffer.length === 0) return 0;
  const nread = core.readSync(rid, buffer);
  return nread === 0 ? null : nread;
}

async function read(rid, buffer) {
  if (buffer.length === 0) return 0;
  const nread = await core.read(rid, buffer);
  return nread === 0 ? null : nread;
}

function writeSync(rid, data) {
  return core.writeSync(rid, data);
}

function write(rid, data) {
  return core.write(rid, data);
}

const READ_PER_ITER = 64 * 1024; // 64kb

function readAll(r) {
  return readAllInner(r);
}
async function readAllInner(r, options) {
  const buffers = [];
  const signal = options?.signal ?? null;
  while (true) {
    signal?.throwIfAborted();
    const buf = new Uint8Array(READ_PER_ITER);
    const read = await r.read(buf);
    if (typeof read == "number") {
      ArrayPrototypePush(
        buffers,
        new Uint8Array(TypedArrayPrototypeGetBuffer(buf), 0, read),
      );
    } else {
      break;
    }
  }
  signal?.throwIfAborted();

  return concatBuffers(buffers);
}

function readAllSync(r) {
  const buffers = [];

  while (true) {
    const buf = new Uint8Array(READ_PER_ITER);
    const read = r.readSync(buf);
    if (typeof read == "number") {
      ArrayPrototypePush(buffers, TypedArrayPrototypeSubarray(buf, 0, read));
    } else {
      break;
    }
  }

  return concatBuffers(buffers);
}

function concatBuffers(buffers) {
  let totalLen = 0;
  for (let i = 0; i < buffers.length; ++i) {
    totalLen += TypedArrayPrototypeGetByteLength(buffers[i]);
  }

  const contents = new Uint8Array(totalLen);

  let n = 0;
  for (let i = 0; i < buffers.length; ++i) {
    const buf = buffers[i];
    TypedArrayPrototypeSet(contents, buf, n);
    n += TypedArrayPrototypeGetByteLength(buf);
  }

  return contents;
}

function readAllSyncSized(r, size) {
  const buf = new Uint8Array(size + 1); // 1B to detect extended files
  let cursor = 0;

  while (cursor < size) {
    const sliceEnd = MathMin(size + 1, cursor + READ_PER_ITER);
    const slice = TypedArrayPrototypeSubarray(buf, cursor, sliceEnd);
    const read = r.readSync(slice);
    if (typeof read == "number") {
      cursor += read;
    } else {
      break;
    }
  }

  // Handle truncated or extended files during read
  if (cursor > size) {
    // Read remaining and concat
    return concatBuffers([buf, readAllSync(r)]);
  } else { // cursor == size
    return TypedArrayPrototypeSubarray(buf, 0, cursor);
  }
}

async function readAllInnerSized(r, size, options) {
  const buf = new Uint8Array(size + 1); // 1B to detect extended files
  let cursor = 0;
  const signal = options?.signal ?? null;
  while (cursor < size) {
    signal?.throwIfAborted();
    const sliceEnd = MathMin(size + 1, cursor + READ_PER_ITER);
    const slice = TypedArrayPrototypeSubarray(buf, cursor, sliceEnd);
    const read = await r.read(slice);
    if (typeof read == "number") {
      cursor += read;
    } else {
      break;
    }
  }
  signal?.throwIfAborted();

  // Handle truncated or extended files during read
  if (cursor > size) {
    // Read remaining and concat
    return concatBuffers([buf, await readAllInner(r, options)]);
  } else {
    return TypedArrayPrototypeSubarray(buf, 0, cursor);
  }
}

class Stdin {
  #readable;

  constructor() {
  }

  get rid() {
    return 0;
  }

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

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

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

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

  setRaw(mode, options = {}) {
    const cbreak = !!(options.cbreak ?? false);
    ops.op_stdin_set_raw(mode, cbreak);
  }
}

class Stdout {
  #writable;

  constructor() {
  }

  get rid() {
    return 1;
  }

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

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

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

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

class Stderr {
  #writable;

  constructor() {
  }

  get rid() {
    return 2;
  }

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

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

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

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

const stdin = new Stdin();
const stdout = new Stdout();
const stderr = new Stderr();

export {
  copy,
  iter,
  iterSync,
  read,
  readAll,
  readAllInner,
  readAllInnerSized,
  readAllSync,
  readAllSyncSized,
  readSync,
  SeekMode,
  stderr,
  stdin,
  stdout,
  write,
  writeSync,
};