/**
 * @fileoverview StreamInfo class and XMLElement class for LSL stream metadata.
 *
 * This module provides classes for declaring and manipulating stream metadata.
 * StreamInfo objects describe the properties of a data stream including its
 * name, type, channel count, sampling rate, and data format.
 *
 * @module streamInfo
 * @see {@link https://labstreaminglayer.readthedocs.io/} - LSL Documentation
 */
import { lsl_create_streaminfo, lsl_destroy_streaminfo, lsl_get_name, lsl_get_type, lsl_get_channel_count, lsl_get_nominal_srate, lsl_get_channel_format, lsl_get_source_id, lsl_get_version, lsl_get_created_at, lsl_get_uid, lsl_get_session_id, lsl_get_hostname, lsl_get_desc, lsl_get_xml, lsl_first_child, lsl_last_child, lsl_next_sibling, lsl_next_sibling_n, lsl_previous_sibling, lsl_previous_sibling_n, lsl_parent, lsl_child, lsl_empty, lsl_is_text, lsl_name, lsl_value, lsl_child_value, lsl_child_value_n, lsl_append_child_value, lsl_prepend_child_value, lsl_set_child_value, lsl_set_name, lsl_set_value, lsl_append_child, lsl_prepend_child, lsl_append_copy, lsl_prepend_copy, lsl_remove_child, lsl_remove_child_n, cf_float32, string2fmt } from './lib/index.js';
import { IRREGULAR_RATE } from './util.js';
/**
 * FinalizationRegistry for automatic cleanup of StreamInfo objects.
 * When a StreamInfo instance is garbage collected, this registry ensures
 * the underlying C object is properly freed to prevent memory leaks.
 *
 * @private
 */
const streamInfoRegistry = new FinalizationRegistry((obj) => {
    try {
        lsl_destroy_streaminfo(obj);
    }
    catch (e) {
        // Silently ignore cleanup errors - the object may already be destroyed
    }
});
/**
 * Represents the declaration of a data stream.
 *
 * StreamInfo is the primary metadata container for LSL streams. It stores:
 * - Core properties: name, type, channel count, sampling rate, data format
 * - Unique identifiers: source_id, uid, session_id
 * - Host information: hostname, version, creation time
 * - Extended metadata: channel labels, units, types via XML description
 *
 * @example
 * ```typescript
 * // Create a simple EEG stream
 * const info = new StreamInfo('MyEEGStream', 'EEG', 8, 250, 'float32');
 *
 * // Add channel labels
 * info.setChannelLabels(['Fp1', 'Fp2', 'F3', 'F4', 'C3', 'C4', 'P3', 'P4']);
 * ```
 *
 * @class
 */
export class StreamInfo {
    /** Pointer to the underlying LSL streaminfo C object */
    obj;
    /**
     * Creates a new StreamInfo object.
     *
     * @param {string} name - Human-readable name of the stream (e.g., 'BioSemi')
     * @param {string} type - Content type of the stream (e.g., 'EEG', 'Markers', 'Audio')
     * @param {number} channelCount - Number of channels in the stream
     * @param {number} nominalSrate - Nominal sampling rate in Hz (0 for irregular)
     * @param {number|string} channelFormat - Data format ('float32', 'string', etc. or constant)
     * @param {string|null} sourceId - Unique source identifier (auto-generated if not provided)
     * @param {any} handle - Internal: existing C object handle for wrapping
     *
     * @throws {Error} If channel format is unknown or stream creation fails
     */
    constructor(name = 'untitled', type = '', channelCount = 1, nominalSrate = IRREGULAR_RATE, channelFormat = cf_float32, sourceId, handle) {
        if (handle !== undefined) {
            // Wrap an existing C streaminfo object (used internally by resolver)
            this.obj = handle;
        }
        else {
            // Create a new streaminfo object
            // Convert string format specification to numeric constant
            if (typeof channelFormat === 'string') {
                channelFormat = string2fmt[channelFormat];
                if (channelFormat === undefined) {
                    throw new Error(`Unknown channel format: ${channelFormat}`);
                }
            }
            // Generate source_id if not provided
            if (sourceId === null || sourceId === undefined) {
                // Create a deterministic hash from stream parameters
                // This ensures the same parameters always generate the same source_id
                const hashInput = `${name}${type}${channelCount}${nominalSrate}${channelFormat}`;
                let hash = 0;
                // Simple hash algorithm: djb2 variant
                for (let i = 0; i < hashInput.length; i++) {
                    const char = hashInput.charCodeAt(i);
                    hash = ((hash << 5) - hash) + char; // hash * 33 + char
                    hash = hash & hash; // Convert to 32-bit integer
                }
                sourceId = hash.toString();
                console.log(`Generated source_id: '${sourceId}' for StreamInfo with name '${name}', type '${type}', ` +
                    `channel_count ${channelCount}, nominal_srate ${nominalSrate}, ` +
                    `and channel_format ${channelFormat}.`);
            }
            // Create the LSL streaminfo C object
            this.obj = lsl_create_streaminfo(name, type, channelCount, nominalSrate, channelFormat, sourceId);
            if (!this.obj) {
                throw new Error('Could not create stream description object.');
            }
            // Register for automatic cleanup when this object is garbage collected
            streamInfoRegistry.register(this, this.obj, this);
        }
    }
    /**
     * Manually destroy the stream info object and free resources.
     * This is called automatically when the object is garbage collected.
     */
    destroy() {
        if (this.obj) {
            try {
                streamInfoRegistry.unregister(this);
                lsl_destroy_streaminfo(this.obj);
            }
            catch (e) {
                console.error('StreamInfo deletion triggered error:', e);
            }
            this.obj = null;
        }
    }
    /**
     * Get the stream name.
     * @returns The name of the stream
     */
    name() {
        return lsl_get_name(this.obj);
    }
    /**
     * Get the stream type.
     * @returns The content type of the stream (e.g., 'EEG', 'Markers')
     */
    type() {
        return lsl_get_type(this.obj);
    }
    /**
     * Get the number of channels.
     * @returns The channel count of the stream
     */
    channelCount() {
        return lsl_get_channel_count(this.obj);
    }
    /**
     * Get the nominal sampling rate.
     * @returns The sampling rate in Hz (0 for irregular rate)
     */
    nominalSrate() {
        return lsl_get_nominal_srate(this.obj);
    }
    /**
     * Get the channel format.
     * @returns The numeric channel format constant
     */
    channelFormat() {
        return lsl_get_channel_format(this.obj);
    }
    /**
     * Get the unique source identifier.
     * @returns The source ID of the stream
     */
    sourceId() {
        return lsl_get_source_id(this.obj);
    }
    /* ============================================================================
     * HOSTING INFORMATION
     * These properties are assigned when the stream is bound to an outlet/inlet
     * ============================================================================ */
    /**
     * Get the protocol version used by the stream.
     * @returns {number} LSL protocol version number
     */
    version() {
        return lsl_get_version(this.obj);
    }
    /**
     * Get the creation timestamp of the stream.
     * @returns {number} LSL timestamp when the stream was created
     */
    createdAt() {
        return lsl_get_created_at(this.obj);
    }
    /**
     * Get the unique identifier of the stream.
     * This UID is generated when the stream is created and remains constant.
     * @returns {string} Unique stream identifier
     */
    uid() {
        return lsl_get_uid(this.obj);
    }
    /**
     * Get the session identifier.
     * Changes when the host system restarts or LSL is reinitialized.
     * @returns {string} Current session identifier
     */
    sessionId() {
        return lsl_get_session_id(this.obj);
    }
    /**
     * Get the hostname of the machine hosting the stream.
     * @returns {string} Hostname or IP address
     */
    hostname() {
        return lsl_get_hostname(this.obj);
    }
    /* ============================================================================
     * DATA DESCRIPTION
     * Extended metadata stored as XML
     * ============================================================================ */
    /**
     * Get the XML description of the stream.
     * This contains extended metadata like channel labels, units, etc.
     * @returns {XMLElement} Root XML element for manipulation
     */
    desc() {
        return new XMLElement(lsl_get_desc(this.obj));
    }
    /**
     * Get the complete stream metadata as an XML string.
     * Useful for debugging or saving stream configuration.
     * @returns {string} XML representation of all stream metadata
     */
    asXml() {
        return lsl_get_xml(this.obj) || '';
    }
    /* ============================================================================
     * CHANNEL METADATA METHODS
     * Convenience methods for managing channel properties
     * ============================================================================ */
    /**
     * Get the labels for all channels.
     * @returns {string[]|null} Array of channel labels or null if not set
     * @example
     * const labels = info.getChannelLabels(); // ['Fp1', 'Fp2', ...]
     */
    getChannelLabels() {
        return this._getChannelInfo('label');
    }
    /**
     * Get the types for all channels.
     * @returns {string[]|null} Array of channel types or null if not set
     * @example
     * const types = info.getChannelTypes(); // ['EEG', 'EEG', ...]
     */
    getChannelTypes() {
        return this._getChannelInfo('type');
    }
    /**
     * Get the units for all channels.
     * @returns {string[]|null} Array of channel units or null if not set
     * @example
     * const units = info.getChannelUnits(); // ['microvolts', 'microvolts', ...]
     */
    getChannelUnits() {
        return this._getChannelInfo('unit');
    }
    /**
     * Generic helper to extract channel information from XML.
     * @private
     * @param {string} name - Property name to extract ('label', 'type', or 'unit')
     * @returns {string[]|null} Array of values or null if not found
     */
    _getChannelInfo(name) {
        const desc = this.desc();
        // Check if channels element exists
        if (desc.child('channels').empty()) {
            return null;
        }
        const chInfos = [];
        const channels = desc.child('channels');
        let ch = channels.child('channel');
        // Iterate through all channel elements
        while (!ch.empty()) {
            const chInfo = ch.child(name).firstChild().value();
            if (chInfo.length !== 0) {
                chInfos.push(chInfo);
            }
            else {
                chInfos.push(null);
            }
            ch = ch.nextSibling();
        }
        // Return null if no channel has this property
        if (chInfos.every(info => info === null)) {
            return null;
        }
        // Warn if channel count mismatch
        if (chInfos.length !== this.channelCount()) {
            console.warn(`The stream description contains ${chInfos.length} elements for ` +
                `${this.channelCount()} channels.`);
        }
        return chInfos.filter(info => info !== null);
    }
    /**
     * Set labels for all channels.
     * @param {string[]} labels - Array of channel labels (must match channel count)
     * @throws {Error} If array length doesn't match channel count
     * @example
     * info.setChannelLabels(['Fp1', 'Fp2', 'F3', 'F4']);
     */
    setChannelLabels(labels) {
        this._setChannelInfo(labels, 'label');
    }
    /**
     * Set types for all channels.
     * @param {string|string[]} types - Single type for all channels or array of types
     * @example
     * info.setChannelTypes('EEG'); // All channels are EEG
     * info.setChannelTypes(['EEG', 'EEG', 'EOG', 'EOG']); // Mixed types
     */
    setChannelTypes(types) {
        const typeArray = typeof types === 'string'
            ? new Array(this.channelCount()).fill(types)
            : types;
        this._setChannelInfo(typeArray, 'type');
    }
    /**
     * Set units for all channels.
     * @param {string|number|(string|number)[]} units - Single unit or array of units
     * @example
     * info.setChannelUnits('microvolts'); // All channels in microvolts
     * info.setChannelUnits(['microvolts', 'millivolts', 'celsius', 'percent']);
     */
    setChannelUnits(units) {
        let unitArray;
        if (typeof units === 'string' || typeof units === 'number') {
            unitArray = new Array(this.channelCount()).fill(typeof units === 'number' ? units.toString() : units);
        }
        else {
            unitArray = units.map(unit => typeof unit === 'number' ? unit.toString() : unit);
        }
        this._setChannelInfo(unitArray, 'unit');
    }
    /**
     * Generic helper to set channel information in XML.
     * @private
     * @param {string[]} chInfos - Array of values to set
     * @param {string} name - Property name ('label', 'type', or 'unit')
     * @throws {Error} If array length doesn't match channel count
     */
    _setChannelInfo(chInfos, name) {
        if (chInfos.length !== this.channelCount()) {
            throw new Error(`The number of provided channel ${name} ${chInfos.length} ` +
                `must match the number of channels ${this.channelCount()}.`);
        }
        // Ensure channels element exists
        const channels = StreamInfo._addFirstNode(this.desc.bind(this), 'channels');
        let ch = channels.child('channel');
        // Set info for each channel
        for (const chInfo of chInfos) {
            // Create channel element if needed
            ch = ch.empty() ? channels.appendChild('channel') : ch;
            StreamInfo._setDescriptionNode(ch, { [name]: chInfo });
            ch = ch.nextSibling();
        }
        // Remove any extra channel elements
        StreamInfo._pruneDescriptionNode(ch, channels);
    }
    /**
     * Helper to ensure an XML node exists, creating it if necessary.
     * @private
     */
    static _addFirstNode(desc, name) {
        const node = desc().child(name);
        return node.empty() ? desc().appendChild(name) : node;
    }
    /**
     * Helper to remove excess XML nodes.
     * @private
     */
    static _pruneDescriptionNode(node, parent) {
        while (!node.empty()) {
            const nodeNext = node.nextSibling();
            parent.removeChild(node);
            node = nodeNext;
        }
    }
    /**
     * Helper to set values in XML nodes.
     * @private
     */
    static _setDescriptionNode(node, mapping) {
        for (const [key, value] of Object.entries(mapping)) {
            // Value is already a string since mapping is { [key: string]: string }
            if (node.child(key).empty()) {
                node.appendChildValue(key, value);
            }
            else {
                node.child(key).firstChild().setValue(value);
            }
        }
    }
    /**
     * Get the internal C object handle.
     * @internal Used by StreamOutlet and StreamInlet
     * @returns {any} Pointer to the C streaminfo object
     */
    getHandle() {
        return this.obj;
    }
}
/**
 * Represents an XML element in the stream description.
 *
 * XMLElement provides methods for navigating and manipulating the XML tree
 * structure that stores extended stream metadata. This is used for channel
 * descriptions, hardware settings, synchronization parameters, etc.
 *
 * @example
 * ```typescript
 * const desc = info.desc();
 * const acq = desc.appendChild('acquisition');
 * acq.appendChildValue('manufacturer', 'ACME');
 * acq.appendChildValue('model', 'StreamMaster3000');
 * ```
 *
 * @class
 */
export class XMLElement {
    /** Pointer to the underlying LSL XML element */
    e;
    /**
     * Creates an XMLElement wrapper.
     * @internal Created internally by StreamInfo methods
     * @param {any} handle - C XML element pointer
     */
    constructor(handle) {
        this.e = handle;
    }
    /* ============================================================================
     * TREE NAVIGATION
     * Methods for traversing the XML tree structure
     * ============================================================================ */
    firstChild() {
        return new XMLElement(lsl_first_child(this.e));
    }
    lastChild() {
        return new XMLElement(lsl_last_child(this.e));
    }
    child(name) {
        return new XMLElement(lsl_child(this.e, name));
    }
    nextSibling(name) {
        if (name === undefined) {
            return new XMLElement(lsl_next_sibling(this.e));
        }
        else {
            return new XMLElement(lsl_next_sibling_n(this.e, name));
        }
    }
    previousSibling(name) {
        if (name === undefined) {
            return new XMLElement(lsl_previous_sibling(this.e));
        }
        else {
            return new XMLElement(lsl_previous_sibling_n(this.e, name));
        }
    }
    parent() {
        return new XMLElement(lsl_parent(this.e));
    }
    /* ============================================================================
     * CONTENT QUERIES
     * Methods for inspecting element content
     * ============================================================================ */
    empty() {
        return Boolean(lsl_empty(this.e));
    }
    isText() {
        return Boolean(lsl_is_text(this.e));
    }
    name() {
        return lsl_name(this.e) || '';
    }
    value() {
        return lsl_value(this.e) || '';
    }
    childValue(name) {
        if (name === undefined) {
            return lsl_child_value(this.e) || '';
        }
        else {
            return lsl_child_value_n(this.e, name) || '';
        }
    }
    /* ============================================================================
     * MODIFICATION
     * Methods for modifying the XML tree
     * ============================================================================ */
    appendChildValue(name, value) {
        return new XMLElement(lsl_append_child_value(this.e, name, value));
    }
    prependChildValue(name, value) {
        return new XMLElement(lsl_prepend_child_value(this.e, name, value));
    }
    setChildValue(name, value) {
        lsl_set_child_value(this.e, name, value);
        return this;
    }
    setName(name) {
        return Boolean(lsl_set_name(this.e, name));
    }
    setValue(value) {
        return Boolean(lsl_set_value(this.e, value));
    }
    appendChild(name) {
        return new XMLElement(lsl_append_child(this.e, name));
    }
    prependChild(name) {
        return new XMLElement(lsl_prepend_child(this.e, name));
    }
    appendCopy(elem) {
        return new XMLElement(lsl_append_copy(this.e, elem.e));
    }
    prependCopy(elem) {
        return new XMLElement(lsl_prepend_copy(this.e, elem.e));
    }
    removeChild(rhs) {
        if (rhs instanceof XMLElement) {
            lsl_remove_child(this.e, rhs.e);
        }
        else {
            lsl_remove_child_n(this.e, rhs);
        }
    }
}
//# sourceMappingURL=streamInfo.js.map