BitStringBuilder.java

/*
 * Copyright (c) 2025, Stein Eldar Johnsen
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.collect.util;

import java.util.Arrays;

import static java.lang.Byte.toUnsignedInt;
import static java.util.Objects.requireNonNull;

/**
 * A Builder of Bit Strings. The Bit String is a sequence of single bit
 * values represented by a boolean array.
 */
public class BitStringBuilder extends BitSequence {
    private boolean[] data;
    private int       size;

    public BitStringBuilder() {
        super();
        data = new boolean[16];
        size = 0;
    }

    public BitStringBuilder(BitSequence bitString) {
        super();
        requireNonNull(bitString, "bitString == null");
        size = bitString.size();
        data = new boolean[Math.max(16, size)];
        bitString.writeTo(data, 0);
    }

    public BitStringBuilder(boolean[] bitArray) {
        this(bitArray, 0, bitArray.length);
    }

    public BitStringBuilder(boolean[] bitArray, int offset, int bitCount) {
        requireNonNull(bitArray, "bitArray == null");
        if (offset < 0 || offset > (bitArray.length - 1)) {
            throw new IllegalArgumentException("offset outside [0.." + (bitArray.length - 1) + "], was " + offset);
        } else if (bitCount < 0 || bitArray.length < offset + bitCount) {
            throw new IllegalArgumentException(
                    "bitCount outside [0.." +
                    (bitArray.length - offset) +
                    "], was " +
                    bitCount);
        }
        this.data = new boolean[Math.max(16, bitCount)];
        this.size = bitCount;
        System.arraycopy(bitArray, offset, data, 0, bitCount);
    }

    // ---- BitStringBuilder ----

    public BitStringBuilder set(int pos, boolean value) {
        if (pos < 0 || size < pos) {
            throw new IndexOutOfBoundsException(pos + " outside [0.." + size + "]");
        }
        if (pos == size) {
            ensureCapacity(size + 1);
            ++size;
        }
        data[pos] = value;
        return this;
    }

    public BitStringBuilder insert(int pos, boolean value) {
        if (pos < 0 || size < pos) {
            throw new IndexOutOfBoundsException(pos + " outside [0.." + size + "]");
        }
        ensureCapacity(size + 1);
        if (pos < size) {
            var cl = size - pos;
            System.arraycopy(data, pos, data, pos + 1, cl);
        }
        data[pos] = value;
        ++size;
        return this;
    }

    public BitStringBuilder insert(int pos, BitSequence bits) {
        return insert(pos, bits.toBitArray());
    }

    public BitStringBuilder insert(int pos, byte value, int bitCount) {
        if (bitCount < 0 || bitCount > 8) {
            throw new IllegalArgumentException("Bit count must be between [0..8], was " + bitCount);
        }
        if (pos == size) {
            return append(value, bitCount);
        }
        ensureCapacity(size + bitCount);
        System.arraycopy(data, pos, data, pos + bitCount, size - pos);
        writeValue(toUnsignedInt(value), bitCount, pos, data);
        size += bitCount;
        return this;
    }

    public BitStringBuilder insert(int pos, int value, int bitCount) {
        if (bitCount < 0 || bitCount > 32) {
            throw new IllegalArgumentException("Bit count must be between [0..32], was " + bitCount);
        }
        if (pos == size) {
            return append(value, bitCount);
        }
        ensureCapacity(size + bitCount);
        System.arraycopy(data, pos, data, pos + bitCount, size - pos);
        writeValue(value, bitCount, pos, data);
        size += bitCount;
        return this;
    }

    public BitStringBuilder insert(int pos, boolean... bitArray) {
        return insert(pos, bitArray, bitArray.length);
    }

    public BitStringBuilder insert(int pos, boolean[] bitArray, int bitCount) {
        if (pos < 0 || size < pos) {
            throw new IndexOutOfBoundsException(pos + " outside [0.." + size + "]");
        } else if (bitCount < 0 || bitArray.length < bitCount) {
            throw new IllegalArgumentException(
                    "Invalid bitCount: " + bitCount + " outside [0.." + bitArray.length + "]");
        }
        if (bitCount == 0) {
            return this;
        }
        ensureCapacity(size + bitCount);
        if (pos < size) {
            var moveLen = size - pos;
            System.arraycopy(data, pos, data, pos + bitCount, moveLen);
        }
        System.arraycopy(bitArray, 0, data, pos, bitCount);
        size += bitCount;
        return this;
    }

    public BitStringBuilder append(boolean value) {
        ensureCapacity(size + 1);
        data[size] = value;
        ++size;
        return this;
    }

    public BitStringBuilder append(BitSequence bitString) {
        ensureCapacity(size + bitString.size());
        bitString.writeTo(data, size);
        size += bitString.size();
        return this;
    }

    public BitStringBuilder append(byte value, int bitCount) {
        if (bitCount < 1 || bitCount > 8) {
            throw new IllegalArgumentException("Bit count must be between [0..8], was " + bitCount);
        }
        ensureCapacity(size + bitCount);
        writeValue(value, bitCount, size, data);
        size += bitCount;
        return this;
    }

    public BitStringBuilder append(int value, int bitCount) {
        if (bitCount < 1 || bitCount > 32) {
            throw new IllegalArgumentException("Bit count must be between [0..32], was " + bitCount);
        }
        ensureCapacity(size + bitCount);
        writeValue(value, bitCount, size, data);
        size += bitCount;
        return this;
    }

    public BitStringBuilder append(boolean... bitArray) {
        return append(bitArray, bitArray.length);
    }

    public BitStringBuilder append(boolean[] bitArray, int bitCount) {
        if (bitCount < 0 || bitArray.length < bitCount) {
            throw new IllegalArgumentException(
                    "Invalid bitCount: " + bitCount + " outside [0.." + bitArray.length + "]");
        } else if (bitCount == 0) {
            return this;
        }
        ensureCapacity(size + bitCount);
        System.arraycopy(bitArray, 0, data, size, bitCount);
        size += bitCount;
        return this;
    }

    public BitString build() {
        if (size == 0) return BitString.EMPTY;
        return new BitString(data, 0, size);
    }

    // ---- BitSequence ----


    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean bitAt(int pos) {
        if (pos < 0 || size <= pos) {
            throw new IndexOutOfBoundsException(pos + " outside [0.." + size + "]");
        }
        return data[pos];
    }

    @Override
    public boolean[] toBitArray() {
        var out = new boolean[size];
        System.arraycopy(data, 0, out, 0, size);
        return out;
    }

    @Override
    public byte[] toByteArray() {
        return toByteArrayInternal(data, size);
    }

    @Override
    public String toBitString() {
        return toStringFromBits(data, size);
    }

    @Override
    protected void writeTo(boolean[] out, int off) {
        System.arraycopy(data, 0, out, off, size);
    }

    // ---- Private ----

    private static void writeValue(int value, int bitCount, int offset, boolean[] buf) {
        var maskStart = 1 << (bitCount - 1);
        for (int i = 0; i < bitCount; i++) {
            var mask = maskStart >>> i;
            buf[offset + i] = (value & mask) != 0;
        }
    }

    private void ensureCapacity(int minSize) {
        if (minSize < data.length) {
            return;
        }
        var newSize = Math.max(minSize, data.length * 2);
        data = Arrays.copyOf(data, newSize);
    }
}