export class AES_CMAC {
    BLOCK_SIZE = 16;
    XOR_RIGHT = new Uint8Array([
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87
    ]);
    EMPTY_BLOCK_SIZE_BUFFER = new Uint8Array(this.BLOCK_SIZE);

    constructor(key) {
        if (![16, 24, 32].includes(key.length)) {
            throw new Error("Key size must be 128, 192, or 256 bits.");
        }
        this._key = key;
    }

    async calculate(message) {
        this._subkeys = await this._generateSubkeys();
        const blockCount = this._getBlockCount(message);

        let x = this.EMPTY_BLOCK_SIZE_BUFFER;
        let y;

        for (let i = 0; i < blockCount - 1; i++) {
            const from = i * this.BLOCK_SIZE;
            const block = message.subarray(from, from + this.BLOCK_SIZE);
            y = this._xor(x, block);
            x = await this._aes(y);
        }

        y = this._xor(x, this._getLastBlock(message));
        x = await this._aes(y);

        return x;
    }

    async _generateSubkeys() {
        const l = await this._aes(this.EMPTY_BLOCK_SIZE_BUFFER);

        let first = this._bitShiftLeft(l);
        if (l[0] & 0x80) {
            first = this._xor(first, this.XOR_RIGHT);
        }

        let second = this._bitShiftLeft(first);
        if (first[0] & 0x80) {
            second = this._xor(second, this.XOR_RIGHT);
        }

        return { first: first, second: second };
    }

    _getBlockCount(message) {
        const blockCount = Math.ceil(message.length / this.BLOCK_SIZE);
        return blockCount === 0 ? 1 : blockCount;
    }

    async _aes(message) {
        const keyBuffer = forge.util.createBuffer(this._key, 'raw');
        const cipher = forge.cipher.createCipher('AES-CBC', keyBuffer);

        const iv = forge.util.createBuffer(new Uint8Array(16), 'raw');
        cipher.start({
            iv: iv.getBytes()
        });

        cipher.update(forge.util.createBuffer(message));
        cipher.finish();

        const outputBuffer = cipher.output;
        return new Uint8Array(outputBuffer.getBytes().slice(0, 16).split('').map(c => c.charCodeAt(0)));
    }

    _getLastBlock(message) {
        const blockCount = this._getBlockCount(message);
        const paddedBlock = this._padding(message, blockCount - 1);

        let complete = false;
        if (message.length > 0) {
            complete = message.length % this.BLOCK_SIZE === 0;
        }

        const key = complete ? this._subkeys.first : this._subkeys.second;
        return this._xor(paddedBlock, key);
    }

    _padding(message, blockIndex) {
        const block = new Uint8Array(this.BLOCK_SIZE);

        const from = blockIndex * this.BLOCK_SIZE;
        const slice = message.subarray(from, from + this.BLOCK_SIZE);
        block.set(slice);

        if (slice.length !== this.BLOCK_SIZE) {
            block[slice.length] = 0x80;
        }

        return block;
    }

    _bitShiftLeft(input) {
        const output = new Uint8Array(input.length);
        let overflow = 0;
        for (let i = input.length - 1; i >= 0; i--) {
            output[i] = (input[i] << 1) | overflow;
            overflow = input[i] & 0x80 ? 1 : 0;
        }
        return output;
    }

    _xor(a, b) {
        const length = Math.min(a.length, b.length);
        const output = new Uint8Array(length);
        for (let i = 0; i < length; i++) {
            output[i] = a[i] ^ b[i];
        }
        return output;
    }
}
