BitBuffer.java

/*
 * Copyright (c) 2025 Tanagra Space
 * SPDX-License-Identifier: MIT
 */

package space.tanagra.ccsds124;

import java.util.Arrays;

/**
 * Variable-length bit buffer for building compressed output.
 *
 * <p>This class accumulates bits during compression, using a 64-bit accumulator for efficient
 * packing. Bits are written in big-endian order (MSB first).
 */
public final class BitBuffer {

  /** Maximum output size in bytes (must accommodate large streams). */
  private static final int MAX_OUTPUT_BYTES = Ccsds124.MAX_PACKET_BYTES * 100;

  /** Output buffer. */
  private byte[] data;

  /** Number of bits written. */
  private int numBits;

  /** 64-bit accumulator for bit packing. */
  private long acc;

  /** Number of bits in the accumulator. */
  private int accBits;

  /** Creates a new empty BitBuffer. */
  public BitBuffer() {
    this.data = new byte[1024]; // Initial capacity
    this.numBits = 0;
    this.acc = 0;
    this.accBits = 0;
  }

  /** Clears the buffer, resetting to empty state. */
  public void clear() {
    numBits = 0;
    acc = 0;
    accBits = 0;
    bytesWritten = 0;
  }

  /**
   * Appends a single bit to the buffer.
   *
   * @param bit the bit value (0 or 1)
   */
  public void appendBit(int bit) {
    acc = (acc << 1) | (bit & 1);
    accBits++;
    numBits++;
    // Flush complete bytes as they accumulate
    if (accBits >= 8) {
      flushAccumulator();
    }
  }

  /**
   * Appends multiple bits from an integer value.
   *
   * @param value the value containing bits (MSB aligned)
   * @param count number of bits to append (1-32)
   */
  public void appendBits(int value, int count) {
    if (count <= 0 || count > 32) {
      return;
    }
    // Mask to the requested number of bits
    long masked = value & ((1L << count) - 1);
    acc = (acc << count) | masked;
    accBits += count;
    numBits += count;
    // Flush complete bytes
    flushAccumulator();
  }

  /**
   * Appends bits from a BitVector.
   *
   * <p>Bits are appended in MSB-first order (bit 0 first, then bit 1, etc.) matching CCSDS bit
   * numbering where bit 0 is the first transmitted bit.
   *
   * @param bv the BitVector to append from
   * @param count number of bits to append
   */
  public void appendBitVector(BitVector bv, int count) {
    // CCSDS MSB-first: append bits in order from bit 0 to bit count-1
    for (int i = 0; i < count; i++) {
      appendBit(bv.getBit(i));
    }
  }

  /**
   * Appends all bits from another BitBuffer.
   *
   * @param other the BitBuffer to append
   */
  public void appendBitBuffer(BitBuffer other) {
    // Flush the other buffer first
    byte[] otherBytes = other.toBytes();
    int otherBits = other.numBits;

    // Append bit by bit
    for (int i = 0; i < otherBits; i++) {
      int byteIndex = i / 8;
      int bitOffset = 7 - (i % 8);
      int bit = (otherBytes[byteIndex] >>> bitOffset) & 1;
      appendBit(bit);
    }
  }

  /** Byte offset tracking - number of complete bytes written to data array. */
  private int bytesWritten;

  /** Flushes complete bytes from the accumulator to the output buffer. */
  private void flushAccumulator() {
    while (accBits >= 8) {
      accBits -= 8;
      int byteValue = (int) ((acc >>> accBits) & 0xFF);
      ensureCapacity(bytesWritten + 1);
      data[bytesWritten] = (byte) byteValue;
      bytesWritten++;
    }
  }

  /**
   * Ensures the data buffer has capacity for the specified number of bytes.
   *
   * @param requiredBytes number of bytes needed
   */
  private void ensureCapacity(int requiredBytes) {
    if (requiredBytes > data.length) {
      int newSize = Math.min(MAX_OUTPUT_BYTES, Math.max(data.length * 2, requiredBytes));
      data = Arrays.copyOf(data, newSize);
    }
  }

  /**
   * Returns the number of bits in the buffer.
   *
   * @return number of bits
   */
  public int length() {
    return numBits;
  }

  /**
   * Converts the buffer to a byte array.
   *
   * @return byte array containing all bits (padded with zeros if necessary)
   */
  public byte[] toBytes() {
    int totalBytes = (numBits + 7) / 8;
    byte[] result = new byte[totalBytes];

    // Copy already-flushed bytes
    System.arraycopy(data, 0, result, 0, bytesWritten);

    // Handle remaining bits in accumulator (less than 8 bits)
    if (accBits > 0 && bytesWritten < totalBytes) {
      int lastByte = (int) ((acc << (8 - accBits)) & 0xFF);
      result[bytesWritten] = (byte) lastByte;
    }

    return result;
  }
}