// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { Transform } from 'node:stream';
import type { Duplex, Readable } from 'node:stream';

import { logPadSize } from './logPadSize.std.js';

const PADDING_CHUNK_SIZE = 64 * 1024;

/**
 * Creates iterator that yields zero-filled padding chunks.
 */
function* generatePadding(size: number) {
  const targetLength = logPadSize(size);
  const paddingSize = targetLength - size;
  const paddingChunks = Math.floor(paddingSize / PADDING_CHUNK_SIZE);
  const paddingChunk = new Uint8Array(PADDING_CHUNK_SIZE); // zero-filled
  for (let i = 0; i < paddingChunks; i += 1) {
    yield paddingChunk;
  }

  const paddingRemainder = new Uint8Array(paddingSize % PADDING_CHUNK_SIZE);
  if (paddingRemainder.byteLength > 0) {
    yield paddingRemainder;
  }
}

// Push as much padding as we can. If we reach the end
// of the padding, return true.
function pushPadding(
  paddingIterator: Iterator<Uint8Array<ArrayBuffer>>,
  readable: Readable
): boolean {
  // eslint-disable-next-line no-constant-condition
  while (true) {
    const result = paddingIterator.next();
    if (result.done) {
      break;
    }
    const keepGoing = readable.push(result.value);
    if (!keepGoing) {
      return false;
    }
  }
  return true;
}

/**
 * Appends zero-padding to the stream to a target bucket size.
 */
export function appendPaddingStream(): Duplex {
  let onReadableDrained: undefined | (() => void);
  let fileSize = 0;

  return new Transform({
    read(size) {
      // When in the process of pushing padding, we pause and wait for
      // read to be called again.
      if (onReadableDrained != null) {
        onReadableDrained();
      }
      // Always call _read, even if we're done.
      Transform.prototype._read.call(this, size);
    },
    transform(chunk, _encoding, callback) {
      fileSize += chunk.byteLength;
      callback(null, chunk);
    },
    flush(callback) {
      const iterator = generatePadding(fileSize);

      onReadableDrained = () => {
        if (!pushPadding(iterator, this)) {
          return;
        }

        callback();
      };
      onReadableDrained();
    },
  });
}
