/**
 * XLSX - Abstract base class for XLSX operations
 *
 * Contains all platform-agnostic logic shared between Node.js and Browser versions:
 * - reconcile: Reconcile model after parsing
 * - _process*Entry: Process individual ZIP entries
 * - add*: Add content to ZIP during writing
 * - prepareModel: Prepare model for writing
 * - loadFromFiles: Load from pre-extracted ZIP data
 */
import { XmlStream } from "../utils/xml-stream.js";
import { StylesXform } from "./xform/style/styles-xform.js";
import { CoreXform } from "./xform/core/core-xform.js";
import { SharedStringsXform } from "./xform/strings/shared-strings-xform.js";
import { RelationshipsXform } from "./xform/core/relationships-xform.js";
import { ContentTypesXform } from "./xform/core/content-types-xform.js";
import { AppXform } from "./xform/core/app-xform.js";
import { WorkbookXform } from "./xform/book/workbook-xform.js";
import { WorkSheetXform } from "./xform/sheet/worksheet-xform.js";
import { FeaturePropertyBagXform } from "./xform/core/feature-property-bag-xform.js";
import { DrawingXform } from "./xform/drawing/drawing-xform.js";
import { TableXform } from "./xform/table/table-xform.js";
import { PivotCacheRecordsXform } from "./xform/pivot-table/pivot-cache-records-xform.js";
import { PivotCacheDefinitionXform } from "./xform/pivot-table/pivot-cache-definition-xform.js";
import { PivotTableXform } from "./xform/pivot-table/pivot-table-xform.js";
import { CommentsXform } from "./xform/comment/comments-xform.js";
import { VmlDrawingXform } from "./xform/drawing/vml-drawing-xform.js";
import { CtrlPropXform } from "./xform/drawing/ctrl-prop-xform.js";
import { theme1Xml } from "./xml/theme1.js";
import { RelType } from "./rel-type.js";
import { StreamBuf } from "../utils/stream-buf.js";
import { bufferToString, base64ToUint8Array } from "../../../utils/utils.js";
import { StreamingZip, ZipDeflateFile } from "../../archive/zip/stream.js";
import { ZipParser } from "../../archive/unzip/zip-parser.js";
import { PassThrough, concatUint8Arrays } from "../../stream/index.js";
import { commentsPath, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, worksheetPath, worksheetRelsPath, worksheetRelTarget } from "../utils/ooxml-paths.js";
import { PassthroughManager } from "../utils/passthrough-manager.js";
class StreamingZipWriterAdapter {
    constructor(options) {
        this.events = new Map();
        this.pipedStream = null;
        this.finalized = false;
        this.level = options?.level ?? 6;
        this.modTime = options?.modTime;
        this.timestamps = options?.timestamps;
        this.zip = new StreamingZip((err, data, final) => {
            if (err) {
                this._emit("error", err);
                return;
            }
            if (data && data.length > 0) {
                this._emit("data", data);
                if (this.pipedStream) {
                    this.pipedStream.write(data);
                }
            }
            if (final) {
                if (this.pipedStream) {
                    this.pipedStream.end();
                }
                this._emit("finish");
            }
        });
    }
    _emit(event, ...args) {
        const callbacks = this.events.get(event);
        if (!callbacks) {
            return;
        }
        for (const cb of callbacks) {
            cb(...args);
        }
    }
    on(event, callback) {
        const callbacks = this.events.get(event) || new Set();
        callbacks.add(callback);
        this.events.set(event, callbacks);
        return this;
    }
    once(event, callback) {
        const wrapped = (...args) => {
            this.off(event, wrapped);
            callback(...args);
        };
        return this.on(event, wrapped);
    }
    off(event, callback) {
        const callbacks = this.events.get(event);
        if (!callbacks) {
            return this;
        }
        callbacks.delete(callback);
        if (callbacks.size === 0) {
            this.events.delete(event);
        }
        return this;
    }
    pipe(stream) {
        this.pipedStream = stream;
    }
    append(data, options) {
        if (this.finalized) {
            throw new Error("Cannot append after finalize");
        }
        let buffer;
        if (options.base64) {
            buffer = base64ToUint8Array(typeof data === "string" ? data : String(data));
        }
        else if (typeof data === "string") {
            buffer = StreamingZipWriterAdapter.textEncoder.encode(data);
        }
        else if (data instanceof Uint8Array) {
            buffer = data;
        }
        else if (ArrayBuffer.isView(data)) {
            buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
        }
        else if (data instanceof ArrayBuffer) {
            buffer = new Uint8Array(data);
        }
        else {
            buffer = data;
        }
        const file = new ZipDeflateFile(options.name, {
            level: this.level,
            modTime: this.modTime,
            timestamps: this.timestamps
        });
        this.zip.add(file);
        file.push(buffer, true);
    }
    finalize() {
        if (this.finalized) {
            return;
        }
        this.finalized = true;
        this.zip.end();
    }
}
StreamingZipWriterAdapter.textEncoder = new TextEncoder();
/**
 * XLSX class - handles Excel file operations
 * Works in both Node.js and Browser environments
 */
class XLSX {
    constructor(workbook) {
        this.workbook = workbook;
    }
    // ===========================================================================
    // Stream creation - cross-platform implementation using modules/stream
    // ===========================================================================
    /**
     * Create a stream from binary data (for media/themes)
     */
    createBinaryStream(data) {
        const stream = new PassThrough();
        stream.end(data);
        return stream;
    }
    /**
     * Create a stream from string content (for XML parsing)
     */
    createTextStream(content) {
        const stream = new PassThrough();
        stream.end(content);
        return stream;
    }
    // ===========================================================================
    // Shared implementations - used by all platforms
    // ===========================================================================
    /**
     * Create a StreamBuf instance for buffering data
     */
    createStreamBuf() {
        return new StreamBuf();
    }
    /**
     * Convert buffer/Uint8Array to string
     */
    bufferToString(data) {
        return bufferToString(data);
    }
    /**
     * Create a ZIP writer adapter.
     * Can be overridden by subclasses for platform-specific implementations.
     */
    createZipWriter(options) {
        return new StreamingZipWriterAdapter(options);
    }
    /**
     * Write all workbook content to a ZIP writer
     * Shared by both Node.js write() and browser writeBuffer()
     */
    async writeToZip(zip, options) {
        const { model } = this.workbook;
        this.prepareModel(model, options);
        await this.addContentTypes(zip, model);
        await this.addOfficeRels(zip, model);
        await this.addWorkbookRels(zip, model);
        await this.addWorksheets(zip, model);
        await this.addSharedStrings(zip, model);
        this.addDrawings(zip, model);
        this.addTables(zip, model);
        this.addPivotTables(zip, model);
        this.addPassthrough(zip, model);
        await Promise.all([this.addThemes(zip, model), this.addStyles(zip, model)]);
        await this.addFeaturePropertyBag(zip, model);
        await this.addMedia(zip, model);
        await Promise.all([this.addApp(zip, model), this.addCore(zip, model)]);
        await this.addWorkbook(zip, model);
    }
    // ===========================================================================
    // Stream/Buffer operations - shared by all platforms
    // ===========================================================================
    /**
     * Read workbook from a stream
     */
    async read(stream, options) {
        // Collect all stream data into a single buffer
        const chunks = [];
        await new Promise((resolve, reject) => {
            const onData = (chunk) => {
                chunks.push(chunk);
            };
            const onEnd = () => {
                stream.off("data", onData);
                stream.off("end", onEnd);
                stream.off("error", onError);
                resolve();
            };
            const onError = (err) => {
                stream.off("data", onData);
                stream.off("end", onEnd);
                stream.off("error", onError);
                reject(err);
            };
            stream.on("data", onData);
            stream.on("end", onEnd);
            stream.on("error", onError);
        });
        return this.loadBuffer(concatUint8Arrays(chunks), options);
    }
    /**
     * Write workbook to a stream
     */
    async write(stream, options) {
        var _a;
        options = options || {};
        options.zip = options.zip || {};
        (_a = options.zip).modTime ?? (_a.modTime = this.workbook.modified ?? this.workbook.created);
        const zip = this.createZipWriter(options.zip);
        zip.pipe(stream);
        await this.writeToZip(zip, options);
        return this._finalize(zip);
    }
    /**
     * Load workbook from buffer/ArrayBuffer/Uint8Array
     */
    async load(data, options) {
        let buffer;
        // Validate input
        const isBuffer = typeof Buffer !== "undefined" ? Buffer.isBuffer(data) : false;
        if (!data ||
            (typeof data === "object" &&
                !isBuffer &&
                !(data instanceof Uint8Array) &&
                !(data instanceof ArrayBuffer))) {
            throw new Error("Can't read the data of 'the loaded zip file'. Is it in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?");
        }
        // Handle base64 input
        if (options && options.base64) {
            buffer = base64ToUint8Array(data.toString());
        }
        else if (data instanceof ArrayBuffer) {
            buffer = new Uint8Array(data);
        }
        else if (data instanceof Uint8Array) {
            buffer = data;
        }
        else {
            // Node.js Buffer or other array-like
            buffer = new Uint8Array(data);
        }
        return this.loadBuffer(buffer, options);
    }
    /**
     * Internal: Load from Uint8Array buffer
     */
    async loadBuffer(buffer, options) {
        const parser = new ZipParser(buffer);
        const filesMap = await parser.extractAll();
        // Convert Map to Record for loadFromFiles
        const allFiles = {};
        for (const [path, content] of filesMap) {
            allFiles[path] = content;
        }
        return this.loadFromFiles(allFiles, options);
    }
    /**
     * Internal: Load workbook from an async stream of ZIP entries.
     *
     * This is the foundation for TRUE streaming reads on platforms that have a
     * streaming ZIP parser (e.g. Node.js `modules/archive` Parse).
     */
    /**
     * Create an empty model for parsing XLSX files.
     * Shared by loadFromZipEntries and loadFromFiles.
     */
    createEmptyModel() {
        return {
            worksheets: [],
            worksheetHash: {},
            worksheetRels: [],
            themes: {},
            media: [],
            mediaIndex: {},
            drawings: {},
            drawingRels: {},
            // Raw drawing XML data for passthrough (when drawing contains chart references)
            rawDrawings: {},
            comments: {},
            tables: {},
            vmlDrawings: {},
            pivotTables: {},
            pivotTableRels: {},
            pivotCacheDefinitions: {},
            pivotCacheRecords: {},
            // Passthrough storage for unknown/unsupported files (charts, etc.)
            passthrough: {}
        };
    }
    /**
     * Collect all data from a stream into a single Uint8Array.
     * Reusable helper for passthrough and drawing processing.
     */
    async collectStreamData(stream) {
        const chunks = [];
        await new Promise((resolve, reject) => {
            stream.on("data", (chunk) => {
                if (typeof chunk === "string") {
                    chunks.push(new TextEncoder().encode(chunk));
                }
                else if (chunk instanceof Uint8Array) {
                    chunks.push(chunk);
                }
                else {
                    chunks.push(new Uint8Array(chunk));
                }
            });
            stream.on("end", () => resolve());
            stream.on("error", reject);
        });
        return concatUint8Arrays(chunks);
    }
    /**
     * Check if a drawing has chart references in its relationships
     */
    drawingHasChartReference(drawing) {
        return (drawing.rels && drawing.rels.some((rel) => rel.Target && rel.Target.includes("/charts/")));
    }
    /**
     * Check if a drawing rels list references charts.
     * Used to decide whether we need to keep raw drawing XML for passthrough.
     */
    drawingRelsHasChartReference(drawingRels) {
        return (Array.isArray(drawingRels) &&
            drawingRels.some(rel => typeof rel?.Target === "string" && rel.Target.includes("/charts/")));
    }
    /**
     * Process a known OOXML entry (workbook, styles, shared strings, etc.)
     * Returns true if handled, false if should be passed to _processDefaultEntry
     */
    async _processKnownEntry(stream, model, entryName, options) {
        const sheetNo = getWorksheetNoFromWorksheetPath(entryName);
        if (sheetNo !== undefined) {
            await this._processWorksheetEntry(stream, model, sheetNo, options, entryName);
            return true;
        }
        switch (entryName) {
            case OOXML_PATHS.rootRels:
                model.globalRels = await this.parseRels(stream);
                return true;
            case OOXML_PATHS.xlWorkbook: {
                const workbook = await this.parseWorkbook(stream);
                model.sheets = workbook.sheets;
                model.definedNames = workbook.definedNames;
                model.views = workbook.views;
                model.properties = workbook.properties;
                model.calcProperties = workbook.calcProperties;
                model.pivotCaches = workbook.pivotCaches;
                return true;
            }
            case OOXML_PATHS.xlSharedStrings:
                model.sharedStrings = new SharedStringsXform();
                await model.sharedStrings.parseStream(stream);
                return true;
            case OOXML_PATHS.xlWorkbookRels:
                model.workbookRels = await this.parseRels(stream);
                return true;
            case OOXML_PATHS.docPropsApp: {
                const appXform = new AppXform();
                const appProperties = await appXform.parseStream(stream);
                if (appProperties) {
                    model.company = appProperties.company;
                    model.manager = appProperties.manager;
                }
                return true;
            }
            case OOXML_PATHS.docPropsCore: {
                const coreXform = new CoreXform();
                const coreProperties = await coreXform.parseStream(stream);
                Object.assign(model, coreProperties);
                return true;
            }
            case OOXML_PATHS.xlStyles:
                model.styles = new StylesXform();
                await model.styles.parseStream(stream);
                return true;
            default:
                return false;
        }
    }
    async loadFromZipEntries(entries, options) {
        const model = this.createEmptyModel();
        for await (const entry of entries) {
            let drained = false;
            const drainEntry = async () => {
                if (drained) {
                    return;
                }
                drained = true;
                await entry.drain();
            };
            if (entry.type === "Directory") {
                await drainEntry();
                continue;
            }
            const entryName = normalizeZipPath(entry.name);
            const stream = entry.stream;
            try {
                const handled = await this._processKnownEntry(stream, model, entryName, options);
                if (!handled) {
                    const defaultHandled = await this._processDefaultEntry(stream, model, entryName);
                    if (!defaultHandled) {
                        // Important for true streaming parsers: always consume unknown entries
                        await drainEntry();
                    }
                }
            }
            finally {
                // Make sure we don't leave the entry stream partially consumed.
                // This is critical for true streaming parsers which may otherwise abort
                // the underlying entry stream (showing up as AbortError/ABORT_ERR).
                try {
                    await drainEntry();
                }
                catch {
                    // ignore drain errors; the primary parse error (if any) is more useful
                }
            }
        }
        this.reconcile(model, options);
        this.workbook.model = model;
        return this.workbook;
    }
    /**
     * Write workbook to buffer
     */
    async writeBuffer(options) {
        var _a;
        options = options || {};
        options.zip = options.zip || {};
        (_a = options.zip).modTime ?? (_a.modTime = this.workbook.modified ?? this.workbook.created);
        const zip = this.createZipWriter(options.zip);
        const stream = this.createStreamBuf();
        zip.pipe(stream);
        await this.writeToZip(zip, options);
        await this._finalize(zip);
        return stream.read() || new Uint8Array(0);
    }
    // ===========================================================================
    // Media handling - base implementation (buffer/base64 only)
    // ===========================================================================
    /**
     * Add media files to ZIP
     * Supports buffer, base64, and filename (if readFileAsync is provided)
     */
    async addMedia(zip, model) {
        await Promise.all(model.media.map(async (medium) => {
            if (medium.type !== "image") {
                throw new Error("Unsupported media");
            }
            // Preserve legacy behavior: `${undefined}` becomes "undefined" in template strings
            const mediaName = medium.name ?? "undefined";
            const filename = mediaPath(`${mediaName}.${medium.extension}`);
            if (medium.filename) {
                if (this.readFileAsync) {
                    const data = await this.readFileAsync(medium.filename);
                    return zip.append(data, { name: filename });
                }
                throw new Error("Loading images from filename is not supported in this environment");
            }
            if (medium.buffer) {
                return zip.append(medium.buffer, { name: filename });
            }
            if (medium.base64) {
                const content = medium.base64.substring(medium.base64.indexOf(",") + 1);
                return zip.append(content, { name: filename, base64: true });
            }
            throw new Error("Unsupported media");
        }));
    }
    // ===========================================================================
    // Parse helpers - shared by all platforms
    // ===========================================================================
    parseRels(stream) {
        const xform = new RelationshipsXform();
        return xform.parseStream(stream);
    }
    parseWorkbook(stream) {
        const xform = new WorkbookXform();
        return xform.parseStream(stream);
    }
    parseSharedStrings(stream) {
        const xform = new SharedStringsXform();
        return xform.parseStream(stream);
    }
    // ===========================================================================
    // Reconcile - shared by all platforms
    // ===========================================================================
    reconcile(model, options) {
        const workbookXform = new WorkbookXform();
        const worksheetXform = new WorkSheetXform(options);
        const drawingXform = new DrawingXform();
        const tableXform = new TableXform();
        workbookXform.reconcile(model);
        // reconcile drawings with their rels
        const drawingOptions = {
            media: model.media,
            mediaIndex: model.mediaIndex
        };
        Object.keys(model.drawings).forEach(name => {
            const drawing = model.drawings[name];
            const drawingRel = model.drawingRels[name];
            if (drawingRel) {
                drawingOptions.rels = drawingRel.reduce((o, rel) => {
                    o[rel.Id] = rel;
                    return o;
                }, {});
                (drawing.anchors ?? []).forEach((anchor) => {
                    const hyperlinks = anchor.picture && anchor.picture.hyperlinks;
                    if (hyperlinks && drawingOptions.rels[hyperlinks.rId]) {
                        hyperlinks.hyperlink = drawingOptions.rels[hyperlinks.rId].Target;
                        delete hyperlinks.rId;
                    }
                });
                drawingXform.reconcile(drawing, drawingOptions);
            }
        });
        // Trim raw drawings for non-chart drawings to avoid bloating the serialized workbook model.
        if (model.rawDrawings && model.drawingRels) {
            for (const name of Object.keys(model.rawDrawings)) {
                const drawingRel = model.drawingRels[name];
                if (drawingRel && !this.drawingRelsHasChartReference(drawingRel)) {
                    delete model.rawDrawings[name];
                }
            }
        }
        // reconcile tables with the default styles
        const tableOptions = {
            styles: model.styles
        };
        Object.values(model.tables).forEach((table) => {
            tableXform.reconcile(table, tableOptions);
        });
        // Reconcile pivot tables
        this._reconcilePivotTables(model);
        const sheetOptions = {
            styles: model.styles,
            sharedStrings: model.sharedStrings,
            media: model.media,
            mediaIndex: model.mediaIndex,
            date1904: model.properties && model.properties.date1904,
            drawings: model.drawings,
            drawingRels: model.drawingRels,
            comments: model.comments,
            tables: model.tables,
            vmlDrawings: model.vmlDrawings,
            pivotTables: model.pivotTablesIndexed
        };
        model.worksheets.forEach((worksheet) => {
            worksheet.relationships = model.worksheetRels[worksheet.sheetNo];
            worksheetXform.reconcile(worksheet, sheetOptions);
        });
        // delete unnecessary parts
        delete model.worksheetHash;
        delete model.worksheetRels;
        delete model.globalRels;
        delete model.sharedStrings;
        delete model.workbookRels;
        delete model.sheetDefs;
        // Preserve default font before deleting styles
        model.defaultFont = model.styles?.defaultFont;
        delete model.styles;
        delete model.mediaIndex;
        delete model.drawings;
        delete model.drawingRels;
        delete model.vmlDrawings;
        delete model.pivotTableRels;
    }
    /**
     * Reconcile pivot tables by linking them to worksheets and their cache data.
     */
    _reconcilePivotTables(model) {
        const rawPivotTables = (model.pivotTables || {});
        if (typeof rawPivotTables !== "object" || Object.keys(rawPivotTables).length === 0) {
            model.pivotTables = [];
            model.pivotTablesIndexed = {};
            return;
        }
        const definitionToCacheId = this._buildDefinitionToCacheIdMap(model);
        const cacheMap = new Map();
        Object.entries(model.pivotCacheDefinitions || {}).forEach(([name, definition]) => {
            const cacheId = definitionToCacheId.get(name);
            if (cacheId !== undefined) {
                const recordsName = name.replace("Definition", "Records");
                cacheMap.set(cacheId, {
                    definition,
                    records: model.pivotCacheRecords?.[recordsName],
                    definitionName: name
                });
            }
        });
        const loadedPivotTables = [];
        const pivotTablesIndexed = {};
        Object.entries(rawPivotTables).forEach(([pivotName, pt]) => {
            const tableNumber = this._extractTableNumber(pivotName);
            const cacheData = cacheMap.get(pt.cacheId);
            const defaultMetric = this._determineMetric(pt.dataFields);
            const completePivotTable = {
                ...pt,
                tableNumber,
                cacheId: String(pt.cacheId),
                cacheDefinition: cacheData?.definition,
                cacheRecords: cacheData?.records,
                cacheFields: cacheData?.definition?.cacheFields ?? [],
                rows: pt.rowFields.filter(f => f >= 0),
                columns: pt.colFields.filter(f => f >= 0 && f !== -2),
                values: pt.dataFields.map(df => df.fld),
                pages: pt.pageFields.map(pf => pf.fld),
                metric: defaultMetric,
                valueMetrics: this._determineValueMetrics(pt.dataFields, defaultMetric),
                applyWidthHeightFormats: pt.applyWidthHeightFormats === "1" ? "1" : "0"
            };
            loadedPivotTables.push(completePivotTable);
            // Key format (e.g., "../pivotTables/pivotTable1.xml") matches worksheet .rels Target values,
            // allowing worksheet reconciliation to look up pivot tables by their relationship target path.
            pivotTablesIndexed[pivotTableRelTargetFromWorksheetName(pivotName)] = completePivotTable;
        });
        loadedPivotTables.sort((a, b) => a.tableNumber - b.tableNumber);
        model.pivotTables = loadedPivotTables;
        model.pivotTablesIndexed = pivotTablesIndexed;
    }
    _extractTableNumber(name) {
        const match = name.match(/pivotTable(\d+)/);
        return match ? parseInt(match[1], 10) : 1;
    }
    _buildCacheIdMap(model) {
        const rIdToCacheId = new Map();
        const pivotCaches = model.pivotCaches ?? [];
        for (const cache of pivotCaches) {
            if (cache.cacheId && cache.rId) {
                rIdToCacheId.set(cache.rId, parseInt(cache.cacheId, 10));
            }
        }
        return rIdToCacheId;
    }
    _buildDefinitionToCacheIdMap(model) {
        const definitionToCacheId = new Map();
        const rIdToCacheId = this._buildCacheIdMap(model);
        const workbookRels = model.workbookRels ?? [];
        for (const rel of workbookRels) {
            if (rel.Type === XLSX.RelType.PivotCacheDefinition && rel.Target) {
                const match = rel.Target.match(/pivotCacheDefinition(\d+)\.xml/);
                if (match) {
                    const defName = `pivotCacheDefinition${match[1]}`;
                    const cacheId = rIdToCacheId.get(rel.Id);
                    if (cacheId !== undefined) {
                        definitionToCacheId.set(defName, cacheId);
                    }
                }
            }
        }
        return definitionToCacheId;
    }
    _determineMetric(dataFields) {
        if (dataFields.length > 0 && dataFields[0].subtotal) {
            return dataFields[0].subtotal;
        }
        return "sum";
    }
    _determineValueMetrics(dataFields, defaultMetric) {
        return dataFields.map(df => df.subtotal || defaultMetric);
    }
    // ===========================================================================
    // Process Entry methods - shared by all platforms
    // ===========================================================================
    async _processWorksheetEntry(stream, model, sheetNo, options, path) {
        const xform = new WorkSheetXform(options);
        const worksheet = await xform.parseStream(stream);
        if (!worksheet) {
            throw new Error(`Failed to parse worksheet ${path}`);
        }
        worksheet.sheetNo = sheetNo;
        model.worksheetHash[path] = worksheet;
        model.worksheets.push(worksheet);
    }
    async _processCommentEntry(stream, model, name) {
        const xform = new CommentsXform();
        const comments = await xform.parseStream(stream);
        model.comments[commentsRelTargetFromWorksheetName(name)] = comments;
    }
    async _processTableEntry(stream, model, name) {
        const xform = new TableXform();
        const table = await xform.parseStream(stream);
        model.tables[tableRelTargetFromWorksheetName(name)] = table;
    }
    async _processWorksheetRelsEntry(stream, model, sheetNo) {
        const xform = new RelationshipsXform();
        const relationships = await xform.parseStream(stream);
        model.worksheetRels[sheetNo] = relationships;
    }
    async _processMediaEntry(stream, model, filename) {
        const lastDot = filename.lastIndexOf(".");
        if (lastDot >= 1) {
            const extension = filename.substr(lastDot + 1);
            const name = filename.substr(0, lastDot);
            await new Promise((resolve, reject) => {
                const streamBuf = this.createStreamBuf();
                const cleanup = () => {
                    stream.off("error", onError);
                    streamBuf.off("error", onError);
                    streamBuf.off("finish", onFinish);
                };
                const onFinish = () => {
                    cleanup();
                    model.mediaIndex[filename] = model.media.length;
                    model.mediaIndex[name] = model.media.length;
                    const medium = {
                        type: "image",
                        name,
                        extension,
                        buffer: streamBuf.read()
                    };
                    model.media.push(medium);
                    resolve();
                };
                const onError = (error) => {
                    cleanup();
                    reject(error);
                };
                streamBuf.once("finish", onFinish);
                stream.on("error", onError);
                streamBuf.on("error", onError);
                stream.pipe(streamBuf);
            });
        }
    }
    /**
     * Process a drawing XML entry.
     *
     * @param stream - Stream to read from (used in loadFromZipEntries path)
     * @param model - Model to populate
     * @param name - Drawing name (e.g., "drawing1")
     * @param rawData - Pre-read raw data (used in loadFromFiles path to avoid re-reading stream)
     */
    async _processDrawingEntry(stream, model, name, rawData) {
        // Use provided rawData if available (loadFromFiles path), otherwise collect from stream.
        // In loadFromFiles, the stream is created from already-decoded text, and collecting from
        // it may not work correctly due to PassThrough stream timing issues.
        const data = rawData ?? (await this.collectStreamData(stream));
        // Parse the drawing for normal processing (images, etc.)
        const xform = new DrawingXform();
        const xmlString = this.bufferToString(data);
        const drawing = await xform.parseStream(this.createTextStream(xmlString));
        model.drawings[name] = drawing;
        // Store raw data; reconcile() may later drop it if charts are not referenced.
        model.rawDrawings[name] = data;
    }
    async _processDrawingRelsEntry(entry, model, name) {
        const xform = new RelationshipsXform();
        const relationships = await xform.parseStream(entry);
        model.drawingRels[name] = relationships;
    }
    async _processVmlDrawingEntry(entry, model, name) {
        const xform = new VmlDrawingXform();
        const vmlDrawing = await xform.parseStream(entry);
        model.vmlDrawings[vmlDrawingRelTargetFromWorksheetName(name)] = vmlDrawing;
    }
    async _processThemeEntry(stream, model, name) {
        await new Promise((resolve, reject) => {
            const streamBuf = this.createStreamBuf();
            const cleanup = () => {
                stream.off("error", onError);
                streamBuf.off("error", onError);
                streamBuf.off("finish", onFinish);
            };
            const onFinish = () => {
                cleanup();
                const data = streamBuf.read();
                model.themes[name] = data
                    ? typeof data === "string"
                        ? data
                        : this.bufferToString(data)
                    : "";
                resolve();
            };
            const onError = (err) => {
                cleanup();
                reject(err);
            };
            streamBuf.once("finish", onFinish);
            stream.on("error", onError);
            streamBuf.on("error", onError);
            stream.pipe(streamBuf);
        });
    }
    async _processPivotTableEntry(stream, model, name) {
        const xform = new PivotTableXform();
        const pivotTable = await xform.parseStream(stream);
        if (pivotTable) {
            model.pivotTables[name] = pivotTable;
        }
    }
    async _processPivotTableRelsEntry(stream, model, name) {
        const xform = new RelationshipsXform();
        const relationships = await xform.parseStream(stream);
        model.pivotTableRels[name] = relationships;
    }
    async _processPivotCacheDefinitionEntry(stream, model, name) {
        const xform = new PivotCacheDefinitionXform();
        const cacheDefinition = await xform.parseStream(stream);
        if (cacheDefinition) {
            model.pivotCacheDefinitions[name] = cacheDefinition;
        }
    }
    async _processPivotCacheRecordsEntry(stream, model, name) {
        const xform = new PivotCacheRecordsXform();
        const cacheRecords = await xform.parseStream(stream);
        if (cacheRecords) {
            model.pivotCacheRecords[name] = cacheRecords;
        }
    }
    // ===========================================================================
    // loadFromFiles - shared logic for loading from pre-extracted ZIP data
    // ===========================================================================
    async loadFromFiles(zipData, options) {
        const model = this.createEmptyModel();
        const entries = Object.keys(zipData).map(name => ({
            name,
            dir: name.endsWith("/"),
            data: zipData[name]
        }));
        for (const entry of entries) {
            if (!entry.dir) {
                const entryName = normalizeZipPath(entry.name);
                // Create appropriate stream based on entry type
                const isBinaryEntry = isBinaryEntryPath(entryName);
                const stream = isBinaryEntry
                    ? this.createBinaryStream(entry.data)
                    : this.createTextStream(this.bufferToString(entry.data));
                const handled = await this._processKnownEntry(stream, model, entryName, options);
                if (!handled) {
                    // Pass raw entry data for drawings to enable passthrough
                    await this._processDefaultEntry(stream, model, entryName, entry.data);
                }
            }
        }
        this.reconcile(model, options);
        this.workbook.model = model;
        return this.workbook;
    }
    /**
     * Process default entries (drawings, comments, tables, etc.)
     * @param rawData Optional raw entry data for passthrough preservation (used by loadFromFiles)
     */
    async _processDefaultEntry(stream, model, entryName, rawData) {
        const sheetNo = getWorksheetNoFromWorksheetRelsPath(entryName);
        if (sheetNo !== undefined) {
            await this._processWorksheetRelsEntry(stream, model, sheetNo);
            return true;
        }
        const mediaFilename = getMediaFilenameFromPath(entryName);
        if (mediaFilename) {
            await this._processMediaEntry(stream, model, mediaFilename);
            return true;
        }
        const drawingName = getDrawingNameFromPath(entryName);
        if (drawingName) {
            await this._processDrawingEntry(stream, model, drawingName, rawData);
            // rawData is now stored inside _processDrawingEntry
            return true;
        }
        const drawingRelsName = getDrawingNameFromRelsPath(entryName);
        if (drawingRelsName) {
            await this._processDrawingRelsEntry(stream, model, drawingRelsName);
            return true;
        }
        const vmlDrawingName = getVmlDrawingNameFromPath(entryName);
        if (vmlDrawingName) {
            await this._processVmlDrawingEntry(stream, model, vmlDrawingName);
            return true;
        }
        const commentsIndex = getCommentsIndexFromPath(entryName);
        if (commentsIndex) {
            await this._processCommentEntry(stream, model, `comments${commentsIndex}`);
            return true;
        }
        const tableName = getTableNameFromPath(entryName);
        if (tableName) {
            await this._processTableEntry(stream, model, tableName);
            return true;
        }
        const themeName = getThemeNameFromPath(entryName);
        if (themeName) {
            await this._processThemeEntry(stream, model, themeName);
            return true;
        }
        // Pivot table files
        const pivotTableName = getPivotTableNameFromPath(entryName);
        if (pivotTableName) {
            await this._processPivotTableEntry(stream, model, pivotTableName);
            return true;
        }
        const pivotTableRelsName = getPivotTableNameFromRelsPath(entryName);
        if (pivotTableRelsName) {
            await this._processPivotTableRelsEntry(stream, model, pivotTableRelsName);
            return true;
        }
        // Pivot cache files
        const pivotCacheDefinitionName = getPivotCacheDefinitionNameFromPath(entryName);
        if (pivotCacheDefinitionName) {
            await this._processPivotCacheDefinitionEntry(stream, model, pivotCacheDefinitionName);
            return true;
        }
        // R9-B8: Skip parsing pivotCacheDefinition .rels files — they are never used
        // during reconciliation and were just deleted at cleanup. The cache definition's
        // r:id attribute (preserved in ParsedCacheDefinition.rId) is sufficient.
        const pivotCacheDefinitionRelsName = getPivotCacheDefinitionNameFromRelsPath(entryName);
        if (pivotCacheDefinitionRelsName) {
            return true;
        }
        const pivotCacheRecordsName = getPivotCacheRecordsNameFromPath(entryName);
        if (pivotCacheRecordsName) {
            await this._processPivotCacheRecordsEntry(stream, model, pivotCacheRecordsName);
            return true;
        }
        // Store passthrough files (charts, etc.) for preservation
        if (PassthroughManager.isPassthroughPath(entryName)) {
            // If raw data is available (loadFromFiles path), use it directly
            if (rawData) {
                model.passthrough[entryName] = rawData;
            }
            else {
                await this._processPassthroughEntry(stream, model, entryName);
            }
            return true;
        }
        return false;
    }
    /**
     * Store a passthrough file for preservation during read/write cycles.
     * These files are not parsed but stored as raw bytes to be written back unchanged.
     */
    async _processPassthroughEntry(stream, model, entryName) {
        const data = await this.collectStreamData(stream);
        model.passthrough[entryName] = data;
    }
    // ===========================================================================
    // Write methods - shared by all platforms
    // ===========================================================================
    async addContentTypes(zip, model) {
        const xform = new ContentTypesXform();
        const xml = xform.toXml(model);
        zip.append(xml, { name: OOXML_PATHS.contentTypes });
    }
    async addApp(zip, model) {
        const xform = new AppXform();
        const xml = xform.toXml(model);
        zip.append(xml, { name: OOXML_PATHS.docPropsApp });
    }
    async addCore(zip, model) {
        const xform = new CoreXform();
        zip.append(xform.toXml(model), { name: OOXML_PATHS.docPropsCore });
    }
    async addThemes(zip, model) {
        const themes = model.themes || { theme1: theme1Xml };
        Object.keys(themes).forEach(name => {
            const xml = themes[name];
            zip.append(xml, { name: themePath(name) });
        });
    }
    async addOfficeRels(zip, _model) {
        const xform = new RelationshipsXform();
        const xml = xform.toXml([
            { Id: "rId1", Type: XLSX.RelType.OfficeDocument, Target: OOXML_PATHS.xlWorkbook },
            { Id: "rId2", Type: XLSX.RelType.CoreProperties, Target: OOXML_PATHS.docPropsCore },
            { Id: "rId3", Type: XLSX.RelType.ExtenderProperties, Target: OOXML_PATHS.docPropsApp }
        ]);
        zip.append(xml, { name: OOXML_PATHS.rootRels });
    }
    async addWorkbookRels(zip, model) {
        let count = 1;
        const relationships = [
            { Id: `rId${count++}`, Type: XLSX.RelType.Styles, Target: OOXML_REL_TARGETS.workbookStyles },
            { Id: `rId${count++}`, Type: XLSX.RelType.Theme, Target: OOXML_REL_TARGETS.workbookTheme1 }
        ];
        if (model.sharedStrings.count) {
            relationships.push({
                Id: `rId${count++}`,
                Type: XLSX.RelType.SharedStrings,
                Target: OOXML_REL_TARGETS.workbookSharedStrings
            });
        }
        // Add FeaturePropertyBag relationship if checkboxes are used
        if (model.hasCheckboxes) {
            relationships.push({
                Id: `rId${count++}`,
                Type: XLSX.RelType.FeaturePropertyBag,
                Target: OOXML_REL_TARGETS.workbookFeaturePropertyBag
            });
        }
        // R9-B6: Deduplicate pivot cache relationships by cacheId. When multiple pivot
        // tables share the same cache, only one workbook relationship should be created.
        // Also assigns rId to each pivot table (R9-B7: typed on PivotTable interface).
        const seenCacheIds = new Map(); // cacheId → rId
        (model.pivotTables ?? []).forEach((pivotTable) => {
            const existing = seenCacheIds.get(pivotTable.cacheId);
            if (existing) {
                // Shared cache: reuse the rId from the first pivot table with this cacheId
                pivotTable.rId = existing;
            }
            else {
                pivotTable.rId = `rId${count++}`;
                seenCacheIds.set(pivotTable.cacheId, pivotTable.rId);
                relationships.push({
                    Id: pivotTable.rId,
                    Type: XLSX.RelType.PivotCacheDefinition,
                    Target: pivotCacheDefinitionRelTargetFromWorkbook(pivotTable.tableNumber)
                });
            }
        });
        model.worksheets.forEach((worksheet) => {
            worksheet.rId = `rId${count++}`;
            // fileIndex is assigned once in prepareModel() — use it directly
            relationships.push({
                Id: worksheet.rId,
                Type: XLSX.RelType.Worksheet,
                Target: worksheetRelTarget(worksheet.fileIndex)
            });
        });
        const xform = new RelationshipsXform();
        const xml = xform.toXml(relationships);
        zip.append(xml, { name: OOXML_PATHS.xlWorkbookRels });
    }
    async addFeaturePropertyBag(zip, model) {
        if (!model.hasCheckboxes) {
            return;
        }
        const xform = new FeaturePropertyBagXform();
        zip.append(xform.toXml({}), { name: OOXML_PATHS.xlFeaturePropertyBag });
    }
    async addSharedStrings(zip, model) {
        if (model.sharedStrings && model.sharedStrings.count) {
            zip.append(model.sharedStrings.xml, { name: OOXML_PATHS.xlSharedStrings });
        }
    }
    async addStyles(zip, model) {
        const { xml } = model.styles;
        if (xml) {
            zip.append(xml, { name: OOXML_PATHS.xlStyles });
        }
    }
    async addWorkbook(zip, model) {
        const xform = new WorkbookXform();
        zip.append(xform.toXml(model), { name: OOXML_PATHS.xlWorkbook });
    }
    async addWorksheets(zip, model) {
        const worksheetXform = new WorkSheetXform();
        const relationshipsXform = new RelationshipsXform();
        const commentsXform = new CommentsXform();
        const vmlDrawingXform = new VmlDrawingXform();
        const ctrlPropXform = new CtrlPropXform();
        model.worksheets.forEach((worksheet) => {
            const { fileIndex } = worksheet;
            let xmlStream = new XmlStream();
            worksheetXform.render(xmlStream, worksheet);
            zip.append(xmlStream.xml, { name: worksheetPath(fileIndex) });
            if (worksheet.rels && worksheet.rels.length) {
                xmlStream = new XmlStream();
                relationshipsXform.render(xmlStream, worksheet.rels);
                zip.append(xmlStream.xml, { name: worksheetRelsPath(fileIndex) });
            }
            // Generate comments XML (separate from VML)
            if (worksheet.comments.length > 0) {
                xmlStream = new XmlStream();
                commentsXform.render(xmlStream, worksheet);
                zip.append(xmlStream.xml, { name: commentsPath(fileIndex) });
            }
            // Generate unified VML drawing (contains both notes and form controls)
            const hasComments = worksheet.comments.length > 0;
            const hasFormControls = worksheet.formControls && worksheet.formControls.length > 0;
            if (hasComments || hasFormControls) {
                xmlStream = new XmlStream();
                vmlDrawingXform.render(xmlStream, {
                    comments: hasComments ? worksheet.comments : [],
                    formControls: hasFormControls ? worksheet.formControls : []
                });
                zip.append(xmlStream.xml, { name: vmlDrawingPath(fileIndex) });
            }
            // Generate ctrlProp files for form controls
            if (hasFormControls) {
                worksheet.formControls.forEach((control) => {
                    const xml = ctrlPropXform.toXml(control);
                    zip.append(xml, { name: ctrlPropPath(control.ctrlPropId) });
                });
            }
        });
    }
    addDrawings(zip, model) {
        const drawingXform = new DrawingXform();
        const relsXform = new RelationshipsXform();
        const rawDrawings = model.rawDrawings || {};
        model.worksheets.forEach((worksheet) => {
            const { drawing } = worksheet;
            if (drawing) {
                // Check if drawing rels contain chart references using helper
                const hasChartReference = this.drawingHasChartReference(drawing);
                if (hasChartReference && rawDrawings[drawing.name]) {
                    // Use raw data for drawings with chart references (passthrough)
                    zip.append(rawDrawings[drawing.name], { name: drawingPath(drawing.name) });
                }
                else {
                    // Use regenerated XML for normal drawings (images, shapes)
                    // Filter out invalid anchors (null, undefined, or missing content)
                    const filteredAnchors = (drawing.anchors ?? []).filter((a) => {
                        if (a == null) {
                            return false;
                        }
                        // Form controls have range.br and shape properties
                        if (a.range?.br && a.shape) {
                            return true;
                        }
                        // One-cell anchors need a valid picture
                        if (!a.br && !a.picture) {
                            return false;
                        }
                        // Two-cell anchors need either picture or shape
                        if (a.br && !a.picture && !a.shape) {
                            return false;
                        }
                        return true;
                    });
                    const drawingForWrite = drawing.anchors
                        ? { ...drawing, anchors: filteredAnchors }
                        : drawing;
                    drawingXform.prepare(drawingForWrite);
                    const xml = drawingXform.toXml(drawingForWrite);
                    zip.append(xml, { name: drawingPath(drawing.name) });
                }
                const relsXml = relsXform.toXml(drawing.rels);
                zip.append(relsXml, { name: drawingRelsPath(drawing.name) });
            }
        });
    }
    addTables(zip, model) {
        const tableXform = new TableXform();
        model.worksheets.forEach((worksheet) => {
            const { tables } = worksheet;
            tables.forEach((table) => {
                tableXform.prepare(table, {});
                const tableXml = tableXform.toXml(table);
                zip.append(tableXml, { name: tablePath(table.target) });
            });
        });
    }
    /**
     * Write passthrough files (charts, etc.) that were preserved during read.
     * These files are written back unchanged to preserve unsupported features.
     */
    addPassthrough(zip, model) {
        const passthroughManager = new PassthroughManager();
        passthroughManager.fromRecord(model.passthrough || {});
        passthroughManager.writeToZip(zip);
    }
    addPivotTables(zip, model) {
        if (!model.pivotTables.length) {
            return;
        }
        const pivotCacheRecordsXform = new PivotCacheRecordsXform();
        const pivotCacheDefinitionXform = new PivotCacheDefinitionXform();
        const pivotTableXform = new PivotTableXform();
        const relsXform = new RelationshipsXform();
        // R9-B6: Track which cacheIds have already been written to avoid duplicating
        // shared caches. Maps cacheId → tableNumber used for the cache file names.
        const writtenCaches = new Map();
        model.pivotTables.forEach((pivotTable) => {
            const n = pivotTable.tableNumber;
            const isLoaded = pivotTable.isLoaded;
            const cacheId = pivotTable.cacheId;
            // R9-B6: Only write cache definition/records/rels once per unique cacheId.
            const cacheAlreadyWritten = writtenCaches.has(cacheId);
            if (!cacheAlreadyWritten) {
                writtenCaches.set(cacheId, n);
                if (isLoaded) {
                    if (pivotTable.cacheDefinition) {
                        const xml = pivotCacheDefinitionXform.toXml(pivotTable.cacheDefinition);
                        zip.append(xml, { name: pivotCacheDefinitionPath(n) });
                    }
                    if (pivotTable.cacheRecords) {
                        const xml = pivotCacheRecordsXform.toXml(pivotTable.cacheRecords);
                        zip.append(xml, { name: pivotCacheRecordsPath(n) });
                    }
                }
                else {
                    let xml = pivotCacheRecordsXform.toXml(pivotTable);
                    zip.append(xml, { name: pivotCacheRecordsPath(n) });
                    xml = pivotCacheDefinitionXform.toXml(pivotTable);
                    zip.append(xml, { name: pivotCacheDefinitionPath(n) });
                }
                // R9-B4: Only write cache definition rels when cache records exist.
                // For loaded pivot tables without cacheRecords (e.g. OLAP), skip the rels file entirely.
                // R9-B3: Use the rId from the loaded cache definition to stay consistent with the XML.
                const hasCacheRecords = isLoaded ? !!pivotTable.cacheRecords : true;
                if (hasCacheRecords) {
                    const cacheRecordsRId = (isLoaded ? pivotTable.cacheDefinition?.rId : undefined) ?? "rId1";
                    const xml = relsXform.toXml([
                        {
                            Id: cacheRecordsRId,
                            Type: XLSX.RelType.PivotCacheRecords,
                            Target: pivotCacheRecordsRelTarget(n)
                        }
                    ]);
                    zip.append(xml, { name: pivotCacheDefinitionRelsPath(n) });
                }
            }
            // Pivot table XML is always written (each pivot table has its own file).
            let xml = pivotTableXform.toXml(pivotTable);
            zip.append(xml, { name: pivotTablePath(n) });
            // Pivot table rels point to the cache definition file. For shared caches,
            // use the tableNumber of the first pivot table that wrote the cache.
            const cacheTableNumber = writtenCaches.get(cacheId);
            xml = relsXform.toXml([
                {
                    Id: "rId1",
                    Type: XLSX.RelType.PivotCacheDefinition,
                    Target: pivotCacheDefinitionRelTargetFromPivotTable(cacheTableNumber)
                }
            ]);
            zip.append(xml, { name: pivotTableRelsPath(n) });
        });
    }
    _finalize(zip) {
        return new Promise((resolve, reject) => {
            zip.on("finish", () => {
                resolve(this);
            });
            zip.on("error", reject);
            zip.finalize();
        });
    }
    prepareModel(model, options) {
        model.creator = model.creator ?? "ExcelTS";
        model.lastModifiedBy = model.lastModifiedBy ?? "ExcelTS";
        model.created = model.created ?? new Date();
        model.modified = model.modified ?? new Date();
        model.useSharedStrings =
            options.useSharedStrings !== undefined ? options.useSharedStrings : true;
        model.useStyles = options.useStyles !== undefined ? options.useStyles : true;
        model.sharedStrings = new SharedStringsXform();
        // Preserve default font from parsed styles if available
        const oldDefaultFont = model.defaultFont;
        model.styles = model.useStyles ? new StylesXform(true) : new StylesXform.Mock();
        if (oldDefaultFont && model.styles.setDefaultFont) {
            model.styles.setDefaultFont(oldDefaultFont);
        }
        const workbookXform = new WorkbookXform();
        const worksheetXform = new WorkSheetXform();
        workbookXform.prepare(model);
        const worksheetOptions = {
            sharedStrings: model.sharedStrings,
            styles: model.styles,
            date1904: model.properties.date1904,
            drawingsCount: 0,
            media: model.media
        };
        worksheetOptions.drawings = model.drawings = [];
        worksheetOptions.commentRefs = model.commentRefs = [];
        worksheetOptions.formControlRefs = model.formControlRefs = [];
        let tableCount = 0;
        model.tables = [];
        model.worksheets.forEach((worksheet, index) => {
            // Assign fileIndex early so that worksheet-xform.prepare() can use it
            // for comment/VML relationship targets and content type names.
            // This ensures consistency with addWorksheets() which writes ZIP entries
            // using the same fileIndex.
            worksheet.fileIndex = index + 1;
            worksheet.tables.forEach((table) => {
                tableCount++;
                table.target = `table${tableCount}.xml`;
                table.id = tableCount;
                model.tables.push(table);
            });
            worksheetXform.prepare(worksheet, worksheetOptions);
        });
        // ContentTypesXform expects this flag
        model.hasCheckboxes = model.styles.hasCheckboxes;
        // Build passthroughContentTypes for ContentTypesXform using PassthroughManager
        const passthrough = model.passthrough || {};
        const passthroughManager = new PassthroughManager();
        passthroughManager.fromRecord(passthrough);
        model.passthroughContentTypes = passthroughManager.getContentTypes();
    }
}
XLSX.RelType = RelType;
export { XLSX };
