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

// This code has been ported almost directly from Go's src/bytes/buffer.go
// Copyright 2009 The Go Authors. All rights reserved. BSD license.
// https://github.com/golang/go/blob/master/LICENSE

import { internals, primordials } from "ext:core/mod.js";
const {
  ArrayBufferPrototypeGetByteLength,
  TypedArrayPrototypeSubarray,
  TypedArrayPrototypeSlice,
  TypedArrayPrototypeSet,
  TypedArrayPrototypeGetBuffer,
  TypedArrayPrototypeGetByteLength,
  MathFloor,
  MathMin,
  PromiseResolve,
  Uint8Array,
  Error,
} = primordials;

import { assert } from "ext:deno_web/00_infra.js";

// MIN_READ is the minimum ArrayBuffer size passed to a read call by
// buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond
// what is required to hold the contents of r, readFrom() will not grow the
// underlying buffer.
const MIN_READ = 32 * 1024;
const MAX_SIZE = 2 ** 32 - 2;

// `off` is the offset into `dst` where it will at which to begin writing values
// from `src`.
// Returns the number of bytes copied.
function copyBytes(src, dst, off = 0) {
  const r = TypedArrayPrototypeGetByteLength(dst) - off;
  if (TypedArrayPrototypeGetByteLength(src) > r) {
    src = TypedArrayPrototypeSubarray(src, 0, r);
  }
  TypedArrayPrototypeSet(dst, src, off);
  return TypedArrayPrototypeGetByteLength(src);
}

class Buffer {
  #buf = null; // contents are the bytes buf[off : len(buf)]
  #off = 0; // read at buf[off], write at buf[buf.byteLength]

  constructor(ab) {
    internals.warnOnDeprecatedApi(
      "new Deno.Buffer()",
      new Error().stack,
      "Use `Buffer` from `https://deno.land/std/io/buffer.ts` instead.",
    );
    if (ab == null) {
      this.#buf = new Uint8Array(0);
      return;
    }

    this.#buf = new Uint8Array(ab);
  }

  bytes(options = { copy: true }) {
    if (options.copy === false) {
      return TypedArrayPrototypeSubarray(this.#buf, this.#off);
    }
    return TypedArrayPrototypeSlice(this.#buf, this.#off);
  }

  empty() {
    return TypedArrayPrototypeGetByteLength(this.#buf) <= this.#off;
  }

  get length() {
    return TypedArrayPrototypeGetByteLength(this.#buf) - this.#off;
  }

  get capacity() {
    return ArrayBufferPrototypeGetByteLength(
      TypedArrayPrototypeGetBuffer(this.#buf),
    );
  }

  truncate(n) {
    if (n === 0) {
      this.reset();
      return;
    }
    if (n < 0 || n > this.length) {
      throw Error("bytes.Buffer: truncation out of range");
    }
    this.#reslice(this.#off + n);
  }

  reset() {
    this.#reslice(0);
    this.#off = 0;
  }

  #tryGrowByReslice(n) {
    const l = TypedArrayPrototypeGetByteLength(this.#buf);
    if (n <= this.capacity - l) {
      this.#reslice(l + n);
      return l;
    }
    return -1;
  }

  #reslice(len) {
    const ab = TypedArrayPrototypeGetBuffer(this.#buf);
    assert(len <= ArrayBufferPrototypeGetByteLength(ab));
    this.#buf = new Uint8Array(ab, 0, len);
  }

  readSync(p) {
    if (this.empty()) {
      // Buffer is empty, reset to recover space.
      this.reset();
      if (TypedArrayPrototypeGetByteLength(p) === 0) {
        // this edge case is tested in 'bufferReadEmptyAtEOF' test
        return 0;
      }
      return null;
    }
    const nread = copyBytes(
      TypedArrayPrototypeSubarray(this.#buf, this.#off),
      p,
    );
    this.#off += nread;
    return nread;
  }

  read(p) {
    const rr = this.readSync(p);
    return PromiseResolve(rr);
  }

  writeSync(p) {
    const m = this.#grow(TypedArrayPrototypeGetByteLength(p));
    return copyBytes(p, this.#buf, m);
  }

  write(p) {
    const n = this.writeSync(p);
    return PromiseResolve(n);
  }

  #grow(n) {
    const m = this.length;
    // If buffer is empty, reset to recover space.
    if (m === 0 && this.#off !== 0) {
      this.reset();
    }
    // Fast: Try to grow by means of a reslice.
    const i = this.#tryGrowByReslice(n);
    if (i >= 0) {
      return i;
    }
    const c = this.capacity;
    if (n <= MathFloor(c / 2) - m) {
      // We can slide things down instead of allocating a new
      // ArrayBuffer. We only need m+n <= c to slide, but
      // we instead let capacity get twice as large so we
      // don't spend all our time copying.
      copyBytes(TypedArrayPrototypeSubarray(this.#buf, this.#off), this.#buf);
    } else if (c + n > MAX_SIZE) {
      throw new Error("The buffer cannot be grown beyond the maximum size.");
    } else {
      // Not enough space anywhere, we need to allocate.
      const buf = new Uint8Array(MathMin(2 * c + n, MAX_SIZE));
      copyBytes(TypedArrayPrototypeSubarray(this.#buf, this.#off), buf);
      this.#buf = buf;
    }
    // Restore this.#off and len(this.#buf).
    this.#off = 0;
    this.#reslice(MathMin(m + n, MAX_SIZE));
    return m;
  }

  grow(n) {
    if (n < 0) {
      throw Error("Buffer.grow: negative count");
    }
    const m = this.#grow(n);
    this.#reslice(m);
  }

  async readFrom(r) {
    let n = 0;
    const tmp = new Uint8Array(MIN_READ);
    while (true) {
      const shouldGrow = this.capacity - this.length < MIN_READ;
      // read into tmp buffer if there's not enough room
      // otherwise read directly into the internal buffer
      const buf = shouldGrow
        ? tmp
        : new Uint8Array(TypedArrayPrototypeGetBuffer(this.#buf), this.length);

      const nread = await r.read(buf);
      if (nread === null) {
        return n;
      }

      // write will grow if needed
      if (shouldGrow) {
        this.writeSync(TypedArrayPrototypeSubarray(buf, 0, nread));
      } else this.#reslice(this.length + nread);

      n += nread;
    }
  }

  readFromSync(r) {
    let n = 0;
    const tmp = new Uint8Array(MIN_READ);
    while (true) {
      const shouldGrow = this.capacity - this.length < MIN_READ;
      // read into tmp buffer if there's not enough room
      // otherwise read directly into the internal buffer
      const buf = shouldGrow
        ? tmp
        : new Uint8Array(TypedArrayPrototypeGetBuffer(this.#buf), this.length);

      const nread = r.readSync(buf);
      if (nread === null) {
        return n;
      }

      // write will grow if needed
      if (shouldGrow) {
        this.writeSync(TypedArrayPrototypeSubarray(buf, 0, nread));
      } else this.#reslice(this.length + nread);

      n += nread;
    }
  }
}

async function readAll(r) {
  internals.warnOnDeprecatedApi(
    "Deno.readAll()",
    new Error().stack,
    "Use `readAll()` from `https://deno.land/std/io/read_all.ts` instead.",
  );
  const buf = new Buffer();
  await buf.readFrom(r);
  return buf.bytes();
}

function readAllSync(r) {
  internals.warnOnDeprecatedApi(
    "Deno.readAllSync()",
    new Error().stack,
    "Use `readAllSync()` from `https://deno.land/std/io/read_all.ts` instead.",
  );
  const buf = new Buffer();
  buf.readFromSync(r);
  return buf.bytes();
}

async function writeAll(w, arr) {
  internals.warnOnDeprecatedApi(
    "Deno.writeAll()",
    new Error().stack,
    "Use `writeAll()` from `https://deno.land/std/io/write_all.ts` instead.",
  );
  let nwritten = 0;
  while (nwritten < arr.length) {
    nwritten += await w.write(TypedArrayPrototypeSubarray(arr, nwritten));
  }
}

function writeAllSync(w, arr) {
  internals.warnOnDeprecatedApi(
    "Deno.writeAllSync()",
    new Error().stack,
    "Use `writeAllSync()` from `https://deno.land/std/io/write_all.ts` instead.",
  );
  let nwritten = 0;
  while (nwritten < arr.length) {
    nwritten += w.writeSync(TypedArrayPrototypeSubarray(arr, nwritten));
  }
}

export { Buffer, readAll, readAllSync, writeAll, writeAllSync };