import * as Token from 'token-types';
import { EndOfStreamError } from 'strtok3';
import initDebug from 'debug';
import { BasicParser } from '../common/BasicParser.js';
import { VorbisStream } from './vorbis/VorbisStream.js';
import { OpusStream } from './opus/OpusStream.js';
import { SpeexStream } from './speex/SpeexStream.js';
import { TheoraStream } from './theora/TheoraStream.js';
import { makeUnexpectedFileContentError } from '../ParseError.js';
import { PageHeader, SegmentTable } from './OggToken.js';
import { FlacStream } from './flac/FlacStream.js';
export class OggContentError extends makeUnexpectedFileContentError('Ogg') {
}
const debug = initDebug('music-metadata:parser:ogg');
class OggStream {
    constructor(metadata, streamSerial, options) {
        this.pageNumber = 0;
        this.closed = false;
        this.metadata = metadata;
        this.streamSerial = streamSerial;
        this.options = options;
    }
    async parsePage(tokenizer, header) {
        this.pageNumber = header.pageSequenceNo;
        debug('serial=%s page#=%s, Ogg.id=%s', header.streamSerialNumber, header.pageSequenceNo, header.capturePattern);
        const segmentTable = await tokenizer.readToken(new SegmentTable(header));
        debug('totalPageSize=%s', segmentTable.totalPageSize);
        const pageData = await tokenizer.readToken(new Token.Uint8ArrayType(segmentTable.totalPageSize));
        debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
        if (header.headerType.firstPage) {
            this.metadata.setFormat('container', 'Ogg');
            const idData = pageData.subarray(0, 7); // Copy this portion
            const asciiId = Array.from(idData)
                .filter(b => b >= 32 && b <= 126) // Keep only printable ASCII
                .map(b => String.fromCharCode(b))
                .join('');
            switch (asciiId) {
                case 'vorbis': // Ogg/Vorbis
                    debug(`Set Ogg stream serial ${header.streamSerialNumber}, codec=Vorbis`);
                    this.pageConsumer = new VorbisStream(this.metadata, this.options);
                    break;
                case 'OpusHea': // Ogg/Opus
                    debug('Set page consumer to Ogg/Opus');
                    this.pageConsumer = new OpusStream(this.metadata, this.options, tokenizer);
                    break;
                case 'Speex  ': // Ogg/Speex
                    debug('Set page consumer to Ogg/Speex');
                    this.pageConsumer = new SpeexStream(this.metadata, this.options, tokenizer);
                    break;
                case 'fishead':
                case 'theora': // Ogg/Theora
                    debug('Set page consumer to Ogg/Theora');
                    this.pageConsumer = new TheoraStream(this.metadata, this.options, tokenizer);
                    break;
                case 'FLAC': // Ogg/Theora
                    debug('Set page consumer to Vorbis');
                    this.pageConsumer = new FlacStream(this.metadata, this.options, tokenizer);
                    break;
                default:
                    throw new OggContentError(`Ogg codec not recognized (id=${asciiId}`);
            }
        }
        if (header.headerType.lastPage) {
            this.closed = true;
        }
        if (this.pageConsumer) {
            await this.pageConsumer.parsePage(header, pageData);
        }
        else
            throw new Error('pageConsumer should be initialized');
    }
}
/**
 * Parser for Ogg logical bitstream framing
 */
export class OggParser extends BasicParser {
    constructor() {
        super(...arguments);
        this.streams = new Map();
    }
    /**
     * Parse page
     * @returns {Promise<void>}
     */
    async parse() {
        this.streams = new Map();
        let header;
        try {
            do {
                header = await this.tokenizer.readToken(PageHeader);
                if (header.capturePattern !== 'OggS')
                    throw new OggContentError('Invalid Ogg capture pattern');
                let stream = this.streams.get(header.streamSerialNumber);
                if (!stream) {
                    stream = new OggStream(this.metadata, header.streamSerialNumber, this.options);
                    this.streams.set(header.streamSerialNumber, stream);
                }
                await stream.parsePage(this.tokenizer, header);
                if (stream.pageNumber > 12 && !(this.options.duration && [...this.streams.values()].find(stream => stream.pageConsumer?.durationOnLastPage))) {
                    debug("Stop processing Ogg stream");
                    break;
                }
            } while (![...this.streams.values()].every(item => item.closed));
        }
        catch (err) {
            if (err instanceof EndOfStreamError) {
                debug("Reached end-of-stream");
            }
            else if (err instanceof OggContentError) {
                this.metadata.addWarning(`Corrupt Ogg content at ${this.tokenizer.position}`);
            }
            else
                throw err;
        }
        for (const stream of this.streams.values()) {
            if (!stream.closed) {
                this.metadata.addWarning(`End-of-stream reached before reaching last page in Ogg stream serial=${stream.streamSerial}`);
                await stream.pageConsumer?.flush();
            }
            stream.pageConsumer?.calculateDuration();
        }
    }
}
