"use strict";
/**
 * WorkbookReader - Browser Streaming Workbook Reader
 *
 * This module contains the full shared implementation for the streaming
 * workbook reader and a browser-compatible `WorkbookReader` that buffers
 * waiting worksheets in memory.
 *
 * Node.js uses `workbook-reader.ts`, which extends the same base implementation
 * with filesystem-specific features (filename input + temp-file buffering).
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkbookReader = exports.WorkbookReaderOptionsSchema = exports.WorkbookReaderBase = void 0;
const _stream_1 = require("../../stream/index.js");
const stream_1 = require("../../archive/unzip/stream.js");
const iterate_stream_1 = require("../utils/iterate-stream.js");
const ooxml_paths_1 = require("../utils/ooxml-paths.js");
const parse_sax_1 = require("../utils/parse-sax.js");
const styles_xform_1 = require("../xlsx/xform/style/styles-xform.js");
const workbook_xform_1 = require("../xlsx/xform/book/workbook-xform.js");
const relationships_xform_1 = require("../xlsx/xform/core/relationships-xform.js");
const worksheet_reader_1 = require("./worksheet-reader.js");
const hyperlink_reader_1 = require("./hyperlink-reader.js");
// ============================================================================
// Base Class
// ============================================================================
class WorkbookReaderBase extends _stream_1.EventEmitter {
    getHyperlinkReader(sheetNo) {
        return this._hyperlinkReadersBySheetNo?.[String(sheetNo)];
    }
    getHyperlink(sheetNo, rId) {
        return this.getHyperlinkReader(sheetNo)?.hyperlinks?.[rId];
    }
    getHyperlinkTarget(sheetNo, rId) {
        return this.getHyperlink(sheetNo, rId)?.target;
    }
    constructor(input, options, WorksheetReaderClass, HyperlinkReaderClass) {
        super();
        this.input = input;
        this.WorksheetReaderClass = WorksheetReaderClass;
        this.HyperlinkReaderClass = HyperlinkReaderClass;
        this.options = {
            worksheets: "emit",
            sharedStrings: "cache",
            hyperlinks: "ignore",
            styles: "ignore",
            entries: "ignore",
            ...options
        };
        this.styles = new styles_xform_1.StylesXform();
        this.styles.init();
    }
    // Default implementation for CommonInput types
    _getStream(input) {
        if (input instanceof _stream_1.Readable) {
            return input;
        }
        // Accept Web ReadableStream (browser fetch() body, Node 18+ fetch(), etc.)
        if (input &&
            typeof input === "object" &&
            typeof input.getReader === "function") {
            const fromWeb = _stream_1.Readable.fromWeb;
            if (typeof fromWeb === "function") {
                return fromWeb(input);
            }
            // Browser wrapper supports `{ stream }` constructor option.
            // Node's Readable does not, so this is best-effort.
            try {
                return new _stream_1.Readable({
                    stream: input
                });
            }
            catch {
                throw new Error("Could not recognise input: ReadableStream");
            }
        }
        let data = input;
        if (data instanceof ArrayBuffer) {
            data = new Uint8Array(data);
        }
        if (data instanceof Uint8Array) {
            // Cross-platform: both Node's Readable and our browser Readable implement `.from()`.
            return _stream_1.Readable.from([data]);
        }
        throw new Error(`Could not recognise input: ${input}`);
    }
    _cleanupWaitingWorksheets(_waitingWorksheets) {
        // Default: attempt best-effort cleanup if the stored object provides it.
        for (const ws of _waitingWorksheets) {
            if (ws && typeof ws.cleanup === "function") {
                ws.cleanup();
            }
        }
    }
    // Unified implementations using passed-in classes
    _createWorksheetReader(params) {
        return new this.WorksheetReaderClass({
            workbook: this,
            id: params.id,
            iterator: params.iterator,
            options: params.options
        });
    }
    _createHyperlinkReader(params) {
        return new this.HyperlinkReaderClass({
            workbook: this,
            id: params.id,
            iterator: params.iterator,
            options: params.options
        });
    }
    async read(input, options) {
        try {
            for await (const { eventType, value } of this.parse(input, options)) {
                switch (eventType) {
                    case "shared-strings":
                        this.emit(eventType, value);
                        break;
                    case "worksheet":
                        this.emit(eventType, value);
                        await value.read();
                        break;
                    case "hyperlinks":
                        this.emit(eventType, value);
                        break;
                }
            }
            this.emit("end");
            this.emit("finished");
        }
        catch (error) {
            this.emit("error", error);
        }
    }
    async *[Symbol.asyncIterator]() {
        for await (const { eventType, value } of this.parse()) {
            if (eventType === "worksheet") {
                yield value;
            }
        }
    }
    async *parse(input, options) {
        if (options) {
            this.options = options;
        }
        const stream = (this.stream = this._getStream(input || this.input));
        const waitingWorksheets = [];
        try {
            for await (const event of this._parseEntries(stream)) {
                if (event.eventType === "waiting-worksheet") {
                    const stored = await this._storeWaitingWorksheet(event.sheetNo, event.entry);
                    waitingWorksheets.push(stored);
                }
                else {
                    yield event;
                }
            }
            yield* this._processWaitingWorksheets(waitingWorksheets);
        }
        catch (error) {
            this._cleanupWaitingWorksheets(waitingWorksheets);
            throw error;
        }
    }
    _emitEntry(payload) {
        if (this.options.entries === "emit") {
            this.emit("entry", payload);
        }
    }
    async _parseRels(entry) {
        const xform = new relationships_xform_1.RelationshipsXform();
        this.workbookRels = await xform.parseStream((0, iterate_stream_1.iterateStream)(entry));
        // Build fast lookup for worksheet relationship ids.
        this._workbookRelIdByTarget = Object.create(null);
        for (const rel of this.workbookRels || []) {
            if (rel?.Target && rel?.Id) {
                this._workbookRelIdByTarget[rel.Target] = rel.Id;
            }
        }
    }
    async _parseWorkbook(entry) {
        this._emitEntry({ type: "workbook" });
        const workbook = new workbook_xform_1.WorkbookXform();
        this.model = await workbook.parseStream((0, iterate_stream_1.iterateStream)(entry));
        this.properties = workbook.map?.workbookPr;
        // Build fast lookup for sheet metadata by relationship id.
        this._sheetByRelId = Object.create(null);
        for (const sheet of this.model?.sheets || []) {
            this._sheetByRelId[sheet.rId] = sheet;
        }
    }
    async *_parseSharedStrings(entry) {
        this._emitEntry({ type: "shared-strings" });
        switch (this.options.sharedStrings) {
            case "cache":
                this.sharedStrings = [];
                break;
            case "emit":
                break;
            default:
                return;
        }
        let text = null;
        let richText = [];
        let index = 0;
        let font = null;
        let inRichText = false;
        for await (const events of (0, parse_sax_1.parseSax)((0, iterate_stream_1.iterateStream)(entry))) {
            for (const { eventType, value } of events) {
                if (eventType === "opentag") {
                    const node = value;
                    switch (node.name) {
                        case "b":
                            font = font || {};
                            font.bold = true;
                            break;
                        case "charset":
                            font = font || {};
                            font.charset = parseInt(node.attributes.charset, 10);
                            break;
                        case "color":
                            font = font || {};
                            font.color = {};
                            if (node.attributes.rgb) {
                                font.color.argb = node.attributes.rgb;
                            }
                            if (node.attributes.val) {
                                font.color.argb = node.attributes.val;
                            }
                            if (node.attributes.theme) {
                                font.color.theme = node.attributes.theme;
                            }
                            break;
                        case "family":
                            font = font || {};
                            font.family = parseInt(node.attributes.val, 10);
                            break;
                        case "i":
                            font = font || {};
                            font.italic = true;
                            break;
                        case "outline":
                            font = font || {};
                            font.outline = true;
                            break;
                        case "rFont":
                            font = font || {};
                            font.name = node.attributes.val;
                            break;
                        case "r":
                            inRichText = true;
                            break;
                        case "si":
                            font = null;
                            richText = [];
                            text = null;
                            inRichText = false;
                            break;
                        case "sz":
                            font = font || {};
                            font.size = parseInt(node.attributes.val, 10);
                            break;
                        case "strike":
                            font = font || {};
                            font.strike = true;
                            break;
                        case "t":
                            text = null;
                            break;
                        case "u":
                            font = font || {};
                            font.underline = true;
                            break;
                        case "vertAlign":
                            font = font || {};
                            font.vertAlign = node.attributes.val;
                            break;
                    }
                }
                else if (eventType === "text") {
                    text = text ? text + value : value;
                }
                else if (eventType === "closetag") {
                    const node = value;
                    switch (node.name) {
                        case "r":
                            if (inRichText) {
                                richText.push({ font, text });
                                font = null;
                                text = null;
                            }
                            break;
                        case "si":
                            if (this.options.sharedStrings === "cache") {
                                this.sharedStrings.push(richText.length ? { richText } : text || "");
                            }
                            else if (this.options.sharedStrings === "emit") {
                                yield { index: index++, text: richText.length ? { richText } : text || "" };
                            }
                            richText = [];
                            font = null;
                            text = null;
                            inRichText = false;
                            break;
                    }
                }
            }
        }
    }
    async _parseStyles(entry) {
        this._emitEntry({ type: "styles" });
        if (this.options.styles === "cache") {
            this.styles = new styles_xform_1.StylesXform();
            await this.styles.parseStream((0, iterate_stream_1.iterateStream)(entry));
        }
    }
    *_parseWorksheet(iterator, sheetNo) {
        this._emitEntry({ type: "worksheet", id: sheetNo });
        const sheetNoNumber = parseInt(sheetNo, 10);
        const worksheetReader = this._createWorksheetReader({
            id: sheetNoNumber,
            iterator,
            options: this.options
        });
        // Preserve original sheet index from the zip path. `worksheetReader.id` may be remapped
        // later using workbook metadata.
        worksheetReader.sheetNo = sheetNoNumber;
        const relId = this._workbookRelIdByTarget?.[(0, ooxml_paths_1.worksheetRelTarget)(sheetNo)];
        const matchingSheet = relId ? this._sheetByRelId?.[relId] : undefined;
        if (matchingSheet) {
            worksheetReader.id = matchingSheet.id;
            worksheetReader.name = matchingSheet.name;
            worksheetReader.state = matchingSheet.state;
        }
        if (this.options.worksheets === "emit") {
            yield { eventType: "worksheet", value: worksheetReader };
        }
    }
    async *_parseHyperlinks(iterator, sheetNo) {
        this._emitEntry({ type: "hyperlinks", id: sheetNo });
        const hyperlinksReader = this._createHyperlinkReader({
            id: parseInt(sheetNo, 10),
            iterator,
            options: this.options
        });
        if (this.options.hyperlinks === "cache") {
            if (!this._hyperlinkReadersBySheetNo) {
                this._hyperlinkReadersBySheetNo = Object.create(null);
            }
            this._hyperlinkReadersBySheetNo[sheetNo] = hyperlinksReader;
            const readFn = hyperlinksReader.read;
            if (typeof readFn === "function") {
                await readFn.call(hyperlinksReader);
            }
            return;
        }
        if (this.options.hyperlinks === "emit") {
            yield { eventType: "hyperlinks", value: hyperlinksReader };
        }
    }
    async *_parseEntries(stream) {
        const zip = (0, stream_1.createParse)({ forceStream: true });
        stream.on("error", (err) => zip.emit("error", err));
        stream.pipe(zip);
        for await (const entry of (0, iterate_stream_1.iterateStream)(zip)) {
            let sheetNo;
            const normalizedPath = (0, ooxml_paths_1.normalizeZipPath)(entry.path);
            switch (normalizedPath) {
                case ooxml_paths_1.OOXML_PATHS.rootRels:
                    break;
                case ooxml_paths_1.OOXML_PATHS.xlWorkbookRels:
                    await this._parseRels(entry);
                    break;
                case ooxml_paths_1.OOXML_PATHS.xlWorkbook:
                    await this._parseWorkbook(entry);
                    break;
                case ooxml_paths_1.OOXML_PATHS.xlSharedStrings:
                    for await (const item of this._parseSharedStrings(entry)) {
                        yield { eventType: "shared-strings", value: item };
                    }
                    break;
                case ooxml_paths_1.OOXML_PATHS.xlStyles:
                    await this._parseStyles(entry);
                    break;
                default:
                    sheetNo = (0, ooxml_paths_1.getWorksheetNoFromWorksheetPath)(normalizedPath)?.toString();
                    if (sheetNo) {
                        // Performance: only wait for sharedStrings when they are actually needed.
                        const hasPrerequisites = !!this.workbookRels &&
                            (this.options.sharedStrings !== "cache" || !!this.sharedStrings);
                        if (hasPrerequisites) {
                            yield* this._parseWorksheet((0, iterate_stream_1.iterateStream)(entry), sheetNo);
                            continue;
                        }
                        else {
                            yield { eventType: "waiting-worksheet", sheetNo, entry };
                            continue;
                        }
                    }
                    sheetNo = (0, ooxml_paths_1.getWorksheetNoFromWorksheetRelsPath)(normalizedPath)?.toString();
                    if (sheetNo) {
                        yield* this._parseHyperlinks((0, iterate_stream_1.iterateStream)(entry), sheetNo);
                        continue;
                    }
                    break;
            }
            entry.autodrain();
        }
    }
}
exports.WorkbookReaderBase = WorkbookReaderBase;
exports.WorkbookReaderOptionsSchema = {
    worksheets: ["emit", "ignore"],
    sharedStrings: ["cache", "emit", "ignore"],
    hyperlinks: ["cache", "emit", "ignore"],
    styles: ["cache", "ignore"],
    entries: ["emit", "ignore"]
};
class WorkbookReader extends WorkbookReaderBase {
    constructor(input, options = {}) {
        super(input, options, worksheet_reader_1.WorksheetReader, hyperlink_reader_1.HyperlinkReader);
    }
    async _storeWaitingWorksheet(sheetNo, entry) {
        const chunks = [];
        const encoder = new TextEncoder();
        for await (const chunk of (0, iterate_stream_1.iterateStream)(entry)) {
            if (chunk instanceof Uint8Array) {
                chunks.push(chunk);
            }
            else if (typeof chunk === "string") {
                chunks.push(encoder.encode(chunk));
            }
        }
        return { sheetNo, data: chunks };
    }
    async *_processWaitingWorksheets(waitingWorksheets) {
        for (const ws of waitingWorksheets) {
            const iterator = (async function* () {
                for (const chunk of ws.data) {
                    yield chunk;
                }
            })();
            yield* this._parseWorksheet(iterator, ws.sheetNo);
        }
    }
}
exports.WorkbookReader = WorkbookReader;
