import { Uint8ArrayList } from 'uint8arraylist';
import { InvalidFrameError, InvalidStateError } from './errors.js';
import { FrameType, HEADER_LENGTH, YAMUX_VERSION } from './frame.js';
// used to bitshift in decoding
// native bitshift can overflow into a negative number, so we bitshift by multiplying by a power of 2
const twoPow24 = 2 ** 24;
/**
 * Decode a header from the front of a buffer
 *
 * @param data - Assumed to have enough bytes for a header
 */
export function decodeHeader(data) {
    if (data[0] !== YAMUX_VERSION) {
        throw new InvalidFrameError('Invalid frame version');
    }
    return {
        type: data[1],
        flag: (data[2] << 8) + data[3],
        streamID: (data[4] * twoPow24) + (data[5] << 16) + (data[6] << 8) + data[7],
        length: (data[8] * twoPow24) + (data[9] << 16) + (data[10] << 8) + data[11]
    };
}
/**
 * Decodes yamux frames from a source
 */
export class Decoder {
    source;
    /** Buffer for in-progress frames */
    buffer;
    /** Used to sanity check against decoding while in an inconsistent state */
    frameInProgress;
    constructor(source) {
        // Normally, when entering a for-await loop with an iterable/async iterable, the only ways to exit the loop are:
        // 1. exhaust the iterable
        // 2. throw an error - slow, undesirable if there's not actually an error
        // 3. break or return - calls the iterable's `return` method, finalizing the iterable, no more iteration possible
        //
        // In this case, we want to enter (and exit) a for-await loop per chunked data frame and continue processing the iterable.
        // To do this, we strip the `return` method from the iterator and can now `break` early and continue iterating.
        // Exiting the main for-await is still possible via 1. and 2.
        this.source = returnlessSource(source);
        this.buffer = new Uint8ArrayList();
        this.frameInProgress = false;
    }
    /**
     * Emits frames from the decoder source.
     *
     * Note: If `readData` is emitted, it _must_ be called before the next iteration
     * Otherwise an error is thrown
     */
    async *emitFrames() {
        for await (const chunk of this.source) {
            this.buffer.append(chunk);
            // Loop to consume as many bytes from the buffer as possible
            // Eg: when a single chunk contains several frames
            while (true) {
                const header = this.readHeader();
                if (header === undefined) {
                    break;
                }
                const { type, length } = header;
                if (type === FrameType.Data) {
                    // This is a data frame, the frame body must still be read
                    // `readData` must be called before the next iteration here
                    this.frameInProgress = true;
                    yield {
                        header,
                        readData: this.readBytes.bind(this, length)
                    };
                }
                else {
                    yield { header };
                }
            }
        }
    }
    readHeader() {
        // Sanity check to ensure a header isn't read when another frame is partially decoded
        // In practice this shouldn't happen
        if (this.frameInProgress) {
            throw new InvalidStateError('decoding frame already in progress');
        }
        if (this.buffer.length < HEADER_LENGTH) {
            // not enough data yet
            return;
        }
        const header = decodeHeader(this.buffer.subarray(0, HEADER_LENGTH));
        this.buffer.consume(HEADER_LENGTH);
        return header;
    }
    async readBytes(length) {
        if (this.buffer.length < length) {
            for await (const chunk of this.source) {
                this.buffer.append(chunk);
                if (this.buffer.length >= length) {
                    // see note above, the iterator is not `return`ed here
                    break;
                }
            }
        }
        const out = this.buffer.sublist(0, length);
        this.buffer.consume(length);
        // The next frame can now be decoded
        this.frameInProgress = false;
        return out;
    }
}
/**
 * Strip the `return` method from a `Source`
 */
export function returnlessSource(source) {
    if (source[Symbol.iterator] !== undefined) {
        const iterator = source[Symbol.iterator]();
        iterator.return = undefined;
        return {
            [Symbol.iterator]() { return iterator; }
        };
    }
    else if (source[Symbol.asyncIterator] !== undefined) {
        const iterator = source[Symbol.asyncIterator]();
        iterator.return = undefined;
        return {
            [Symbol.asyncIterator]() { return iterator; }
        };
    }
    else {
        throw new Error('a source must be either an iterable or an async iterable');
    }
}
//# sourceMappingURL=decode.js.map