BitReader.java

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

package space.tanagra.ccsds124;

/**
 * Sequential bit reader for decompression.
 *
 * <p>This class reads bits from a byte array in big-endian order (MSB first), tracking the current
 * position for sequential parsing.
 */
public final class BitReader {

  /** Source data. */
  private final byte[] data;

  /** Total number of available bits. */
  private final int numBits;

  /** Current bit position. */
  private int position;

  /**
   * Creates a new BitReader.
   *
   * @param data source byte array
   * @param numBits total number of bits to read
   */
  public BitReader(byte[] data, int numBits) {
    this.data = data;
    this.numBits = Math.min(numBits, data.length * 8);
    this.position = 0;
  }

  /**
   * Reads a single bit.
   *
   * @return the bit value (0 or 1)
   * @throws Ccsds124Exception if no bits remaining
   */
  public int readBit() throws Ccsds124Exception {
    if (position >= numBits) {
      throw new Ccsds124Exception("Bit buffer underflow");
    }
    int byteIndex = position / 8;
    int bitOffset = 7 - (position % 8);
    position++;
    return (data[byteIndex] >>> bitOffset) & 1;
  }

  /**
   * Reads multiple bits and returns them as an integer.
   *
   * <p>Optimized to read aligned bytes when possible, falling back to bit-by-bit for unaligned
   * reads.
   *
   * @param count number of bits to read (1-32)
   * @return the bits as an integer
   * @throws Ccsds124Exception if not enough bits remaining
   */
  public int readBits(int count) throws Ccsds124Exception {
    if (count <= 0 || count > 32) {
      throw new Ccsds124Exception("Invalid bit count: " + count);
    }
    if (position + count > numBits) {
      throw new Ccsds124Exception(
          "Bit buffer underflow: need " + count + " bits, have " + remaining());
    }

    int value = 0;
    int remaining = count;

    // Handle leading unaligned bits
    int bitOffset = position & 7;
    if (bitOffset != 0) {
      int bitsInCurrentByte = 8 - bitOffset;
      int bitsToRead = Math.min(bitsInCurrentByte, remaining);
      int byteIndex = position >>> 3;
      int mask = (1 << bitsToRead) - 1;
      int shift = bitsInCurrentByte - bitsToRead;
      value = (data[byteIndex] >>> shift) & mask;
      position += bitsToRead;
      remaining -= bitsToRead;
    }

    // Read full bytes
    while (remaining >= 8) {
      int byteIndex = position >>> 3;
      value = (value << 8) | (data[byteIndex] & 0xFF);
      position += 8;
      remaining -= 8;
    }

    // Handle trailing bits
    if (remaining > 0) {
      int byteIndex = position >>> 3;
      int shift = 8 - remaining;
      value = (value << remaining) | ((data[byteIndex] >>> shift) & ((1 << remaining) - 1));
      position += remaining;
    }

    return value;
  }

  /**
   * Peeks at the next bit without advancing position.
   *
   * @return the bit value (0 or 1), or -1 if no bits remaining
   */
  public int peekBit() {
    if (position >= numBits) {
      return -1;
    }
    int byteIndex = position / 8;
    int bitOffset = 7 - (position % 8);
    return (data[byteIndex] >>> bitOffset) & 1;
  }

  /**
   * Returns the number of bits remaining.
   *
   * @return remaining bits
   */
  public int remaining() {
    return numBits - position;
  }

  /**
   * Returns the current bit position.
   *
   * @return current position
   */
  public int position() {
    return position;
  }

  /**
   * Aligns position to the next byte boundary.
   *
   * @return number of bits skipped
   */
  public int alignByte() {
    int skipped = 0;
    while (position % 8 != 0 && position < numBits) {
      position++;
      skipped++;
    }
    return skipped;
  }
}