import { bigUintN } from './BigIntArbitrary.js';
import { Arbitrary } from './definition/Arbitrary.js';
import { Shrinkable } from './definition/Shrinkable.js';
export function countToggledBits(n) {
    let count = 0;
    while (n > BigInt(0)) {
        if (n & BigInt(1))
            ++count;
        n >>= BigInt(1);
    }
    return count;
}
export function computeNextFlags(flags, nextSize) {
    const allowedMask = (BigInt(1) << BigInt(nextSize)) - BigInt(1);
    const preservedFlags = flags & allowedMask;
    let numMissingFlags = countToggledBits(flags - preservedFlags);
    let nFlags = preservedFlags;
    for (let mask = BigInt(1); mask <= allowedMask && numMissingFlags !== 0; mask <<= BigInt(1)) {
        if (!(nFlags & mask)) {
            nFlags |= mask;
            --numMissingFlags;
        }
    }
    return nFlags;
}
class MixedCaseArbitrary extends Arbitrary {
    constructor(stringArb, toggleCase) {
        super();
        this.stringArb = stringArb;
        this.toggleCase = toggleCase;
    }
    computeTogglePositions(chars) {
        const positions = [];
        for (let idx = 0; idx !== chars.length; ++idx) {
            if (this.toggleCase(chars[idx]) !== chars[idx])
                positions.push(idx);
        }
        return positions;
    }
    wrapper(rawCase, chars, togglePositions, flags, flagsContext) {
        const newChars = chars.slice();
        for (let idx = 0, mask = BigInt(1); idx !== togglePositions.length; ++idx, mask <<= BigInt(1)) {
            if (flags & mask)
                newChars[togglePositions[idx]] = this.toggleCase(newChars[togglePositions[idx]]);
        }
        return new Shrinkable(newChars.join(''), () => this.shrinkImpl(rawCase, chars, togglePositions, flags, flagsContext));
    }
    shrinkImpl(rawCase, chars, togglePositions, flags, flagsContext) {
        return rawCase
            .shrink()
            .map((s) => {
            const nChars = [...s.value_];
            const nTogglePositions = this.computeTogglePositions(nChars);
            const nFlags = computeNextFlags(flags, nTogglePositions.length);
            return this.wrapper(s, nChars, nTogglePositions, nFlags, undefined);
        })
            .join(bigUintN(togglePositions.length)
            .contextualShrink(flags, flagsContext)
            .map((contextualValue) => {
            return this.wrapper(new Shrinkable(rawCase.value), chars, togglePositions, contextualValue[0], contextualValue[1]);
        }));
    }
    generate(mrng) {
        const rawCaseShrinkable = this.stringArb.generate(mrng);
        const chars = [...rawCaseShrinkable.value_];
        const togglePositions = this.computeTogglePositions(chars);
        const flagsArb = bigUintN(togglePositions.length);
        const flags = flagsArb.generate(mrng).value_;
        return this.wrapper(rawCaseShrinkable, chars, togglePositions, flags, undefined);
    }
}
function defaultToggleCase(rawChar) {
    const upper = rawChar.toUpperCase();
    if (upper !== rawChar)
        return upper;
    return rawChar.toLowerCase();
}
export function mixedCase(stringArb, constraints) {
    if (typeof BigInt === 'undefined') {
        throw new Error(`mixedCase requires BigInt support`);
    }
    const toggleCase = (constraints && constraints.toggleCase) || defaultToggleCase;
    return new MixedCaseArbitrary(stringArb, toggleCase);
}
