import { assert } from "../assert.js";
import { max3 } from "../math/max3.js";
import { min2 } from "../math/min2.js";
import { align_32 } from "./align_32.js";
import { lsb_32 } from "./lsb_32.js";
import { bitCount } from "./operations/bitCount.js";

/**
 * Used for overallocating space when bit set needs to grow
 * @constant
 * @type {number}
 */
const GROW_FACTOR = 1.3;

/**
 * Used to allow some un-assigned space to be retained when shrinking
 * @constant
 * @type {number}
 */
const DEFAULT_SHRINK_FACTOR = 0.5;

/**
 * Minimum number of bits to increase the capacity by
 * @type {number}
 */
const RESIZE_COUNT_THRESHOLD = 128;

/**
 * @readonly
 * @type {number}
 */
const DEFAULT_INITIAL_CAPACITY = 64;

/**
 * A dynamically resizable bitset (bit array) implementation.
 * The bitset automatically grows and shrinks as bits are set and cleared.
 *
 * @example
 *  const bits = new BitSet();
 *  bits.set(10);
 *
 *  bits.get(11) // false
 *  bits.get(10) // true
 *  bits.get(9)  // false
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class BitSet {

    /**
     * @constructor
     * @param {number} [initial_capacity] optional, can supply initial capacity in bits to avoid resizing.
     */
    constructor(initial_capacity = DEFAULT_INITIAL_CAPACITY) {
        assert.isNonNegativeInteger(initial_capacity, 'initial_capacity');

        /**
         * Number of bits currently in use (highest set bit + 1).
         * @private
         * @type {number}
         */
        this.__length = 0;

        /**
         * Current capacity in bits, this is at least equal to length
         * Always a multiple of 32.
         * @private
         * @type {number}
         */
        this.__capacity = align_32(initial_capacity);

        /**
         * Uint32-backed storage
         * @type {Uint32Array}
         * @private
         */
        this.__data_uint32 = new Uint32Array(this.__capacity >> 5);

        /**
         * The fraction of capacity at which to trigger a shrink operation.
         * @type {number}
         */
        this.__shrinkFactor = DEFAULT_SHRINK_FACTOR;
    }

    /**
     * Prevents the BitSet from shrinking automatically.
     * Useful for fixed-size bitsets.
     * @returns {void}
     */
    preventShrink() {
        this.setShrinkFactor(0);
    }

    /**
     *
     * @param {number} x The new shrink factor (must be between 0 and 1, inclusive of 0, exclusive of 1).
     */
    setShrinkFactor(x) {
        assert.greaterThanOrEqual(x, 0, 'x >= 0');
        assert.lessThan(x, 1, 'x < 1');

        this.__shrinkFactor = x;
    }
    /**
     * Sets the capacity of the bitset, ensuring enough space.
     * Does NOT change set bits.
     * @param {number} bit_count The new capacity, in bits.
     * @throws {Error} If the requested capacity is smaller than the current {@link size}.
     * @returns {void}
     */
    setCapacity(bit_count) {
        assert.isNonNegativeInteger(bit_count, "bit_count");

        if (this.__length > bit_count) {
            throw  new Error(`Current length(=${this.__length}) is greater than requested size(=${bit_count})`);
        }

        this.__resize(bit_count);
    }

    /**
     * Returns the number of bits currently "used" by the BitSet.
     * This is equivalent to the index of the highest set bit plus one.
     * @returns {number} The size of the BitSet (in bits).
     */
    size() {
        return this.__length;
    }

    /**
     * Returns the current capacity (allocated bits) of the BitSet.
     * This is always a multiple of 32.
     * @returns {number} The capacity of the BitSet (in bits).
     */
    capacity() {
        return this.__capacity;
    }

    /**
     * Resizes the internal Uint32Array to the specified capacity.
     * @param {number} bitCapacity New capacity in bits.
     * @private
     */
    __resize(bitCapacity) {
        assert.isNonNegativeInteger(bitCapacity, 'bitCapacity');

        const uint32_capacity = Math.ceil(bitCapacity / 32);

        const oldData = this.__data_uint32;
        const newData = new Uint32Array(uint32_capacity);

        //copy data from old container

        const oldDataSize = oldData.length;

        if (oldDataSize < uint32_capacity) {
            newData.set(oldData);
        } else {
            //creating a sub-array is using heap memory, so we prefer to avoid it
            newData.set(oldData.subarray(0, uint32_capacity));
        }

        this.__data_uint32 = newData;

        this.__capacity = uint32_capacity * 32;
    }

    /**
     * Updates the internal length of the bitset after clearing bits.
     * @private
     */
    __updateLength() {
        const found_length = this.previousSetBit(this.__length) + 1;

        if (found_length < this.__length) {
            this.__setLength(found_length);
        }
    }

    /**
     * Sets the internal length of the BitSet and manages capacity.
     * Grows or shrinks if necessary
     * @param {number} new_length The new length of the BitSet.
     * @private
     */
    __setLength(new_length) {
        this.__length = new_length;

        const capacity = this.__capacity;

        if (new_length > capacity) {

            const growSize = Math.ceil(max3(
                new_length,
                capacity + RESIZE_COUNT_THRESHOLD,
                capacity * GROW_FACTOR
            ));

            this.__resize(growSize);

        } else {
            if (
                new_length < capacity - RESIZE_COUNT_THRESHOLD
                && new_length < capacity * this.__shrinkFactor
            ) {

                this.__resize(new_length);

            }
        }
    }

    /**
     * Returns the index of the nearest bit that is set to true that occurs on or before the specified starting index
     * @param {number} from_index The index to start searching from (inclusive).
     * @returns {number} Index of previous set bit, or -1 if no set bit found
     */
    previousSetBit(from_index) {
        assert.isNonNegativeInteger(from_index, `fromIndex`);

        const index = min2(from_index, this.__length - 1);

        let word_index = index >> 5;
        let bit_index = index & 31; // modulo operation is slow, bitwise and is fast, this is the same as %8

        const data = this.__data_uint32;

        let word = data[word_index];

        //handle first byte separately due to potential partial traversal
        for (; bit_index >= 0; bit_index--) {
            if ((word & (1 << bit_index)) !== 0) {
                return word_index * 32 + bit_index;
            }
        }

        //unwind first byte
        word_index--;

        //scan the rest
        for (; word_index >= 0; word_index--) {

            word = data[word_index];

            for (bit_index = 31; bit_index >= 0; bit_index--) {

                if ((word & (1 << bit_index)) !== 0) {
                    return word_index * 32 + bit_index;
                }

            }
        }

        return -1;
    }

    /**
     * Returns the index of the first bit that is set to false that occurs on or after the specified starting index.
     * @param {number} from_index The index to start searching from (inclusive).
     * @returns {number} index of the next set bit, or -1 if no bits are set beyond supplied index
     */
    nextSetBit(from_index) {
        // assert.ok(fromIndex >= 0, `fromIndex must be greater or equal to 0, instead was ${fromIndex}`);
        // assert.ok(Number.isInteger(fromIndex), `fromIndex must be an integer, instead was ${fromIndex}`);

        const bit_length = this.__length;
        if (from_index >= bit_length) {
            //index is out of bounds, return -1
            return -1;
        }

        const data = this.__data_uint32;

        let word_index = from_index >> 5;
        let word;
        let bit_address;

        let bit_index = from_index & 31;

        if (bit_index !== 0) {
            // bit offset boundary in inside the word, we need to mask part of the word

            const fill_mask = ~((1 << bit_index) - 1);

            const masked_word = data[word_index] & fill_mask;

            if (masked_word !== 0) {
                bit_index = lsb_32(masked_word);
                return (word_index << 5) + bit_index;
            }

            word_index++;
        }


        //scan the rest of the words
        const word_count = (bit_length + 31) >> 5; // Math.ceil(x /32)
        for (; word_index < word_count; word_index++) {
            word = data[word_index];

            if (word === 0) {
                // all 0s
                continue;
            }

            bit_index = lsb_32(word);

            bit_address = (word_index << 5) + bit_index;

            return bit_address;
        }


        return -1;
    }

    /**
     * Returns the index of the first bit that is set to false that occurs on or after the specified starting index.
     * @param {number} from_index The index to start searching from (inclusive).
     * @returns {number} Index of the next clear bit (bit set to false), or the current length of BitSet if all the bits are set.
     */
    nextClearBit(from_index) {
        //assert.ok(fromIndex >= 0, `fromIndex must be greater or equal to 0, instead was ${fromIndex}`);
        //assert.ok(Number.isInteger(fromIndex), `fromIndex must be an integer, instead was ${fromIndex}`);

        let word_index = from_index >> 5;

        let word;

        // treat first word specially, as we may need to mask out portion of a word to skip certain number of bits
        let bit_index = from_index & 31;

        const data = this.__data_uint32;

        if (bit_index !== 0) {
            // bit offset boundary in inside the word, we need to mask part of the word

            word = data[word_index];

            const fill_mask = (1 << bit_index) - 1;

            // apply mask and convert to uint32 via shift
            const masked_word = (word | fill_mask) >>> 0;

            if (masked_word !== 4294967295) {
                bit_index = lsb_32(~masked_word);
                return bit_index + word_index * 32;
            }

            word_index++;
        }
        const set_length = this.__length;
        const word_count = (set_length + 31) >> 5; // Math.ceil(x /32)

        //scan the rest
        for (; word_index < word_count; word_index++) {
            word = data[word_index];

            if (word === 4294967295) {
                // no 0s
                continue;
            }

            bit_index = lsb_32(~word);
            return bit_index + word_index * 32;

        }

        return set_length;
    }

    /**
     * Sets the bit at the specified index to the specified value.
     * @param {number} bit_index The index of the bit to set.
     * @param {boolean} value The value to set the bit to (true for 1, false for 0).
     * @returns {void}
     */
    set(bit_index, value) {
        assert.isNonNegativeInteger(bit_index, 'bit_index');
        assert.isBoolean(value, 'value');

        const word_offset = bit_index >> 5;
        const bit_offset = bit_index & 31;

        //const oldLength = this.__length;

        const word_mask = 1 << bit_offset;

        if (value) {
            const bitIndexInc = bit_index + 1;

            if (bitIndexInc > this.__length) {
                // ensure capacity
                this.__setLength(bitIndexInc);
            }

            //set
            this.__data_uint32[word_offset] |= word_mask;


        } else if (bit_index < this.__length) {
            //clear
            this.__data_uint32[word_offset] &= ~word_mask;

            // trim down set size potentially
            this.__updateLength();
        }

        //DEBUG validate firstClearBit value
        //assert.ok(this.__firstClearBitIndex === -1 || this.__firstClearBitIndex === scanToFirstClearBit(this), `Invalid first clear bit index, oldLength=${oldLength}, bitIndex=${bitIndex}`);
    }

    /**
     * Sets the bit specified by the index to false (0).
     * @param {number} bit_index The index of the bit to clear.
     */
    clear(bit_index) {
        this.set(bit_index, false);
    }

    /**
     * Set all bits in a given range
     * @param {number} start_index first bit to be set (inclusive)
     * @param {number} end_index last bit to be set (inclusive)
     */
    setRange(start_index, end_index) {
        assert.greaterThanOrEqual(start_index, 0, "invalid start index");
        assert.greaterThanOrEqual(end_index, 0, "invalid end index");


        for (let i = start_index; i <= end_index; i++) {
            this.set(i, true);
        }

    }

    /**
     * Clears bit values in a given (inclusive) range
     * @param {number} start_index first bit to be cleared (inclusive)
     * @param {number} end_index clear up to here, excluding this position
     */
    clearRange(start_index, end_index) {
        assert.greaterThanOrEqual(start_index, 0, "invalid start index");
        assert.greaterThanOrEqual(end_index, 0, "invalid end index");

        for (let i = start_index; i < end_index; i++) {
            this.set(i, false);
        }
    }

    /**
     * Returns the value of the bit with the specified index.
     * @param {int} bit_index The index of the bit to get.
     * @returns {boolean} The value of the bit (true for 1, false for 0).  Returns `false` if `bitIndex` is out of bounds.
     */
    get(bit_index) {
        assert.isNonNegativeInteger(bit_index, "bitIndex");

        if (bit_index >= this.__length) {
            //bit is outside the recorded region
            return false;
        }

        const byteOffset = bit_index >> 5;
        const bitOffset = bit_index & 31;

        const word = this.__data_uint32[byteOffset];

        const masked_word = word & (1 << bitOffset);

        return masked_word !== 0;
    }

    /**
     * Set a bit at the specified index and return its previous value
     * @param {number} index
     * @returns {boolean}
     */
    getAndSet(index) {
        const v = this.get(index);

        if (!v) {
            this.set(index, true);
        }

        return v;
    }

    /**
     * Clear a bit at the specified index and return its previous value
     * @param {number} index
     * @returns {boolean}
     */
    getAndClear(index) {
        const v = this.get(index);

        if (v) {
            this.set(index, false);
        }

        return v;
    }

    /**
     *
     * @param {number} distance
     * @param {number} start_index
     * @param {number} end_index
     */
    shift_right(distance, start_index, end_index) {
        assert.isNonNegativeInteger(distance, 'distance');

        for (let i = end_index; i >= start_index; i--) {
            const v = this.get(i);
            this.set(i + distance, v);
        }
    }

    /**
     *
     * @param {number} distance
     * @param {number} start_index
     * @param {number} end_index
     */
    shift_left(distance, start_index, end_index) {
        assert.isNonNegativeInteger(distance, 'distance');

        for (let i = start_index; i <= end_index; i++) {
            const v = this.get(i);
            this.set(i - distance, v);
        }
    }

    /**
     *
     * @param {number} distance
     * @param {number} start_index
     * @param {number} end_index
     */
    shift(distance, start_index, end_index) {
        if (distance > 0) {
            this.shift_right(distance, start_index, end_index);
        } else {
            this.shift_right(-distance, start_index, end_index);
        }
    }

    /**
     * Sets all the bits in this BitSet to false.
     */
    reset() {
        const current_length = this.__length;

        if (current_length <= 0) {
            // no action required
            return;
        }

        if (current_length <= 32) {
            // one word, set first word to 0 manually
            this.__data_uint32[0] = 0;
        } else {
            this.__data_uint32.fill(0, 0, Math.ceil(current_length / 32));
        }

        this.__length = 0;
    }

    /**
     * Number of set bits in this BitSet.
     * Also known as "Hamming weight" or "population count".
     * @returns {number}
     */
    cardinality(){
        const length = this.__length;

        if (length === 0) {
            return 0;
        }

        const data = this.__data_uint32;
        let count = 0;

        // Number of fully utilized 32-bit words
        const full_words = length >> 5;

        // Count bits in all complete words
        for (let i = 0; i < full_words; i++) {
            count += bitCount(data[i]);
        }

        // Handle any remaining bits in the partial last word
        const remainder_bits = length & 31;

        if (remainder_bits > 0) {
            // Create a mask to ignore any bits beyond __length
            // e.g., if remainder is 3, mask is (1<<3)-1 = 7 (0b111)
            const mask = (1 << remainder_bits) - 1;
            count += bitCount(data[full_words] & mask);
        }

        return count;
    }

    /**
     * Copy contents of another bit set into this one
     * @param {BitSet} other
     */
    copy(other) {
        const length = other.__length;

        const uint32_size = length >> 5;

        const old_length = this.__length;


        if (old_length !== length) {

            if (old_length < length) {
                this.__resize(length);
            } else {
                // clear everything past the target region
                this.__data_uint32.fill(0, uint32_size);
            }

            this.__length = length;
        }


        for (let i = 0; i < uint32_size; i++) {
            this.__data_uint32[i] = other.__data_uint32[i];
        }

        const byte_aligned_bit_size = uint32_size << 5;

        const overflow_bits = length - byte_aligned_bit_size;

        for (let i = 0; i < overflow_bits; i++) {
            const bitIndex = byte_aligned_bit_size + i;

            this.set(bitIndex, other.get(bitIndex));
        }
    }

    /**
     *
     * @param {number} x
     * @returns {BitSet}
     */
    static fixedSize(x) {
        const r = new BitSet(x);

        r.preventShrink();

        return r;
    }
}