/**
 * Buffered Stream
 *
 * A Duplex-like stream that manages internal buffering with chunk-based read/write operations.
 * Works identically in both browser and Node.js environments.
 */
import { EventEmitter } from "./event-emitter.js";
import { textEncoder } from "./shared.js";
/**
 * String chunk implementation
 */
export class StringChunk {
    constructor(data) {
        this._data = data;
    }
    get length() {
        // Cache the buffer on first access
        return this.toUint8Array().length;
    }
    copy(target, targetOffset, offset, length) {
        const source = this.toUint8Array();
        const copyLength = Math.min(length, source.length - offset);
        target.set(source.subarray(offset, offset + copyLength), targetOffset);
        return copyLength;
    }
    toUint8Array() {
        if (!this._buffer) {
            this._buffer = textEncoder.encode(this._data);
        }
        return this._buffer;
    }
}
/**
 * Uint8Array chunk implementation
 */
export class BufferChunk {
    constructor(data) {
        this._data = data;
    }
    get length() {
        return this._data.length;
    }
    copy(target, targetOffset, offset, length) {
        const copyLength = Math.min(length, this._data.length - offset);
        target.set(this._data.subarray(offset, offset + copyLength), targetOffset);
        return copyLength;
    }
    toUint8Array() {
        return this._data;
    }
}
/**
 * Read-Write buffer for efficient chunk management
 */
class ReadWriteBuffer {
    constructor(size) {
        this.iRead = 0;
        this.iWrite = 0;
        this.size = size;
        this.buffer = new Uint8Array(size);
    }
    get length() {
        return this.iWrite - this.iRead;
    }
    get isEOD() {
        return this.iRead === this.iWrite;
    }
    get isFull() {
        return this.iWrite === this.size;
    }
    read(size) {
        if (size === 0) {
            return null;
        }
        if (size === undefined || size >= this.length) {
            const buf = this.toUint8Array();
            this.iRead = this.iWrite;
            return buf;
        }
        const buf = new Uint8Array(size);
        buf.set(this.buffer.subarray(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, size);
        this.iWrite += size;
        return size;
    }
    toUint8Array() {
        if (this.iRead === 0 && this.iWrite === this.size) {
            return this.buffer;
        }
        // Use subarray for zero-copy view when returning whole remaining buffer
        return this.buffer.subarray(this.iRead, this.iWrite);
    }
}
/**
 * Browser-compatible Buffered Stream with efficient chunk management
 */
export class BufferedStream extends EventEmitter {
    constructor(options = {}) {
        super();
        this._chunks = [];
        this._chunkReadIndex = 0;
        this._buffers = [];
        this._bufferReadIndex = 0;
        this._finished = false;
        this._destroyed = false;
        this._totalLength = 0;
        this._batchSize = options.batchSize ?? 16384; // 16KB default
    }
    /**
     * Write data to the stream
     */
    write(chunk) {
        if (this._destroyed) {
            this.emit("error", new Error("Cannot write to destroyed stream"));
            return false;
        }
        const dataChunk = typeof chunk === "string" ? new StringChunk(chunk) : new BufferChunk(chunk);
        this._chunks.push(dataChunk);
        this._totalLength += dataChunk.length;
        return true;
    }
    /**
     * Read data from the stream
     */
    read(size) {
        // Try to satisfy the read from existing buffers
        let buffer = this._getBuffer(size);
        if (!buffer) {
            // Create new buffers from pending chunks
            while (this._chunkReadIndex < this._chunks.length) {
                const chunk = this._chunks[this._chunkReadIndex++];
                this._addChunkToBuffers(chunk);
            }
            // Reset/compact chunk queue when drained
            if (this._chunkReadIndex === this._chunks.length) {
                this._chunks.length = 0;
                this._chunkReadIndex = 0;
            }
            else if (this._chunkReadIndex > 1024 && this._chunkReadIndex * 2 > this._chunks.length) {
                this._chunks = this._chunks.slice(this._chunkReadIndex);
                this._chunkReadIndex = 0;
            }
            buffer = this._getBuffer(size);
        }
        return buffer;
    }
    /**
     * Signal end of writes
     */
    end(chunk) {
        if (chunk !== undefined) {
            this.write(chunk);
        }
        this._finished = true;
        this.emit("finish");
        this.emit("end");
    }
    /**
     * Destroy the stream
     */
    destroy(error) {
        if (this._destroyed) {
            return;
        }
        this._destroyed = true;
        this._chunks = [];
        this._chunkReadIndex = 0;
        this._buffers = [];
        this._bufferReadIndex = 0;
        this._totalLength = 0;
        if (error) {
            this.emit("error", error);
        }
        this.emit("close");
    }
    /**
     * Get total buffered length
     */
    get bufferedLength() {
        return this._totalLength;
    }
    /**
     * Get buffer of specified size from internal buffers
     */
    _getBuffer(size) {
        while (this._bufferReadIndex < this._buffers.length) {
            const buf = this._buffers[this._bufferReadIndex];
            if (buf.isEOD) {
                this._bufferReadIndex++;
                continue;
            }
            // Track length reduction
            const beforeLen = buf.length;
            const data = buf.read(size);
            this._totalLength -= beforeLen - buf.length;
            if (buf.isEOD) {
                this._bufferReadIndex++;
            }
            // Reset/compact buffer queue occasionally
            if (this._bufferReadIndex === this._buffers.length) {
                this._buffers.length = 0;
                this._bufferReadIndex = 0;
            }
            else if (this._bufferReadIndex > 1024 && this._bufferReadIndex * 2 > this._buffers.length) {
                this._buffers = this._buffers.slice(this._bufferReadIndex);
                this._bufferReadIndex = 0;
            }
            return data;
        }
        // Queue drained
        if (this._bufferReadIndex !== 0) {
            this._buffers.length = 0;
            this._bufferReadIndex = 0;
        }
        return null;
    }
    /**
     * Add chunk to internal buffers
     */
    _addChunkToBuffers(chunk) {
        let chunkOffset = 0;
        let chunkLength = chunk.length;
        while (chunkLength > 0) {
            if (this._buffers.length === 0 || this._buffers[this._buffers.length - 1].isFull) {
                this._buffers.push(new ReadWriteBuffer(this._batchSize));
            }
            const buffer = this._buffers[this._buffers.length - 1];
            const written = buffer.write(chunk, chunkOffset, chunkLength);
            chunkOffset += written;
            chunkLength -= written;
        }
    }
    /**
     * Get all buffered data as a single Uint8Array
     */
    toUint8Array() {
        // Fast path: no data
        if (this._totalLength === 0) {
            return new Uint8Array(0);
        }
        // Allocate exactly once, then copy remaining chunks/buffers.
        const out = new Uint8Array(this._totalLength);
        let offset = 0;
        for (let i = this._chunkReadIndex; i < this._chunks.length; i++) {
            const chunk = this._chunks[i];
            offset += chunk.copy(out, offset, 0, chunk.length);
        }
        for (let i = this._bufferReadIndex; i < this._buffers.length; i++) {
            const buf = this._buffers[i];
            if (buf.isEOD) {
                continue;
            }
            const view = buf.toUint8Array();
            out.set(view, offset);
            offset += view.length;
        }
        // Defensive: if internal counters drift, avoid returning trailing zeros.
        return offset === out.length ? out : out.subarray(0, offset);
    }
    /**
     * Check if stream is finished
     */
    get isFinished() {
        return this._finished;
    }
    /**
     * Check if stream is destroyed
     */
    get destroyed() {
        return this._destroyed;
    }
}
