/**
 * StreamBuf - Cross-Platform Multi-purpose Read-Write Stream
 *
 * A unified implementation that works in both Node.js and Browser environments
 * using the cross-platform EventEmitter from modules/stream.
 *
 * Features:
 * - As MemBuf: write data, then call toBuffer() to consolidate
 * - As StreamHub: pipe to multiple writable streams
 * - As readable stream: feed data into writable part and read from it
 */
import { EventEmitter } from "../../stream/index.js";
import { StringBuf } from "./string-buf.js";
class StringChunk {
    constructor(data) {
        this._data = data;
    }
    get length() {
        return this.toBuffer().length;
    }
    copy(target, targetOffset, offset, length) {
        const buf = this.toBuffer();
        const bytesToCopy = Math.min(length, buf.length - offset);
        target.set(buf.subarray(offset, offset + bytesToCopy), targetOffset);
        return bytesToCopy;
    }
    toBuffer() {
        if (!this._buffer) {
            this._buffer = new TextEncoder().encode(this._data);
        }
        return this._buffer;
    }
}
class StringBufChunk {
    constructor(data) {
        this._data = data;
    }
    get length() {
        return this._data.length;
    }
    copy(target, targetOffset, offset, length) {
        const buf = this.toBuffer();
        const bytesToCopy = Math.min(length, buf.length - offset);
        target.set(buf.subarray(offset, offset + bytesToCopy), targetOffset);
        return bytesToCopy;
    }
    toBuffer() {
        return this._data.toBuffer();
    }
}
class BufferChunk {
    constructor(data) {
        this._data = data;
    }
    get length() {
        return this._data.length;
    }
    copy(target, targetOffset, offset, length) {
        const bytesToCopy = Math.min(length, this._data.length - offset);
        target.set(this._data.subarray(offset, offset + bytesToCopy), targetOffset);
        return bytesToCopy;
    }
    toBuffer() {
        return this._data;
    }
}
// =============================================================================
// ReadWriteBuf - a single buffer supporting simple read-write
// =============================================================================
class ReadWriteBuf {
    constructor(size) {
        this.size = size;
        this.buffer = new Uint8Array(size);
        this.iRead = 0;
        this.iWrite = 0;
    }
    toBuffer() {
        if (this.iRead === 0 && this.iWrite === this.size) {
            return this.buffer;
        }
        return this.buffer.slice(this.iRead, this.iWrite);
    }
    get length() {
        return this.iWrite - this.iRead;
    }
    get eod() {
        return this.iRead === this.iWrite;
    }
    get full() {
        return this.iWrite === this.size;
    }
    read(size) {
        if (size === 0) {
            return null;
        }
        if (size === undefined || size >= this.length) {
            const buf = this.toBuffer();
            this.iRead = this.iWrite;
            return buf;
        }
        const buf = this.buffer.slice(this.iRead, this.iRead + size);
        this.iRead += size;
        return buf;
    }
    write(chunk, offset, length) {
        const size = Math.min(length, this.size - this.iWrite);
        chunk.copy(this.buffer, this.iWrite, offset, offset + size);
        this.iWrite += size;
        return size;
    }
}
// =============================================================================
// StreamBuf - Cross-Platform Implementation
// =============================================================================
/**
 * StreamBuf is a multi-purpose read-write stream that works in both
 * Node.js and Browser environments.
 *
 * It extends EventEmitter to provide stream-like events:
 * - 'data': emitted when data is written (flowing mode)
 * - 'readable': emitted when data is available to read
 * - 'finish': emitted when end() is called
 * - 'error': emitted on errors
 * - 'drain': emitted when buffer drains (after pipe)
 */
class StreamBuf extends EventEmitter {
    constructor(options) {
        super();
        // Native WritableStream support
        this._writableStream = null;
        this._writableStreamWriter = null;
        this._asyncWriteQueue = Promise.resolve();
        this.bufSize = options?.bufSize || 1024 * 1024;
        this.buffers = [];
        this.batch = options?.batch || false;
        this.corked = false;
        this.paused = false;
        this.encoding = null;
        this.pipes = [];
        this._ended = false;
    }
    /**
     * Returns true if the stream is writable (not ended)
     * Required for compatibility with Node.js pipe()
     */
    get writable() {
        return !this._ended;
    }
    /**
     * Consolidate all buffers into a single Uint8Array
     */
    toBuffer() {
        switch (this.buffers.length) {
            case 0:
                return null;
            case 1:
                return this.buffers[0].toBuffer();
            default: {
                const totalLength = this.buffers.reduce((acc, buf) => acc + buf.length, 0);
                const result = new Uint8Array(totalLength);
                let offset = 0;
                for (const rwBuf of this.buffers) {
                    const buf = rwBuf.toBuffer();
                    result.set(buf, offset);
                    offset += buf.length;
                }
                return result;
            }
        }
    }
    _getWritableBuffer() {
        if (this.buffers.length) {
            const last = this.buffers[this.buffers.length - 1];
            if (!last.full) {
                return last;
            }
        }
        const buf = new ReadWriteBuf(this.bufSize);
        this.buffers.push(buf);
        return buf;
    }
    async _pipeChunk(chunk) {
        const writePromises = this.pipes.map((pipe) => new Promise(resolve => {
            pipe.write(chunk.toBuffer(), () => resolve());
        }));
        await Promise.all(writePromises);
    }
    _writeToBuffers(chunk) {
        let inPos = 0;
        const inLen = chunk.length;
        while (inPos < inLen) {
            const buffer = this._getWritableBuffer();
            inPos += buffer.write(chunk, inPos, inLen - inPos);
        }
    }
    /**
     * Write data to the stream
     */
    async write(data, encoding, callback) {
        const nop = () => { };
        if (typeof encoding === "function") {
            callback = encoding;
        }
        callback = callback || nop;
        // Create chunk from data
        let chunk;
        if (data instanceof StringBuf || (data && data.constructor?.name === "StringBuf")) {
            chunk = new StringBufChunk(data);
        }
        else if (data instanceof Uint8Array) {
            chunk = new BufferChunk(data);
        }
        else if (ArrayBuffer.isView(data)) {
            chunk = new BufferChunk(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
        }
        else if (data instanceof ArrayBuffer) {
            chunk = new BufferChunk(new Uint8Array(data));
        }
        else if (typeof data === "string") {
            chunk = new StringChunk(data);
        }
        else {
            throw new Error("Chunk must be one of type String, Uint8Array, ArrayBuffer or StringBuf.");
        }
        // Handle piping and buffering
        if (this.pipes.length) {
            if (this.batch) {
                this._writeToBuffers(chunk);
                while (!this.corked && this.buffers.length > 1) {
                    const buf = this.buffers.shift();
                    await this._pipeChunk(new BufferChunk(buf.toBuffer()));
                }
            }
            else if (!this.corked) {
                await this._pipeChunk(chunk);
                callback();
            }
            else {
                this._writeToBuffers(chunk);
                queueMicrotask(() => callback());
            }
        }
        else {
            const chunkBuffer = chunk.toBuffer();
            if (!this.paused) {
                this.emit("data", chunkBuffer);
            }
            // Also write to native WritableStream if connected
            if (this._writableStreamWriter) {
                this._asyncWriteQueue = this._asyncWriteQueue.then(() => this._writableStreamWriter.write(chunkBuffer));
            }
            this._writeToBuffers(chunk);
            this.emit("readable");
        }
        return true;
    }
    /**
     * Cork the stream - buffer writes until uncork
     */
    cork() {
        this.corked = true;
    }
    _flush() {
        if (this.pipes.length) {
            const flushAll = async () => {
                while (this.buffers.length) {
                    const buf = this.buffers.shift();
                    await this._pipeChunk(new BufferChunk(buf.toBuffer()));
                }
            };
            flushAll().catch(err => this.emit("error", err));
        }
    }
    /**
     * Uncork the stream - flush buffered writes
     */
    uncork() {
        this.corked = false;
        this._flush();
    }
    /**
     * End the stream
     */
    end(chunk, encoding, callback) {
        const writeComplete = (error) => {
            if (error) {
                callback?.(error);
                return;
            }
            this._ended = true;
            this._flush();
            this.pipes.forEach((pipe) => {
                if (typeof pipe.end === "function") {
                    pipe.end();
                }
            });
            // If we have a native WritableStream, wait for all async writes to complete
            if (this._writableStreamWriter) {
                this._asyncWriteQueue
                    .then(() => this._writableStreamWriter.close())
                    .then(() => {
                    this.emit("finish");
                })
                    .catch(err => {
                    this.emit("error", err);
                });
            }
            else {
                this.emit("finish");
            }
        };
        if (chunk) {
            this.write(chunk, encoding, writeComplete);
        }
        else {
            writeComplete();
        }
    }
    /**
     * Read from the stream
     */
    read(size) {
        if (size) {
            const buffers = [];
            let remaining = size;
            while (remaining && this.buffers.length && !this.buffers[0].eod) {
                const first = this.buffers[0];
                const buffer = first.read(remaining);
                if (buffer) {
                    remaining -= buffer.length;
                    buffers.push(buffer);
                }
                if (first.eod && first.full) {
                    this.buffers.shift();
                }
            }
            return concatUint8Arrays(buffers);
        }
        const buffers = this.buffers.map(buf => buf.toBuffer()).filter(Boolean);
        this.buffers = [];
        return concatUint8Arrays(buffers);
    }
    /**
     * Read from the stream and return as string.
     * Cross-platform compatible - works identically in Node.js and Browser.
     */
    readString(encoding) {
        const enc = encoding || this.encoding || "utf-8";
        const buf = this.read();
        if (typeof Buffer !== "undefined" && buf instanceof Buffer) {
            return buf.toString(enc);
        }
        return new TextDecoder(enc).decode(buf);
    }
    /**
     * Set encoding for string reads
     */
    setEncoding(encoding) {
        this.encoding = encoding;
    }
    /**
     * Pause the stream
     */
    pause() {
        this.paused = true;
    }
    /**
     * Resume the stream
     */
    resume() {
        this.paused = false;
    }
    /**
     * Check if stream is paused
     */
    isPaused() {
        return this.paused;
    }
    /**
     * Pipe to a writable stream
     */
    pipe(destination) {
        this.pipes.push(destination);
        if (!this.paused && this.buffers.length) {
            this.end();
        }
        return destination;
    }
    /**
     * Pipe to a native WritableStream (browser Streams API).
     * This properly handles async writes and waits for completion before finish.
     */
    pipeTo(writableStream) {
        this._writableStream = writableStream;
        this._writableStreamWriter = writableStream.getWriter();
    }
    /**
     * Remove a piped destination
     */
    unpipe(destination) {
        this.pipes = this.pipes.filter((pipe) => pipe !== destination);
    }
    /**
     * Put data back at the front (not implemented)
     */
    unshift() {
        throw new Error("Not Implemented");
    }
    /**
     * Wrap a stream (not implemented)
     */
    wrap() {
        throw new Error("Not Implemented");
    }
    /**
     * Push data to the stream (alias for write)
     */
    push(chunk) {
        if (chunk !== null) {
            this.write(chunk);
        }
        return true;
    }
}
// =============================================================================
// Utility Functions
// =============================================================================
/**
 * Concatenate multiple Uint8Arrays into one
 * Returns Buffer in Node.js for better toString() compatibility
 */
function concatUint8Arrays(arrays) {
    // In Node.js, use Buffer.concat for better compatibility (Buffer.toString() works)
    if (typeof Buffer !== "undefined" && typeof Buffer.concat === "function") {
        return Buffer.concat(arrays);
    }
    if (arrays.length === 0) {
        return new Uint8Array(0);
    }
    if (arrays.length === 1) {
        return arrays[0];
    }
    const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0);
    const result = new Uint8Array(totalLength);
    let offset = 0;
    for (const arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}
export { StreamBuf };
