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

// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials

import { core } from "ext:core/mod.js";
import {
  op_brotli_compress,
  op_brotli_compress_async,
  op_brotli_compress_stream,
  op_brotli_compress_stream_end,
  op_brotli_decompress,
  op_brotli_decompress_async,
  op_brotli_decompress_stream,
  op_brotli_decompress_stream_end,
  op_create_brotli_compress,
  op_create_brotli_decompress,
} from "ext:core/ops";

import { zlib as constants } from "ext:deno_node/internal_binding/constants.ts";
import { TextEncoder } from "ext:deno_web/08_text_encoding.js";
import { Transform } from "node:stream";
import { Buffer } from "node:buffer";

const enc = new TextEncoder();
const toU8 = (input) => {
  if (typeof input === "string") {
    return enc.encode(input);
  }

  if (input.buffer) {
    return new Uint8Array(input.buffer);
  }

  return input;
};

export function createBrotliCompress(options) {
  return new BrotliCompress(options);
}

export function createBrotliDecompress(options) {
  return new BrotliDecompress(options);
}

export class BrotliDecompress extends Transform {
  #context;

  // TODO(littledivy): use `options` argument
  constructor(_options = {}) {
    super({
      // TODO(littledivy): use `encoding` argument
      transform(chunk, _encoding, callback) {
        const input = toU8(chunk);
        const output = new Uint8Array(chunk.byteLength);
        const avail = op_brotli_decompress_stream(context, input, output);
        this.push(output.slice(0, avail));
        callback();
      },
      flush(callback) {
        const output = new Uint8Array(1024);
        let avail;
        while ((avail = op_brotli_decompress_stream_end(context, output)) > 0) {
          this.push(output.slice(0, avail));
        }
        core.close(context);
        callback();
      },
    });

    this.#context = op_create_brotli_decompress();
    const context = this.#context;
  }
}

export class BrotliCompress extends Transform {
  #context;

  constructor(options = {}) {
    super({
      // TODO(littledivy): use `encoding` argument
      transform(chunk, _encoding, callback) {
        const input = toU8(chunk);
        const output = new Uint8Array(brotliMaxCompressedSize(input.length));
        const written = op_brotli_compress_stream(context, input, output);
        if (written > 0) {
          this.push(output.slice(0, written));
        }
        callback();
      },
      flush(callback) {
        const output = new Uint8Array(1024);
        let avail;
        while ((avail = op_brotli_compress_stream_end(context, output)) > 0) {
          this.push(output.slice(0, avail));
        }
        core.close(context);
        callback();
      },
    });

    const params = Object.values(options?.params ?? {});
    this.#context = op_create_brotli_compress(params);
    const context = this.#context;
  }
}

function oneOffCompressOptions(options) {
  const quality = options?.params?.[constants.BROTLI_PARAM_QUALITY] ??
    constants.BROTLI_DEFAULT_QUALITY;
  const lgwin = options?.params?.[constants.BROTLI_PARAM_LGWIN] ??
    constants.BROTLI_DEFAULT_WINDOW;
  const mode = options?.params?.[constants.BROTLI_PARAM_MODE] ??
    constants.BROTLI_MODE_GENERIC;

  return {
    quality,
    lgwin,
    mode,
  };
}

function brotliMaxCompressedSize(input) {
  if (input == 0) return 2;

  // [window bits / empty metadata] + N * [uncompressed] + [last empty]
  const numLargeBlocks = input >> 24;
  const overhead = 2 + (4 * numLargeBlocks) + 3 + 1;
  const result = input + overhead;

  return result < input ? 0 : result;
}

export function brotliCompress(
  input,
  options,
  callback,
) {
  const buf = toU8(input);

  if (typeof options === "function") {
    callback = options;
    options = {};
  }

  const { quality, lgwin, mode } = oneOffCompressOptions(options);
  op_brotli_compress_async(buf, quality, lgwin, mode)
    .then((result) => callback(null, Buffer.from(result)))
    .catch((err) => callback(err));
}

export function brotliCompressSync(
  input,
  options,
) {
  const buf = toU8(input);
  const output = new Uint8Array(brotliMaxCompressedSize(buf.length));

  const { quality, lgwin, mode } = oneOffCompressOptions(options);
  const len = op_brotli_compress(buf, output, quality, lgwin, mode);
  return Buffer.from(output.subarray(0, len));
}

export function brotliDecompress(input) {
  const buf = toU8(input);
  return op_brotli_decompress_async(buf)
    .then((result) => callback(null, Buffer.from(result)))
    .catch((err) => callback(err));
}

export function brotliDecompressSync(input) {
  return Buffer.from(op_brotli_decompress(toU8(input)));
}