"use strict";
/**
 * Pull Stream
 *
 * A stream that allows pulling data from internal buffer with pattern matching.
 * Works identically in both browser and Node.js environments.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.PullStream = void 0;
const event_emitter_1 = require("./event-emitter.js");
const EMPTY_U8 = new Uint8Array(0);
/**
 * Browser-compatible Pull Stream - Read data from buffer on demand with pattern matching
 */
class PullStream extends event_emitter_1.EventEmitter {
    constructor(_options = {}) {
        super();
        // Single growable buffer with read/write cursors.
        // IMPORTANT: never mutate bytes that have already been returned via subarray
        // (to keep views stable). When we need to reclaim prefix space, we allocate
        // a new buffer and copy the remaining bytes.
        this._buffer = new Uint8Array(0);
        this._bufferReadIndex = 0;
        this._bufferWriteIndex = 0;
        this.finished = false;
        this._destroyed = false;
    }
    // Maintain legacy protected accessor for subclasses.
    // Returned value is a view of the readable region.
    get buffer() {
        if (this._bufferReadIndex === this._bufferWriteIndex) {
            return EMPTY_U8;
        }
        return this._buffer.subarray(this._bufferReadIndex, this._bufferWriteIndex);
    }
    set buffer(buf) {
        if (buf.length === 0) {
            this._buffer = EMPTY_U8;
            this._bufferReadIndex = 0;
            this._bufferWriteIndex = 0;
            return;
        }
        this._buffer = buf;
        this._bufferReadIndex = 0;
        this._bufferWriteIndex = buf.length;
    }
    /**
     * Write data to the stream
     */
    write(chunk) {
        if (this._destroyed) {
            this.emit("error", new Error("Cannot write to destroyed stream"));
            return false;
        }
        const chunkLen = chunk.length;
        if (chunkLen === 0) {
            this.emit("chunk");
            return true;
        }
        // Fast path: first write can reuse caller buffer without copy.
        if (this._buffer.length === 0) {
            this._buffer = chunk;
            this._bufferReadIndex = 0;
            this._bufferWriteIndex = chunkLen;
            this.emit("chunk");
            return true;
        }
        const required = this._bufferWriteIndex + chunkLen;
        if (required <= this._buffer.length) {
            this._buffer.set(chunk, this._bufferWriteIndex);
            this._bufferWriteIndex += chunkLen;
            this.emit("chunk");
            return true;
        }
        // Need a new buffer. We keep previously returned views stable by allocating.
        const remaining = this._bufferWriteIndex - this._bufferReadIndex;
        const nextLength = remaining + chunkLen;
        const prevCap = this._buffer.length;
        // Grow exponentially to avoid O(n^2) copying on many small writes.
        const nextCap = Math.max(nextLength, prevCap > 0 ? prevCap * 2 : 1024);
        const next = new Uint8Array(nextCap);
        next.set(this._buffer.subarray(this._bufferReadIndex, this._bufferWriteIndex), 0);
        next.set(chunk, remaining);
        this._buffer = next;
        this._bufferReadIndex = 0;
        this._bufferWriteIndex = nextLength;
        this.emit("chunk");
        return true;
    }
    /**
     * Signal end of input
     */
    end(chunk) {
        if (chunk !== undefined) {
            this.write(chunk);
        }
        this.finished = true;
        this.emit("chunk", false);
        this.emit("finish");
        this.emit("end");
    }
    /**
     * Destroy the stream
     */
    destroy(error) {
        if (this._destroyed) {
            return;
        }
        this._destroyed = true;
        this._buffer = EMPTY_U8;
        this._bufferReadIndex = 0;
        this._bufferWriteIndex = 0;
        if (error) {
            this.emit("error", error);
        }
        this.emit("close");
    }
    pull(sizeOrPattern, includePattern) {
        if (typeof sizeOrPattern === "number") {
            return this._pullSize(sizeOrPattern);
        }
        return this._pullPattern(sizeOrPattern, includePattern ?? false);
    }
    /**
     * Pull until pattern is found (alias for pull(pattern, includePattern))
     */
    pullUntil(pattern, includePattern) {
        return this._pullPattern(pattern, includePattern ?? false);
    }
    _pullSize(size) {
        return new Promise((resolve, reject) => {
            const tryPull = () => {
                if (this._destroyed) {
                    reject(new Error("Stream destroyed"));
                    return;
                }
                if (size === 0) {
                    resolve(this._buffer.subarray(this._bufferReadIndex, this._bufferReadIndex));
                    return;
                }
                const available = this._bufferWriteIndex - this._bufferReadIndex;
                if (available >= size) {
                    const start = this._bufferReadIndex;
                    const end = start + size;
                    const result = this._buffer.subarray(start, end);
                    this._bufferReadIndex = end;
                    if (this._bufferReadIndex === this._bufferWriteIndex) {
                        this._buffer = EMPTY_U8;
                        this._bufferReadIndex = 0;
                        this._bufferWriteIndex = 0;
                    }
                    resolve(result);
                    return;
                }
                if (this.finished) {
                    // Return whatever we have
                    const result = this._bufferReadIndex === this._bufferWriteIndex
                        ? EMPTY_U8
                        : this._buffer.subarray(this._bufferReadIndex, this._bufferWriteIndex);
                    this._buffer = EMPTY_U8;
                    this._bufferReadIndex = 0;
                    this._bufferWriteIndex = 0;
                    resolve(result);
                    return;
                }
                // Wait for more data
                this.once("chunk", tryPull);
            };
            tryPull();
        });
    }
    _pullPattern(pattern, includePattern) {
        return new Promise((resolve, reject) => {
            const tryPull = () => {
                if (this._destroyed) {
                    reject(new Error("Stream destroyed"));
                    return;
                }
                // Match empty pattern without consuming anything.
                if (pattern.length === 0) {
                    this._match = 0;
                    resolve(this._buffer.subarray(this._bufferReadIndex, this._bufferReadIndex));
                    return;
                }
                const matchIndexAbs = this._indexOf(this._buffer, this._bufferReadIndex, this._bufferWriteIndex, pattern);
                if (matchIndexAbs !== -1) {
                    this._match = matchIndexAbs - this._bufferReadIndex;
                    const patternLen = pattern.length;
                    const resultEndAbs = includePattern ? matchIndexAbs + patternLen : matchIndexAbs;
                    const consumeTo = matchIndexAbs + patternLen;
                    const result = this._buffer.subarray(this._bufferReadIndex, resultEndAbs);
                    this._bufferReadIndex = consumeTo;
                    if (this._bufferReadIndex === this._bufferWriteIndex) {
                        this._buffer = EMPTY_U8;
                        this._bufferReadIndex = 0;
                        this._bufferWriteIndex = 0;
                    }
                    resolve(result);
                    return;
                }
                if (this.finished) {
                    // Pattern not found, return everything
                    const result = this._bufferReadIndex === this._bufferWriteIndex
                        ? EMPTY_U8
                        : this._buffer.subarray(this._bufferReadIndex, this._bufferWriteIndex);
                    this._buffer = EMPTY_U8;
                    this._bufferReadIndex = 0;
                    this._bufferWriteIndex = 0;
                    resolve(result);
                    return;
                }
                // Wait for more data
                this.once("chunk", tryPull);
            };
            tryPull();
        });
    }
    /**
     * Get the match position from last pattern match
     */
    get matchPosition() {
        return this._match;
    }
    /**
     * Get remaining buffer length
     */
    get length() {
        return this._bufferWriteIndex - this._bufferReadIndex;
    }
    /**
     * Check if stream is finished
     */
    get isFinished() {
        return this.finished;
    }
    /**
     * Check if stream is destroyed
     */
    get destroyed() {
        return this._destroyed;
    }
    /**
     * Find pattern in Uint8Array (like Buffer.indexOf)
     */
    _indexOf(haystack, start, end, needle) {
        const needleLen = needle.length;
        if (needleLen === 0) {
            return start;
        }
        const haystackLen = end;
        if (needleLen > haystackLen - start) {
            return -1;
        }
        const first = needle[0];
        const last = haystackLen - needleLen;
        outer: for (let i = start; i <= last; i++) {
            // Quick check first byte
            if (haystack[i] !== first) {
                continue;
            }
            // Check rest of pattern
            for (let j = 1; j < needleLen; j++) {
                if (haystack[i + j] !== needle[j]) {
                    continue outer;
                }
            }
            return i;
        }
        return -1;
    }
}
exports.PullStream = PullStream;
