import { once } from 'node:events';
import { Readable } from 'node:stream';
import { AVSEEK_CUR, AVSEEK_END, AVSEEK_SET, AVSEEK_SIZE } from '../constants/constants.js';
import { IOContext } from '../lib/index.js';
import { IO_BUFFER_SIZE, MAX_PACKET_SIZE } from './constants.js';
/**
 * Factory for creating custom I/O contexts.
 *
 * Provides simplified creation of I/O contexts from buffers or custom callbacks.
 * Handles buffer management and seek operations for in-memory media.
 * Bridges the gap between high-level media operations and custom I/O sources.
 * Essential for processing media from non-file sources like network streams or memory.
 *
 * @example
 * ```typescript
 * import { IOStream, Demuxer } from 'node-av/api';
 *
 * // From buffer
 * const buffer = await fs.readFile('video.mp4');
 * const ioContext = IOStream.create(buffer);
 * const input = await Demuxer.open(buffer);
 * ```
 *
 * @example
 * ```typescript
 * // Custom I/O callbacks
 * const callbacks = {
 *   read: async (size: number) => {
 *     // Read from custom source
 *     return Buffer.alloc(size);
 *   },
 *   seek: async (offset: bigint, whence: AVSeekWhence) => {
 *     // Seek in custom source
 *     return offset;
 *   }
 * };
 *
 * const ioContext = IOStream.create(callbacks, {
 *   bufferSize: 4096
 * });
 * ```
 *
 * @see {@link IOContext} For low-level I/O operations
 * @see {@link Demuxer} For using I/O contexts
 * @see {@link IOInputCallbacks} For callback interface
 */
export class IOStream {
    static create(input, options = {}) {
        const { bufferSize = IO_BUFFER_SIZE } = options;
        // Handle Buffer
        if (Buffer.isBuffer(input)) {
            return this.createFromBuffer(input, bufferSize);
        }
        // Handle Readable stream (must be before callbacks check since Readable has 'read')
        if (input instanceof Readable) {
            return this.createFromReadable(input, bufferSize);
        }
        // Handle custom callbacks
        if (typeof input === 'object' && 'read' in input) {
            return this.createFromInputCallbacks(input, bufferSize);
        }
        throw new TypeError('Invalid input type. Expected Buffer, Readable, or IOInputCallbacks');
    }
    static createOutput(output, options) {
        const { bufferSize = IO_BUFFER_SIZE, maxPacketSize = MAX_PACKET_SIZE } = options ?? {};
        // Handle Writable stream (must be before callbacks check since Writable has 'write')
        if (typeof output === 'object' && 'writable' in output) {
            return this.createForWritable(output, bufferSize, maxPacketSize);
        }
        // Handle custom callbacks
        if (typeof output === 'object' && 'write' in output) {
            return this.createFromOutputCallbacks(output, bufferSize, maxPacketSize);
        }
        throw new TypeError('Invalid output type. Expected Writable or IOOutputCallbacks');
    }
    /**
     * Create I/O context from buffer.
     *
     * Sets up read and seek callbacks for in-memory buffer.
     * Manages position tracking and EOF handling.
     *
     * @param buffer - Source buffer
     *
     * @param bufferSize - Internal buffer size
     *
     * @returns Configured I/O context
     *
     * @internal
     */
    static createFromBuffer(buffer, bufferSize) {
        let position = 0;
        const ioContext = new IOContext();
        ioContext.allocContextWithCallbacks(bufferSize, 0, (size) => {
            if (position >= buffer.length) {
                return null; // EOF
            }
            const chunk = buffer.subarray(position, Math.min(position + size, buffer.length));
            position += chunk.length;
            return chunk;
        }, undefined, (offset, whence) => {
            switch (whence) {
                case AVSEEK_SIZE:
                    return BigInt(buffer.length);
                case AVSEEK_SET:
                    position = Number(offset);
                    break;
                case AVSEEK_CUR:
                    position += Number(offset);
                    break;
                case AVSEEK_END:
                    position = buffer.length + Number(offset);
                    break;
            }
            // Clamp position
            position = Math.max(0, Math.min(position, buffer.length));
            return BigInt(position);
        });
        return ioContext;
    }
    /**
     * Create I/O context from a Node.js Readable stream.
     *
     * Sets up async read callbacks that buffer data from the stream.
     * Seeking is not supported for streams.
     *
     * @param stream - Node.js Readable stream
     *
     * @param bufferSize - Internal buffer size
     *
     * @returns Configured I/O context
     *
     * @internal
     */
    static createFromReadable(stream, bufferSize) {
        let chunks = [];
        let totalBuffered = 0;
        let streamEnded = false;
        let streamError = null;
        let pendingResolve = null;
        const wakeUp = () => {
            if (pendingResolve) {
                const resolve = pendingResolve;
                pendingResolve = null;
                resolve();
            }
        };
        const removeListeners = () => {
            stream.off('readable', onReadable);
            stream.off('end', onEnd);
            stream.off('error', onError);
            stream.off('close', wakeUp);
        };
        const onReadable = () => wakeUp();
        const onEnd = () => {
            streamEnded = true;
            wakeUp();
        };
        const onError = (err) => {
            streamError = err;
            wakeUp();
        };
        // Track state changes and wake up pending reads
        stream.on('readable', onReadable);
        stream.on('end', onEnd);
        stream.on('error', onError);
        stream.on('close', wakeUp);
        const ioContext = new IOContext();
        ioContext.allocContextWithCallbacks(bufferSize, 0, // read mode
        async (size) => {
            // Pull-based: read from stream on demand
            while (totalBuffered < size && !streamEnded && !streamError && !stream.destroyed) {
                // Try to read available data first
                let chunk;
                while ((chunk = stream.read()) !== null) {
                    chunks.push(chunk);
                    totalBuffered += chunk.length;
                }
                // If we have enough data, stop waiting
                if (totalBuffered >= size)
                    break;
                // If stream is done, stop waiting
                if (streamEnded || streamError || stream.destroyed)
                    break;
                // Wait for more data
                await new Promise((resolve) => {
                    pendingResolve = resolve;
                });
            }
            // No data available — EOF, remove listeners
            if (totalBuffered === 0) {
                removeListeners();
                return null;
            }
            const concat = Buffer.concat(chunks);
            const result = concat.subarray(0, Math.min(size, concat.length));
            const remainder = concat.subarray(result.length);
            chunks = remainder.length > 0 ? [remainder] : [];
            totalBuffered = remainder.length;
            // If stream is done and all data consumed, remove listeners
            if (totalBuffered === 0 && (streamEnded || streamError || stream.destroyed)) {
                removeListeners();
            }
            return result;
        }, undefined, // no write
        undefined);
        return ioContext;
    }
    /**
     * Create I/O context for writing to a Node.js Writable stream.
     *
     * Sets up async write callbacks with backpressure support.
     * The buffer is copied before writing since FFmpeg reuses its internal buffer.
     *
     * @param stream - Node.js Writable stream
     *
     * @param bufferSize - Internal buffer size
     *
     * @param maxPacketSize - Maximum packet size
     *
     * @returns Configured I/O context
     *
     * @internal
     */
    static createForWritable(stream, bufferSize, maxPacketSize) {
        const ioContext = new IOContext();
        ioContext.allocContextWithCallbacks(bufferSize, 1, // write mode
        undefined, // no read
        async (buffer) => {
            // Copy buffer — FFmpeg reuses the internal buffer after callback returns
            const copy = Buffer.from(buffer);
            if (!stream.write(copy)) {
                // Backpressure: wait for drain
                await once(stream, 'drain');
            }
            return copy.length;
        }, undefined);
        ioContext.maxPacketSize = maxPacketSize;
        return ioContext;
    }
    /**
     * Create I/O context from custom input callbacks.
     *
     * Sets up custom I/O with user-provided read and optional seek callbacks.
     *
     * @param callbacks - User I/O callbacks
     *
     * @param bufferSize - Internal buffer size
     *
     * @returns Configured I/O context
     *
     * @internal
     */
    static createFromInputCallbacks(callbacks, bufferSize) {
        const ioContext = new IOContext();
        ioContext.allocContextWithCallbacks(bufferSize, 0, // read mode
        callbacks.read, undefined, callbacks.seek);
        return ioContext;
    }
    /**
     * Create I/O context from custom output callbacks.
     *
     * Sets up custom I/O with user-provided write, optional seek, and optional read callbacks.
     *
     * @param callbacks - User I/O callbacks
     *
     * @param bufferSize - Internal buffer size
     *
     * @param maxPacketSize - Maximum packet size
     *
     * @returns Configured I/O context
     *
     * @internal
     */
    static createFromOutputCallbacks(callbacks, bufferSize, maxPacketSize) {
        const ioContext = new IOContext();
        ioContext.allocContextWithCallbacks(bufferSize, 1, // write mode
        callbacks.read, callbacks.write, callbacks.seek);
        ioContext.maxPacketSize = maxPacketSize;
        return ioContext;
    }
}
//# sourceMappingURL=io-stream.js.map