/**
 * @fileoverview StreamOutlet class for broadcasting data streams.
 *
 * This module provides the StreamOutlet class which broadcasts data samples
 * to the network. Outlets are the data sources in LSL - they push samples
 * that can be received by any number of StreamInlets.
 *
 * @module outlet
 * @see {@link https://labstreaminglayer.readthedocs.io/} - LSL Documentation
 */
import { lsl_create_outlet, lsl_destroy_outlet, lsl_have_consumers, lsl_wait_for_consumers, lsl_get_info, fmt2push_sample, fmt2push_chunk, fmt2push_chunk_n, cf_string } from './lib/index.js';
import { StreamInfo } from './streamInfo.js';
import { handleError } from './util.js';
/**
 * FinalizationRegistry for automatic cleanup of StreamOutlet objects.
 * Ensures the underlying C outlet is properly destroyed when the JavaScript
 * object is garbage collected, preventing resource leaks.
 *
 * @private
 */
const outletRegistry = new FinalizationRegistry((obj) => {
    try {
        lsl_destroy_outlet(obj);
    }
    catch (e) {
        // Silently ignore cleanup errors - outlet may already be destroyed
    }
});
/**
 * A stream outlet broadcasts data samples to the network.
 *
 * StreamOutlet is the data source in LSL. It pushes samples containing
 * measurement data or event markers to the network where they can be
 * received by any number of StreamInlets.
 *
 * Key features:
 * - Automatic buffering with configurable size
 * - Support for all LSL data types
 * - Optional pushthrough mode for low latency
 * - Chunk transmission for efficiency
 * - Consumer detection
 *
 * @example
 * ```typescript
 * // Create an EEG data outlet
 * const info = new StreamInfo('BioSemi', 'EEG', 32, 256, 'float32');
 * const outlet = new StreamOutlet(info);
 *
 * // Push samples at regular intervals
 * setInterval(() => {
 *   const sample = generateEEGData(); // 32 channels
 *   outlet.pushSample(sample);
 * }, 1000/256); // 256 Hz
 * ```
 *
 * @class
 */
export class StreamOutlet {
    /** Pointer to the underlying LSL outlet C object */
    obj;
    /** Numeric channel format constant for type checking */
    channelFormat;
    /** Number of channels for validation */
    channelCount;
    /** Function pointer for pushing single samples */
    doPushSample;
    /** Function pointer for pushing chunks (single timestamp) */
    doPushChunk;
    /** Function pointer for pushing chunks (multiple timestamps) */
    doPushChunkN;
    /**
     * Creates a new StreamOutlet.
     *
     * @param {StreamInfo} info - Stream metadata object describing the stream
     * @param {number} chunkSize - Preferred chunk size for transmission (0 = no chunking)
     *                              Samples are buffered until this size is reached.
     * @param {number} maxBuffered - Maximum amount of data to buffer in seconds (default: 360)
     *                               Older samples are dropped if buffer is full.
     *
     * @throws {Error} If outlet creation fails or channel format is unsupported
     *
     * @example
     * ```typescript
     * const outlet = new StreamOutlet(info, 0, 360); // No chunking, 6 min buffer
     * const outlet = new StreamOutlet(info, 128, 10); // 128-sample chunks, 10s buffer
     * ```
     */
    constructor(info, chunkSize = 0, maxBuffered = 360) {
        // Create the LSL outlet C object
        this.obj = lsl_create_outlet(info.getHandle(), chunkSize, maxBuffered);
        if (!this.obj) {
            throw new Error('Could not create stream outlet.');
        }
        // Register for automatic cleanup
        outletRegistry.register(this, this.obj, this);
        // Cache stream properties to avoid repeated lookups
        this.channelFormat = info.channelFormat();
        this.channelCount = info.channelCount();
        // Select type-specific push functions based on channel format
        // This avoids runtime type checking on every push operation
        this.doPushSample = fmt2push_sample[this.channelFormat];
        this.doPushChunk = fmt2push_chunk[this.channelFormat];
        this.doPushChunkN = fmt2push_chunk_n[this.channelFormat];
        if (!this.doPushSample) {
            throw new Error(`Unsupported channel format: ${this.channelFormat}`);
        }
    }
    /**
     * Destroy the outlet and free resources.
     * Called automatically when the object is garbage collected.
     */
    destroy() {
        if (this.obj) {
            try {
                outletRegistry.unregister(this);
                lsl_destroy_outlet(this.obj);
            }
            catch (e) {
                console.error('StreamOutlet deletion triggered error:', e);
            }
            this.obj = null;
        }
    }
    /**
     * Push a single sample into the outlet.
     * @param x Array of channel values
     * @param timestamp Optional timestamp (0 = use current LSL time)
     * @param pushthrough Whether to push through network buffers
     */
    pushSample(x, timestamp = 0.0, pushthrough = true) {
        // Input validation
        if (!Array.isArray(x)) {
            throw new TypeError('Sample must be an array');
        }
        if (x.length !== this.channelCount) {
            throw new Error(`Length of the sample (${x.length}) must correspond to the stream's channel count (${this.channelCount}).`);
        }
        // Convert the sample to appropriate format
        let sample;
        if (this.channelFormat === cf_string) {
            // For string channels, ensure all values are strings
            sample = x.map(v => String(v));
            // String arrays are passed directly to the C function
            // (Koffi handles the string marshalling)
        }
        else {
            // For numeric channels, create appropriate typed array for efficiency
            // TypedArrays provide direct memory access without conversion overhead
            let TypedArray;
            switch (this.channelFormat) {
                case 1: // cf_float32 - 32-bit floating point
                    TypedArray = Float32Array;
                    break;
                case 2: // cf_double64 - 64-bit floating point
                    TypedArray = Float64Array;
                    break;
                case 4: // cf_int32 - 32-bit signed integer
                    TypedArray = Int32Array;
                    break;
                case 5: // cf_int16 - 16-bit signed integer
                    TypedArray = Int16Array;
                    break;
                case 6: // cf_int8 - 8-bit signed integer
                    TypedArray = Int8Array;
                    break;
                case 7: // cf_int64 - 64-bit signed integer
                    TypedArray = BigInt64Array;
                    break;
                default:
                    throw new Error(`Unsupported channel format: ${this.channelFormat}`);
            }
            sample = new TypedArray(x);
        }
        // Push the sample to the outlet
        const result = this.doPushSample(this.obj, sample, timestamp, // 0 = use current LSL time
        pushthrough ? 1 : 0 // 1 = bypass network buffering for low latency
        );
        // Check for errors (timeout, lost connection, etc.)
        handleError(result);
    }
    /**
     * Push a chunk of samples into the outlet.
     * @param x 2D array of samples or flattened array
     * @param timestamp Single timestamp or array of timestamps
     * @param pushthrough Whether to push through network buffers
     */
    pushChunk(x, timestamp = 0.0, pushthrough = true) {
        // Input validation
        if (!Array.isArray(x)) {
            throw new TypeError('Chunk must be an array');
        }
        // Determine push function based on timestamp format
        // Single timestamp: all samples share the same timestamp
        // Array of timestamps: each sample has its own timestamp
        const hasMultipleTimestamps = Array.isArray(timestamp);
        const pushFunc = hasMultipleTimestamps ? this.doPushChunkN : this.doPushChunk;
        // Process input data format
        let flatData;
        let numSamples;
        if (x.length === 0) {
            return; // Don't send empty chunks - no data to transmit
        }
        if (Array.isArray(x[0])) {
            // 2D array format: [[ch1_s1, ch2_s1], [ch1_s2, ch2_s2], ...]
            // Each inner array is one sample across all channels
            numSamples = x.length;
            flatData = [];
            // Flatten while validating channel count
            for (const sample of x) {
                if (sample.length !== this.channelCount) {
                    throw new Error(`Each sample must have the same number of channels (${this.channelCount}).`);
                }
                flatData.push(...sample);
            }
        }
        else {
            // 1D array format: [ch1_s1, ch2_s1, ch1_s2, ch2_s2, ...]
            // Data is already flattened in channel-major order
            flatData = x;
            // Validate that we have complete samples
            if (flatData.length % this.channelCount !== 0) {
                throw new Error(`Data length must be a multiple of channel count (${this.channelCount}).`);
            }
            numSamples = flatData.length / this.channelCount;
        }
        // Validate timestamp array length if multiple timestamps
        if (hasMultipleTimestamps && timestamp.length !== numSamples) {
            throw new Error(`Timestamp array length (${timestamp.length}) must match number of samples (${numSamples}).`);
        }
        // Convert data to appropriate format for C function
        let dataBuffer;
        if (this.channelFormat === cf_string) {
            // String channels: ensure all values are strings
            const strings = flatData.map(v => String(v));
            dataBuffer = strings;
        }
        else {
            // For numeric channels, create appropriate typed array
            let TypedArray;
            switch (this.channelFormat) {
                case 1: // cf_float32
                    TypedArray = Float32Array;
                    break;
                case 2: // cf_double64
                    TypedArray = Float64Array;
                    break;
                case 4: // cf_int32
                    TypedArray = Int32Array;
                    break;
                case 5: // cf_int16
                    TypedArray = Int16Array;
                    break;
                case 6: // cf_int8
                    TypedArray = Int8Array;
                    break;
                case 7: // cf_int64
                    TypedArray = BigInt64Array;
                    break;
                default:
                    throw new Error(`Unsupported channel format: ${this.channelFormat}`);
            }
            dataBuffer = new TypedArray(flatData);
        }
        // Prepare timestamp parameter based on format
        let timestampParam;
        if (hasMultipleTimestamps) {
            // Convert timestamp array to Float64Array for C function
            timestampParam = new Float64Array(timestamp);
        }
        else {
            // Single timestamp is passed directly
            timestampParam = timestamp;
        }
        // Push the chunk to the outlet
        const result = pushFunc(this.obj, dataBuffer, // Flattened data buffer
        flatData.length, // Total number of values (samples * channels)
        timestampParam, // Timestamp(s)
        pushthrough ? 1 : 0 // Bypass buffering flag
        );
        // Check for transmission errors
        handleError(result);
    }
    /**
     * Check if any inlets are currently connected to this outlet.
     *
     * @returns {boolean} True if at least one inlet is connected
     *
     * @example
     * ```typescript
     * if (outlet.haveConsumers()) {
     *   // Someone is listening, send data
     *   outlet.pushSample(data);
     * }
     * ```
     */
    haveConsumers() {
        return Boolean(lsl_have_consumers(this.obj));
    }
    /**
     * Wait for inlets to connect to this outlet.
     *
     * Blocks until at least one inlet subscribes or timeout expires.
     * Useful for ensuring data isn't lost at stream startup.
     *
     * @param {number} timeout - Maximum time to wait in seconds
     * @returns {boolean} True if a consumer connected, false if timeout
     *
     * @example
     * ```typescript
     * console.log('Waiting for receivers...');
     * if (outlet.waitForConsumers(5.0)) {
     *   console.log('Connected! Starting data transmission.');
     * } else {
     *   console.log('No receivers found after 5 seconds.');
     * }
     * ```
     */
    waitForConsumers(timeout) {
        return Boolean(lsl_wait_for_consumers(this.obj, timeout));
    }
    /**
     * Get the StreamInfo object associated with this outlet.
     *
     * Returns a new StreamInfo object with updated metadata including
     * hostname, session ID, and creation time.
     *
     * @returns {StreamInfo} Updated stream information
     *
     * @example
     * ```typescript
     * const info = outlet.getInfo();
     * console.log(`Stream ${info.name()} created at ${info.createdAt()}`);
     * ```
     */
    getInfo() {
        const infoHandle = lsl_get_info(this.obj);
        return new StreamInfo('', '', 0, 0, 0, '', infoHandle);
    }
}
//# sourceMappingURL=outlet.js.map