import initDebug from 'debug';
import * as Token from 'token-types';
import * as util from '../common/Util.js';
import { AttachedPictureType, SyncTextHeader, TextEncodingToken, TextHeader } from './ID3v2Token.js';
import { Genres } from '../id3v1/ID3v1Parser.js';
import { makeUnexpectedFileContentError } from '../ParseError.js';
import { decodeUintBE } from '../common/Util.js';
const debug = initDebug('music-metadata:id3v2:frame-parser');
const defaultEnc = 'latin1'; // latin1 == iso-8859-1;
export function parseGenre(origVal) {
    // match everything inside parentheses
    const genres = [];
    let code;
    let word = '';
    for (const c of origVal) {
        if (typeof code === 'string') {
            if (c === '(' && code === '') {
                word += '(';
                code = undefined;
            }
            else if (c === ')') {
                if (word !== '') {
                    genres.push(word);
                    word = '';
                }
                const genre = parseGenreCode(code);
                if (genre) {
                    genres.push(genre);
                }
                code = undefined;
            }
            else
                code += c;
        }
        else if (c === '(') {
            code = '';
        }
        else {
            word += c;
        }
    }
    if (word) {
        if (genres.length === 0 && word.match(/^\d*$/)) {
            word = parseGenreCode(word);
        }
        if (word) {
            genres.push(word);
        }
    }
    return genres;
}
function parseGenreCode(code) {
    if (code === 'RX')
        return 'Remix';
    if (code === 'CR')
        return 'Cover';
    if (code.match(/^\d*$/)) {
        return Genres[Number.parseInt(code)];
    }
}
export class FrameParser {
    /**
     * Create id3v2 frame parser
     * @param major - Major version, e.g. (4) for  id3v2.4
     * @param warningCollector - Used to collect decode issue
     */
    constructor(major, warningCollector) {
        this.major = major;
        this.warningCollector = warningCollector;
    }
    readData(uint8Array, type, includeCovers) {
        if (uint8Array.length === 0) {
            this.warningCollector.addWarning(`id3v2.${this.major} header has empty tag type=${type}`);
            return;
        }
        const { encoding, bom } = TextEncodingToken.get(uint8Array, 0);
        const length = uint8Array.length;
        let offset = 0;
        let output = []; // ToDo
        const nullTerminatorLength = FrameParser.getNullTerminatorLength(encoding);
        let fzero;
        debug(`Parsing tag type=${type}, encoding=${encoding}, bom=${bom}`);
        switch (type !== 'TXXX' && type[0] === 'T' ? 'T*' : type) {
            case 'T*': // 4.2.1. Text information frames - details
            case 'GRP1': // iTunes-specific ID3v2 grouping field
            case 'IPLS': // v2.3: Involved people list
            case 'MVIN':
            case 'MVNM':
            case 'PCS':
            case 'PCST': {
                let text;
                try {
                    text = util.decodeString(uint8Array.subarray(1), encoding).replace(/\x00+$/, '');
                }
                catch (error) {
                    if (error instanceof Error) {
                        this.warningCollector.addWarning(`id3v2.${this.major} type=${type} header has invalid string value: ${error.message}`);
                        break;
                    }
                    throw error;
                }
                switch (type) {
                    case 'TMCL': // Musician credits list
                    case 'TIPL': // Involved people list
                    case 'IPLS': // Involved people list
                        output = FrameParser.functionList(this.splitValue(type, text));
                        break;
                    case 'TRK':
                    case 'TRCK':
                    case 'TPOS':
                        output = text;
                        break;
                    case 'TCOM':
                    case 'TEXT':
                    case 'TOLY':
                    case 'TOPE':
                    case 'TPE1':
                    case 'TSRC':
                        // id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
                        output = this.splitValue(type, text);
                        break;
                    case 'TCO':
                    case 'TCON':
                        output = this.splitValue(type, text).map(v => parseGenre(v)).reduce((acc, val) => acc.concat(val), []);
                        break;
                    case 'PCS':
                    case 'PCST':
                        // TODO: Why `default` not results `1` but `''`?
                        output = this.major >= 4 ? this.splitValue(type, text) : [text];
                        output = (Array.isArray(output) && output[0] === '') ? 1 : 0;
                        break;
                    default:
                        output = this.major >= 4 ? this.splitValue(type, text) : [text];
                }
                break;
            }
            case 'TXXX': {
                const idAndData = FrameParser.readIdentifierAndData(uint8Array, offset + 1, length, encoding);
                const textTag = {
                    description: idAndData.id,
                    text: this.splitValue(type, util.decodeString(idAndData.data, encoding).replace(/\x00+$/, ''))
                };
                output = textTag;
                break;
            }
            case 'PIC':
            case 'APIC':
                if (includeCovers) {
                    const pic = {};
                    offset += 1;
                    switch (this.major) {
                        case 2:
                            pic.format = util.decodeString(uint8Array.subarray(offset, offset + 3), 'latin1'); // 'latin1'; // latin1 == iso-8859-1;
                            offset += 3;
                            break;
                        case 3:
                        case 4:
                            fzero = util.findZero(uint8Array, offset, length, defaultEnc);
                            pic.format = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
                            offset = fzero + 1;
                            break;
                        default:
                            throw makeUnexpectedMajorVersionError(this.major);
                    }
                    pic.format = FrameParser.fixPictureMimeType(pic.format);
                    pic.type = AttachedPictureType[uint8Array[offset]];
                    offset += 1;
                    fzero = util.findZero(uint8Array, offset, length, encoding);
                    pic.description = util.decodeString(uint8Array.subarray(offset, fzero), encoding);
                    offset = fzero + nullTerminatorLength;
                    pic.data = uint8Array.subarray(offset, length);
                    output = pic;
                }
                break;
            case 'CNT':
            case 'PCNT':
                output = decodeUintBE(uint8Array);
                break;
            case 'SYLT': {
                const syltHeader = SyncTextHeader.get(uint8Array, 0);
                offset += SyncTextHeader.len;
                const result = {
                    descriptor: '',
                    language: syltHeader.language,
                    contentType: syltHeader.contentType,
                    timeStampFormat: syltHeader.timeStampFormat,
                    syncText: []
                };
                let readSyllables = false;
                while (offset < length) {
                    const nullStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), syltHeader.encoding);
                    offset += nullStr.len;
                    if (readSyllables) {
                        const timestamp = Token.UINT32_BE.get(uint8Array, offset);
                        offset += Token.UINT32_BE.len;
                        result.syncText.push({
                            text: nullStr.text,
                            timestamp
                        });
                    }
                    else {
                        result.descriptor = nullStr.text;
                        readSyllables = true;
                    }
                }
                output = result;
                break;
            }
            case 'ULT':
            case 'USLT':
            case 'COM':
            case 'COMM': {
                const textHeader = TextHeader.get(uint8Array, offset);
                offset += TextHeader.len;
                const descriptorStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), textHeader.encoding);
                offset += descriptorStr.len;
                const textStr = FrameParser.readNullTerminatedString(uint8Array.subarray(offset), textHeader.encoding);
                const comment = {
                    language: textHeader.language,
                    descriptor: descriptorStr.text,
                    text: textStr.text
                };
                output = comment;
                break;
            }
            case 'UFID': {
                const ufid = FrameParser.readIdentifierAndData(uint8Array, offset, length, defaultEnc);
                output = { owner_identifier: ufid.id, identifier: ufid.data };
                break;
            }
            case 'PRIV': { // private frame
                const priv = FrameParser.readIdentifierAndData(uint8Array, offset, length, defaultEnc);
                output = { owner_identifier: priv.id, data: priv.data };
                break;
            }
            case 'POPM': { // Popularimeter
                fzero = util.findZero(uint8Array, offset, length, defaultEnc);
                const email = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
                offset = fzero + 1;
                const valueLen = length - offset - 1;
                output = {
                    email,
                    rating: Token.UINT8.get(uint8Array, offset),
                    counter: valueLen > 0 ? util.decodeUintBE(uint8Array.subarray(offset + 1)) : undefined
                };
                break;
            }
            case 'GEOB': { // General encapsulated object
                fzero = util.findZero(uint8Array, offset + 1, length, encoding);
                const mimeType = util.decodeString(uint8Array.subarray(offset + 1, fzero), defaultEnc);
                offset = fzero + 1;
                fzero = util.findZero(uint8Array, offset, length, encoding);
                const filename = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
                offset = fzero + 1;
                fzero = util.findZero(uint8Array, offset, length, encoding);
                const description = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
                offset = fzero + 1;
                const geob = {
                    type: mimeType,
                    filename,
                    description,
                    data: uint8Array.subarray(offset, length)
                };
                output = geob;
                break;
            }
            // W-Frames:
            case 'WCOM':
            case 'WCOP':
            case 'WOAF':
            case 'WOAR':
            case 'WOAS':
            case 'WORS':
            case 'WPAY':
            case 'WPUB':
                // Decode URL
                fzero = util.findZero(uint8Array, offset + 1, length, encoding);
                output = util.decodeString(uint8Array.subarray(offset, fzero), defaultEnc);
                break;
            case 'WXXX': {
                // Decode URL
                fzero = util.findZero(uint8Array, offset + 1, length, encoding);
                const description = util.decodeString(uint8Array.subarray(offset + 1, fzero), encoding);
                offset = fzero + (encoding === 'utf-16le' ? 2 : 1);
                output = { description, url: util.decodeString(uint8Array.subarray(offset, length), defaultEnc) };
                break;
            }
            case 'WFD':
            case 'WFED':
                output = util.decodeString(uint8Array.subarray(offset + 1, util.findZero(uint8Array, offset + 1, length, encoding)), encoding);
                break;
            case 'MCDI': {
                // Music CD identifier
                output = uint8Array.subarray(0, length);
                break;
            }
            default:
                debug(`Warning: unsupported id3v2-tag-type: ${type}`);
                break;
        }
        return output;
    }
    static readNullTerminatedString(uint8Array, encoding) {
        let offset = encoding.bom ? 2 : 0;
        const zeroIndex = util.findZero(uint8Array, offset, uint8Array.length, encoding.encoding);
        const txt = uint8Array.subarray(offset, zeroIndex);
        if (encoding.encoding === 'utf-16le') {
            offset = zeroIndex + 2;
        }
        else {
            offset = zeroIndex + 1;
        }
        return {
            text: util.decodeString(txt, encoding.encoding),
            len: offset
        };
    }
    static fixPictureMimeType(pictureType) {
        pictureType = pictureType.toLocaleLowerCase();
        switch (pictureType) {
            case 'jpg':
                return 'image/jpeg';
            case 'png':
                return 'image/png';
        }
        return pictureType;
    }
    /**
     * Converts TMCL (Musician credits list) or TIPL (Involved people list)
     * @param entries
     */
    static functionList(entries) {
        const res = {};
        for (let i = 0; i + 1 < entries.length; i += 2) {
            const names = entries[i + 1].split(',');
            res[entries[i]] = res[entries[i]] ? res[entries[i]].concat(names) : names;
        }
        return res;
    }
    /**
     * id3v2.4 defines that multiple T* values are separated by 0x00
     * id3v2.3 defines that TCOM, TEXT, TOLY, TOPE & TPE1 values are separated by /
     * @param tag - Tag name
     * @param text - Concatenated tag value
     * @returns Split tag value
     */
    splitValue(tag, text) {
        let values;
        if (this.major < 4) {
            values = text.split(/\x00/g);
            if (values.length > 1) {
                this.warningCollector.addWarning(`ID3v2.${this.major} ${tag} uses non standard null-separator.`);
            }
            else {
                values = text.split(/\//g);
            }
        }
        else {
            values = text.split(/\x00/g);
        }
        return FrameParser.trimArray(values);
    }
    static trimArray(values) {
        return values.map(value => value.replace(/\x00+$/, '').trim());
    }
    static readIdentifierAndData(uint8Array, offset, length, encoding) {
        const fzero = util.findZero(uint8Array, offset, length, encoding);
        const id = util.decodeString(uint8Array.subarray(offset, fzero), encoding);
        offset = fzero + FrameParser.getNullTerminatorLength(encoding);
        return { id, data: uint8Array.subarray(offset, length) };
    }
    static getNullTerminatorLength(enc) {
        return enc === 'utf-16le' ? 2 : 1;
    }
}
export class Id3v2ContentError extends makeUnexpectedFileContentError('id3v2') {
}
function makeUnexpectedMajorVersionError(majorVer) {
    throw new Id3v2ContentError(`Unexpected majorVer: ${majorVer}`);
}
