// ASF Objects
import * as Token from 'token-types';
import * as util from '../common/Util.js';
import AsfGuid from './AsfGuid.js';
import { getParserForAttr, parseUnicodeAttr } from './AsfUtil.js';
import { AttachedPictureType } from '../id3v2/ID3v2Token.js';
import { makeUnexpectedFileContentError } from '../ParseError.js';
export class AsfContentParseError extends makeUnexpectedFileContentError('ASF') {
}
/**
 * Data Type: Specifies the type of information being stored. The following values are recognized.
 */
export const DataType = {
    /**
     * Unicode string. The data consists of a sequence of Unicode characters.
     */
    UnicodeString: 0,
    /**
     * BYTE array. The type of data is implementation-specific.
     */
    ByteArray: 1,
    /**
     * BOOL. The data is 2 bytes long and should be interpreted as a 16-bit unsigned integer. Only 0x0000 or 0x0001 are permitted values.
     */
    Bool: 2,
    /**
     * DWORD. The data is 4 bytes long and should be interpreted as a 32-bit unsigned integer.
     */
    DWord: 3,
    /**
     * QWORD. The data is 8 bytes long and should be interpreted as a 64-bit unsigned integer.
     */
    QWord: 4,
    /**
     * WORD. The data is 2 bytes long and should be interpreted as a 16-bit unsigned integer.
     */
    Word: 5
};
/**
 * Token for: 3. ASF top-level Header Object
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3
 */
export const TopLevelHeaderObjectToken = {
    len: 30,
    get: (buf, off) => {
        const base = HeaderObjectToken.get(buf, off);
        if (base.objectSize < TopLevelHeaderObjectToken.len) {
            throw new AsfContentParseError(`Invalid ASF top-level header object size: ${base.objectSize}`);
        }
        return {
            ...base,
            numberOfHeaderObjects: Token.UINT32_LE.get(buf, off + 24)
            // reserved: 2 bytes
        };
    }
};
/**
 * Token for: 3.1 Header Object (mandatory, one only)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_1
 */
export const HeaderObjectToken = {
    len: 24,
    get: (buf, off) => {
        const objectSize = Token.UINT64_LE.get(buf, off + 16);
        if (objectSize < BigInt(HeaderObjectToken.len) || objectSize > BigInt(Number.MAX_SAFE_INTEGER)) {
            throw new AsfContentParseError(`Invalid ASF header object size: ${objectSize}`);
        }
        const header = {
            objectId: AsfGuid.fromBin(buf, off),
            objectSize: Number(objectSize)
        };
        return header;
    }
};
export class State {
    constructor(header) {
        this.len = header.objectSize - HeaderObjectToken.len;
    }
    postProcessTag(tags, name, valueType, data) {
        if (name === 'WM/Picture') {
            tags.push({ id: name, value: WmPictureToken.fromBuffer(data) });
        }
        else {
            const parseAttr = getParserForAttr(valueType);
            if (!parseAttr) {
                throw new AsfContentParseError(`unexpected value headerType: ${valueType}`);
            }
            tags.push({ id: name, value: parseAttr(data) });
        }
    }
}
/**
 * Token for: 3.2: File Properties Object (mandatory, one only)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_2
 */
export class FilePropertiesObject extends State {
    get(buf, off) {
        return {
            fileId: AsfGuid.fromBin(buf, off),
            fileSize: Token.UINT64_LE.get(buf, off + 16),
            creationDate: Token.UINT64_LE.get(buf, off + 24),
            dataPacketsCount: Token.UINT64_LE.get(buf, off + 32),
            playDuration: Token.UINT64_LE.get(buf, off + 40),
            sendDuration: Token.UINT64_LE.get(buf, off + 48),
            preroll: Token.UINT64_LE.get(buf, off + 56),
            flags: {
                broadcast: util.getBit(buf, off + 64, 24),
                seekable: util.getBit(buf, off + 64, 25)
            },
            // flagsNumeric: Token.UINT32_LE.get(buf, off + 64),
            minimumDataPacketSize: Token.UINT32_LE.get(buf, off + 68),
            maximumDataPacketSize: Token.UINT32_LE.get(buf, off + 72),
            maximumBitrate: Token.UINT32_LE.get(buf, off + 76)
        };
    }
}
FilePropertiesObject.guid = AsfGuid.FilePropertiesObject;
/**
 * Token for: 3.3 Stream Properties Object (mandatory, one per stream)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_3
 */
export class StreamPropertiesObject extends State {
    get(buf, off) {
        return {
            streamType: AsfGuid.decodeMediaType(AsfGuid.fromBin(buf, off)),
            errorCorrectionType: AsfGuid.fromBin(buf, off + 8)
            // ToDo
        };
    }
}
StreamPropertiesObject.guid = AsfGuid.StreamPropertiesObject;
/**
 * 3.4: Header Extension Object (mandatory, one only)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_4
 */
export class HeaderExtensionObject {
    constructor() {
        this.len = 22;
    }
    get(buf, off) {
        const view = new DataView(buf.buffer, off);
        return {
            reserved1: AsfGuid.fromBin(buf, off),
            reserved2: view.getUint16(16, true),
            extensionDataSize: view.getUint32(18, true)
        };
    }
}
HeaderExtensionObject.guid = AsfGuid.HeaderExtensionObject;
/**
 * 3.5: The Codec List Object provides user-friendly information about the codecs and formats used to encode the content found in the ASF file.
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_5
 */
const CodecListObjectHeader = {
    len: 20,
    get: (buf, off) => {
        const view = new DataView(buf.buffer, off);
        return {
            entryCount: view.getUint16(16, true)
        };
    }
};
class BoundedReader {
    constructor(tokenizer, size) {
        this.tokenizer = tokenizer;
        this.remaining = size;
    }
    async readToken(token) {
        this.ensureAvailable(token.len);
        const value = await this.tokenizer.readToken(token);
        this.remaining -= token.len;
        return value;
    }
    async readNumber(token) {
        return this.readToken(token);
    }
    async readBuffer(length) {
        this.ensureAvailable(length);
        const buffer = new Uint8Array(length);
        await this.tokenizer.readBuffer(buffer);
        this.remaining -= length;
        return buffer;
    }
    async ignoreRemaining() {
        const ignored = await this.tokenizer.ignore(this.remaining);
        if (ignored !== this.remaining) {
            throw new AsfContentParseError(`Unexpected end of ASF Codec List Object; missing ${this.remaining - ignored} bytes`);
        }
        this.remaining = 0;
    }
    ensureAvailable(length) {
        if (length > this.remaining) {
            throw new AsfContentParseError(`ASF Codec List field size ${length} exceeds remaining object payload size ${this.remaining}`);
        }
    }
}
async function readString(reader) {
    const length = await reader.readNumber(Token.UINT16_LE);
    return (await reader.readToken(new Token.StringType(length * 2, 'utf-16le'))).replace('\0', '');
}
/**
 * 3.5: Read the Codec-List-Object, which provides user-friendly information about the codecs and formats used to encode the content found in the ASF file.
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_5
 */
export async function readCodecEntries(tokenizer, payloadSize) {
    const reader = new BoundedReader(tokenizer, payloadSize);
    const codecHeader = await reader.readToken(CodecListObjectHeader);
    const entries = [];
    for (let i = 0; i < codecHeader.entryCount; ++i) {
        entries.push(await readCodecEntry(reader));
    }
    await reader.ignoreRemaining();
    return entries;
}
async function readInformation(reader) {
    const length = await reader.readNumber(Token.UINT16_LE);
    return reader.readBuffer(length);
}
/**
 * Read Codec-Entries
 * @param reader
 */
async function readCodecEntry(reader) {
    const type = await reader.readNumber(Token.UINT16_LE);
    return {
        type: {
            videoCodec: (type & 0x0001) === 0x0001,
            audioCodec: (type & 0x0002) === 0x0002
        },
        codecName: await readString(reader),
        description: await readString(reader),
        information: await readInformation(reader)
    };
}
/**
 * 3.10 Content Description Object (optional, one only)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_10
 */
export class ContentDescriptionObjectState extends State {
    get(buf, off) {
        const tags = [];
        const view = new DataView(buf.buffer, off);
        let pos = 10;
        for (let i = 0; i < ContentDescriptionObjectState.contentDescTags.length; ++i) {
            const length = view.getUint16(i * 2, true);
            if (length > 0) {
                const tagName = ContentDescriptionObjectState.contentDescTags[i];
                const end = pos + length;
                tags.push({ id: tagName, value: parseUnicodeAttr(buf.subarray(off + pos, off + end)) });
                pos = end;
            }
        }
        return tags;
    }
}
ContentDescriptionObjectState.guid = AsfGuid.ContentDescriptionObject;
ContentDescriptionObjectState.contentDescTags = ['Title', 'Author', 'Copyright', 'Description', 'Rating'];
/**
 * 3.11 Extended Content Description Object (optional, one only)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/03_asf_top_level_header_object.html#3_11
 */
export class ExtendedContentDescriptionObjectState extends State {
    get(buf, off) {
        const tags = [];
        const view = new DataView(buf.buffer, off);
        const attrCount = view.getUint16(0, true);
        let pos = 2;
        for (let i = 0; i < attrCount; i += 1) {
            const nameLen = view.getUint16(pos, true);
            pos += 2;
            const name = parseUnicodeAttr(buf.subarray(off + pos, off + pos + nameLen));
            pos += nameLen;
            const valueType = view.getUint16(pos, true);
            pos += 2;
            const valueLen = view.getUint16(pos, true);
            pos += 2;
            const value = buf.subarray(off + pos, off + pos + valueLen);
            pos += valueLen;
            this.postProcessTag(tags, name, valueType, value);
        }
        return tags;
    }
}
ExtendedContentDescriptionObjectState.guid = AsfGuid.ExtendedContentDescriptionObject;
/**
 * 4.1 Extended Stream Properties Object (optional, 1 per media stream)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/04_objects_in_the_asf_header_extension_object.html#4_1
 */
export class ExtendedStreamPropertiesObjectState extends State {
    get(buf, off) {
        const view = new DataView(buf.buffer, off);
        return {
            startTime: Token.UINT64_LE.get(buf, off),
            endTime: Token.UINT64_LE.get(buf, off + 8),
            dataBitrate: view.getInt32(12, true),
            bufferSize: view.getInt32(16, true),
            initialBufferFullness: view.getInt32(20, true),
            alternateDataBitrate: view.getInt32(24, true),
            alternateBufferSize: view.getInt32(28, true),
            alternateInitialBufferFullness: view.getInt32(32, true),
            maximumObjectSize: view.getInt32(36, true),
            flags: {
                reliableFlag: util.getBit(buf, off + 40, 0),
                seekableFlag: util.getBit(buf, off + 40, 1),
                resendLiveCleanpointsFlag: util.getBit(buf, off + 40, 2)
            },
            // flagsNumeric: Token.UINT32_LE.get(buf, off + 64),
            streamNumber: view.getInt16(42, true),
            streamLanguageId: view.getInt16(44, true),
            averageTimePerFrame: view.getInt32(52, true),
            streamNameCount: view.getInt32(54, true),
            payloadExtensionSystems: view.getInt32(56, true),
            streamNames: [], // ToDo
            streamPropertiesObject: null
        };
    }
}
ExtendedStreamPropertiesObjectState.guid = AsfGuid.ExtendedStreamPropertiesObject;
/**
 * 4.7  Metadata Object (optional, 0 or 1)
 * Ref: http://drang.s4.xrea.com/program/tips/id3tag/wmp/04_objects_in_the_asf_header_extension_object.html#4_7
 */
export class MetadataObjectState extends State {
    get(uint8Array, off) {
        const tags = [];
        const view = new DataView(uint8Array.buffer, off);
        const descriptionRecordsCount = view.getUint16(0, true);
        let pos = 2;
        for (let i = 0; i < descriptionRecordsCount; i += 1) {
            pos += 4;
            const nameLen = view.getUint16(pos, true);
            pos += 2;
            const dataType = view.getUint16(pos, true);
            pos += 2;
            const dataLen = view.getUint32(pos, true);
            pos += 4;
            const name = parseUnicodeAttr(uint8Array.subarray(off + pos, off + pos + nameLen));
            pos += nameLen;
            const data = uint8Array.subarray(off + pos, off + pos + dataLen);
            pos += dataLen;
            this.postProcessTag(tags, name, dataType, data);
        }
        return tags;
    }
}
MetadataObjectState.guid = AsfGuid.MetadataObject;
// 4.8	Metadata Library Object (optional, 0 or 1)
export class MetadataLibraryObjectState extends MetadataObjectState {
}
MetadataLibraryObjectState.guid = AsfGuid.MetadataLibraryObject;
/**
 * Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd757977(v=vs.85).aspx
 */
export class WmPictureToken {
    static fromBuffer(buffer) {
        const pic = new WmPictureToken(buffer.length);
        return pic.get(buffer, 0);
    }
    constructor(len) {
        this.len = len;
    }
    get(buffer, offset) {
        const view = new DataView(buffer.buffer, offset);
        const typeId = view.getUint8(0);
        const size = view.getInt32(1, true);
        let index = 5;
        while (view.getUint16(index) !== 0) {
            index += 2;
        }
        const format = new Token.StringType(index - 5, 'utf-16le').get(buffer, 5);
        while (view.getUint16(index) !== 0) {
            index += 2;
        }
        const description = new Token.StringType(index - 5, 'utf-16le').get(buffer, 5);
        return {
            type: AttachedPictureType[typeId],
            format,
            description,
            size,
            data: buffer.slice(index + 4)
        };
    }
}
