import initDebug from 'debug';
import * as strtok3 from 'strtok3/core';
import { StringType } from 'token-types';
import { Buffer } from 'node:buffer';
import * as util from '../common/Util.js';
import { BasicParser } from '../common/BasicParser.js';
import { DataType, DescriptorParser, Header, TagFooter, TagItemHeader } from './APEv2Token.js';
const debug = initDebug('music-metadata:parser:APEv2');
const tagFormat = 'APEv2';
const preamble = 'APETAGEX';
export class APEv2Parser extends BasicParser {
    constructor() {
        super(...arguments);
        this.ape = {};
    }
    static tryParseApeHeader(metadata, tokenizer, options) {
        const apeParser = new APEv2Parser();
        apeParser.init(metadata, tokenizer, options);
        return apeParser.tryParseApeHeader();
    }
    /**
     * Calculate the media file duration
     * @param ah ApeHeader
     * @return {number} duration in seconds
     */
    static calculateDuration(ah) {
        let duration = ah.totalFrames > 1 ? ah.blocksPerFrame * (ah.totalFrames - 1) : 0;
        duration += ah.finalFrameBlocks;
        return duration / ah.sampleRate;
    }
    /**
     * Calculates the APEv1 / APEv2 first field offset
     * @param reader
     * @param offset
     */
    static async findApeFooterOffset(reader, offset) {
        // Search for APE footer header at the end of the file
        const apeBuf = Buffer.alloc(TagFooter.len);
        await reader.randomRead(apeBuf, 0, TagFooter.len, offset - TagFooter.len);
        const tagFooter = TagFooter.get(apeBuf, 0);
        if (tagFooter.ID === 'APETAGEX') {
            debug(`APE footer header at offset=${offset}`);
            return { footer: tagFooter, offset: offset - tagFooter.size };
        }
    }
    static parseTagFooter(metadata, buffer, options) {
        const footer = TagFooter.get(buffer, buffer.length - TagFooter.len);
        if (footer.ID !== preamble)
            throw new Error('Unexpected APEv2 Footer ID preamble value.');
        strtok3.fromBuffer(buffer);
        const apeParser = new APEv2Parser();
        apeParser.init(metadata, strtok3.fromBuffer(buffer), options);
        return apeParser.parseTags(footer);
    }
    /**
     * Parse APEv1 / APEv2 header if header signature found
     */
    async tryParseApeHeader() {
        if (this.tokenizer.fileInfo.size && this.tokenizer.fileInfo.size - this.tokenizer.position < TagFooter.len) {
            debug(`No APEv2 header found, end-of-file reached`);
            return;
        }
        const footer = await this.tokenizer.peekToken(TagFooter);
        if (footer.ID === preamble) {
            await this.tokenizer.ignore(TagFooter.len);
            return this.parseTags(footer);
        }
        else {
            debug(`APEv2 header not found at offset=${this.tokenizer.position}`);
            if (this.tokenizer.fileInfo.size) {
                // Try to read the APEv2 header using just the footer-header
                const remaining = this.tokenizer.fileInfo.size - this.tokenizer.position; // ToDo: take ID3v1 into account
                const buffer = Buffer.alloc(remaining);
                await this.tokenizer.readBuffer(buffer);
                return APEv2Parser.parseTagFooter(this.metadata, buffer, this.options);
            }
        }
    }
    async parse() {
        const descriptor = await this.tokenizer.readToken(DescriptorParser);
        if (descriptor.ID !== 'MAC ')
            throw new Error('Unexpected descriptor ID');
        this.ape.descriptor = descriptor;
        const lenExp = descriptor.descriptorBytes - DescriptorParser.len;
        const header = await (lenExp > 0 ? this.parseDescriptorExpansion(lenExp) : this.parseHeader());
        await this.tokenizer.ignore(header.forwardBytes);
        return this.tryParseApeHeader();
    }
    async parseTags(footer) {
        const keyBuffer = Buffer.alloc(256); // maximum tag key length
        let bytesRemaining = footer.size - TagFooter.len;
        debug(`Parse APE tags at offset=${this.tokenizer.position}, size=${bytesRemaining}`);
        for (let i = 0; i < footer.fields; i++) {
            if (bytesRemaining < TagItemHeader.len) {
                this.metadata.addWarning(`APEv2 Tag-header: ${footer.fields - i} items remaining, but no more tag data to read.`);
                break;
            }
            // Only APEv2 tag has tag item headers
            const tagItemHeader = await this.tokenizer.readToken(TagItemHeader);
            bytesRemaining -= TagItemHeader.len + tagItemHeader.size;
            await this.tokenizer.peekBuffer(keyBuffer, { length: Math.min(keyBuffer.length, bytesRemaining) });
            let zero = util.findZero(keyBuffer, 0, keyBuffer.length);
            const key = await this.tokenizer.readToken(new StringType(zero, 'ascii'));
            await this.tokenizer.ignore(1);
            bytesRemaining -= key.length + 1;
            switch (tagItemHeader.flags.dataType) {
                case DataType.text_utf8: { // utf-8 text-string
                    const value = await this.tokenizer.readToken(new StringType(tagItemHeader.size, 'utf8'));
                    const values = value.split(/\x00/g);
                    for (const val of values) {
                        this.metadata.addTag(tagFormat, key, val);
                    }
                    break;
                }
                case DataType.binary: // binary (probably artwork)
                    if (this.options.skipCovers) {
                        await this.tokenizer.ignore(tagItemHeader.size);
                    }
                    else {
                        const picData = Buffer.alloc(tagItemHeader.size);
                        await this.tokenizer.readBuffer(picData);
                        zero = util.findZero(picData, 0, picData.length);
                        const description = picData.toString('utf8', 0, zero);
                        const data = Buffer.from(picData.slice(zero + 1));
                        this.metadata.addTag(tagFormat, key, {
                            description,
                            data
                        });
                    }
                    break;
                case DataType.external_info:
                    debug(`Ignore external info ${key}`);
                    await this.tokenizer.ignore(tagItemHeader.size);
                    break;
                case DataType.reserved:
                    debug(`Ignore external info ${key}`);
                    this.metadata.addWarning(`APEv2 header declares a reserved datatype for "${key}"`);
                    await this.tokenizer.ignore(tagItemHeader.size);
                    break;
            }
        }
    }
    async parseDescriptorExpansion(lenExp) {
        await this.tokenizer.ignore(lenExp);
        return this.parseHeader();
    }
    async parseHeader() {
        const header = await this.tokenizer.readToken(Header);
        // ToDo before
        this.metadata.setFormat('lossless', true);
        this.metadata.setFormat('container', 'Monkey\'s Audio');
        this.metadata.setFormat('bitsPerSample', header.bitsPerSample);
        this.metadata.setFormat('sampleRate', header.sampleRate);
        this.metadata.setFormat('numberOfChannels', header.channel);
        this.metadata.setFormat('duration', APEv2Parser.calculateDuration(header));
        return {
            forwardBytes: this.ape.descriptor.seekTableBytes + this.ape.descriptor.headerDataBytes +
                this.ape.descriptor.apeFrameDataBytes + this.ape.descriptor.terminatingDataBytes
        };
    }
}
