/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */ /** */

import { Writeable } from '../Types';
import { LazyResizableTypedArray } from '../LazyResizableTypedArray';

type WriteableNumberArrayLike = Writeable<ArrayLike<number>>;

export abstract class BitArray {
    protected static readonly _bitCountPerByte = 8;
    public readonly bitCountPerArrayElement: number;

    public constructor(
        protected readonly _ref: { buffer: WriteableNumberArrayLike },
        public readonly bytesPerElement: number
    ) {
        this.bitCountPerArrayElement = bytesPerElement * 8;
    }

    public get bitCount(): number {
        return this._ref.buffer.length * this.bitCountPerArrayElement;
    }

    public get byteCount(): number {
        return this._ref.buffer.length * this.bytesPerElement;
    }

    public get(index: number): boolean {
        const bitExistsAtArrayIndex = Math.floor(index / this.bitCountPerArrayElement);
        if (bitExistsAtArrayIndex >= this._ref.buffer.length) {
            return false;
        }

        const indexOfBitInsideArrayElement = index - bitExistsAtArrayIndex * this.bitCountPerArrayElement;
        const arrayElementContainingBit = this._ref.buffer[bitExistsAtArrayIndex];
        return arrayElementContainingBit === (arrayElementContainingBit | (1 << indexOfBitInsideArrayElement));
    }

    public abstract set(index: number, truthy: unknown): BitArray;

    public toBinaryStrings(): string[] {
        const binary: string[] = [];
        const len = this._ref.buffer.length;
        for (let i = 0; i < len; ++i) {
            // Handles both sparse and dense arrays. Notice type coersion with >>> to force unsigned integer.
            binary.push(((this._ref.buffer[i] ?? 0) >>> 0).toString(2).padStart(this.bitCountPerArrayElement, '0'));
        }
        return binary;
    }

    public abstract clear(): void;
}

export class DenseBitArray<A extends Int8Array | Int16Array | Int32Array> extends BitArray {
    public constructor(private readonly _lazyResizable: LazyResizableTypedArray<number, A>) {
        // Pass _lazyResizable (byRef) so BitArray will reference _lazyResizable.buffer. Means that
        // when this.setMinLenght() resizes _lazyResizable.buffer then BitArray will still reference the new array
        super(_lazyResizable, _lazyResizable.buffer.BYTES_PER_ELEMENT);
    }
    public setMinLength(bitMinLength: number): void {
        const arrayMinLength = Math.floor(bitMinLength / this.bitCountPerArrayElement);
        this._lazyResizable.setMinLength(arrayMinLength + 1);
    }

    public set(index: number, truthy: unknown): BitArray {
        const arrayIndex = Math.floor(index / this.bitCountPerArrayElement);
        this._lazyResizable.setMinLength(arrayIndex + 1);

        const bitIndex = index - arrayIndex * this.bitCountPerArrayElement;

        if (!truthy) {
            this._ref.buffer[arrayIndex] = this._ref.buffer[arrayIndex] & ~(1 << bitIndex);
        } else {
            this._ref.buffer[arrayIndex] = this._ref.buffer[arrayIndex] | (1 << bitIndex);
        }
        return this;
    }

    public clear(): void {
        this._lazyResizable.reset();
    }
}

export class SparseBitArray extends BitArray {
    public constructor() {
        // [0] creates 32bit array in V8 engine
        super({ buffer: [0] }, Int32Array.BYTES_PER_ELEMENT);
    }

    public set(index: number, truthy: unknown): BitArray {
        const arrayIndex = Math.floor(index / this.bitCountPerArrayElement);
        const bitIndex = index - arrayIndex * this.bitCountPerArrayElement;

        if (!truthy) {
            this._ref.buffer[arrayIndex] = this._ref.buffer[arrayIndex] & ~(1 << bitIndex);
        } else {
            this._ref.buffer[arrayIndex] = this._ref.buffer[arrayIndex] | (1 << bitIndex);
        }
        return this;
    }

    public clear(): void {
        this._ref.buffer = [0];
    }
}
