"use strict";
/**
 * WorkbookWriter - Browser Streaming Excel Writer
 *
 * This module contains the full cross-platform implementation for the streaming
 * workbook writer and a browser-compatible `WorkbookWriter` class.
 *
 * Node.js uses `workbook-writer.ts`, which extends the same base implementation
 * with filesystem-specific features (filename output + image loading).
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkbookWriter = exports.WorkbookWriterOptionsSchema = exports.WorkbookWriterBase = void 0;
const stream_1 = require("../../archive/zip/stream.js");
const stream_buf_1 = require("../utils/stream-buf.js");
const utils_1 = require("../../../utils/utils.js");
const rel_type_1 = require("../xlsx/rel-type.js");
const styles_xform_1 = require("../xlsx/xform/style/styles-xform.js");
const shared_strings_1 = require("../utils/shared-strings.js");
const defined_names_1 = require("../defined-names.js");
const core_xform_1 = require("../xlsx/xform/core/core-xform.js");
const relationships_xform_1 = require("../xlsx/xform/core/relationships-xform.js");
const content_types_xform_1 = require("../xlsx/xform/core/content-types-xform.js");
const app_xform_1 = require("../xlsx/xform/core/app-xform.js");
const workbook_xform_1 = require("../xlsx/xform/book/workbook-xform.js");
const shared_strings_xform_1 = require("../xlsx/xform/strings/shared-strings-xform.js");
const feature_property_bag_xform_1 = require("../xlsx/xform/core/feature-property-bag-xform.js");
const theme1_1 = require("../xlsx/xml/theme1.js");
const _stream_1 = require("../../stream/index.js");
const ooxml_paths_1 = require("../utils/ooxml-paths.js");
const worksheet_writer_1 = require("./worksheet-writer.js");
const EMPTY_U8 = new Uint8Array(0);
const TEXT_DECODER = new TextDecoder();
// ============================================================================
// Base Class
// ============================================================================
class WorkbookWriterBase {
    constructor(options, WorksheetWriterClass) {
        this.WorksheetWriterClass = WorksheetWriterClass;
        this.created = options.created || new Date();
        this.modified = options.modified || this.created;
        this.creator = options.creator ?? "ExcelTS";
        this.lastModifiedBy = options.lastModifiedBy ?? "ExcelTS";
        this.lastPrinted = options.lastPrinted;
        this.useSharedStrings = options.useSharedStrings ?? false;
        this.sharedStrings = new shared_strings_1.SharedStrings();
        this.styles = options.useStyles ? new styles_xform_1.StylesXform(true) : new styles_xform_1.StylesXform.Mock(true);
        this._definedNames = new defined_names_1.DefinedNames();
        this._worksheets = [];
        this.views = [];
        this.zipOptions = options.zip;
        const level = options.zip?.zlib?.level ?? options.zip?.compressionOptions?.level ?? 1;
        this.compressionLevel = Math.max(0, Math.min(9, level));
        this.media = [];
        this.commentRefs = [];
        this._trueStreaming = options.trueStreaming ?? false;
        // Create Zip instance
        this.zip = new stream_1.Zip((err, data, final) => {
            if (err) {
                this.stream.emit("error", err);
            }
            else {
                // `streaming-zip` already emits `Uint8Array`; avoid copying per chunk.
                this.stream.write(data);
                if (final) {
                    this.stream.end();
                }
            }
        });
        // Setup output stream
        this.stream = this._createOutputStream(options);
        // Add initial files
        this.promise = Promise.all([this.addThemes(), this.addOfficeRels()]);
    }
    /**
     * Create output stream - can be overridden by Node.js to support filename
     */
    _createOutputStream(options) {
        if (options.stream) {
            return (0, _stream_1.Writeable)(options.stream);
        }
        return new stream_buf_1.StreamBuf();
    }
    get definedNames() {
        return this._definedNames;
    }
    _openStream(path) {
        const stream = new stream_buf_1.StreamBuf({
            bufSize: this._trueStreaming ? 4096 : 65536,
            batch: !this._trueStreaming
        });
        const zipFile = new stream_1.ZipDeflate(path, { level: this.compressionLevel });
        this.zip.add(zipFile);
        const onData = (chunk) => zipFile.push(chunk);
        stream.on("data", onData);
        stream.once("finish", () => {
            stream.removeListener("data", onData);
            zipFile.push(EMPTY_U8, true);
            stream.emit("zipped");
        });
        return stream;
    }
    _addFile(data, name, base64) {
        const zipFile = new stream_1.ZipDeflate(name, { level: this.compressionLevel });
        this.zip.add(zipFile);
        let buffer;
        if (base64) {
            const base64Data = typeof data === "string" ? data : TEXT_DECODER.decode(data);
            buffer = (0, utils_1.base64ToUint8Array)(base64Data);
        }
        else if (typeof data === "string") {
            buffer = (0, _stream_1.stringToUint8Array)(data);
        }
        else {
            buffer = data;
        }
        zipFile.push(buffer, true);
    }
    _commitWorksheets() {
        const commitWorksheet = (worksheet) => {
            if (!worksheet.committed) {
                return new Promise(resolve => {
                    worksheet.stream.once("zipped", () => resolve());
                    worksheet.commit();
                });
            }
            return Promise.resolve();
        };
        const promises = this._worksheets.map(commitWorksheet);
        return promises.length ? Promise.all(promises).then(() => { }) : Promise.resolve();
    }
    async commit() {
        await this.promise;
        await this._commitWorksheets();
        await this.addMedia();
        await Promise.all([
            this.addContentTypes(),
            this.addApp(),
            this.addCore(),
            this.addSharedStrings(),
            this.addStyles(),
            this.addFeaturePropertyBag(),
            this.addWorkbookRels()
        ]);
        await this.addWorkbook();
        await this._finalize();
    }
    get nextId() {
        for (let i = 1; i < this._worksheets.length; i++) {
            if (!this._worksheets[i]) {
                return i;
            }
        }
        return this._worksheets.length || 1;
    }
    addImage(image) {
        const id = this.media.length;
        const medium = {
            ...image,
            type: "image",
            name: `image${id}.${image.extension}`
        };
        this.media.push(medium);
        return id;
    }
    getImage(id) {
        return this.media[id];
    }
    addWorksheet(name, options) {
        const opts = options || {};
        const useSharedStrings = opts.useSharedStrings !== undefined ? opts.useSharedStrings : this.useSharedStrings;
        if (opts.tabColor) {
            console.trace("tabColor option has moved to { properties: tabColor: {...} }");
            opts.properties = { tabColor: opts.tabColor, ...opts.properties };
        }
        const id = this.nextId;
        name = name ?? `sheet${id}`;
        const worksheet = new this.WorksheetWriterClass({
            id,
            name,
            workbook: this,
            useSharedStrings,
            properties: opts.properties,
            state: opts.state,
            pageSetup: opts.pageSetup,
            views: opts.views,
            autoFilter: opts.autoFilter,
            headerFooter: opts.headerFooter
        });
        this._worksheets[id] = worksheet;
        return worksheet;
    }
    getWorksheet(id) {
        if (id === undefined) {
            return this._worksheets.find(() => true);
        }
        if (typeof id === "number") {
            return this._worksheets[id];
        }
        if (typeof id === "string") {
            return this._worksheets.find(ws => ws?.name === id);
        }
        return undefined;
    }
    addStyles() {
        return new Promise(resolve => {
            this._addFile(this.styles.xml, ooxml_paths_1.OOXML_PATHS.xlStyles);
            resolve();
        });
    }
    addThemes() {
        return new Promise(resolve => {
            this._addFile(theme1_1.theme1Xml, ooxml_paths_1.OOXML_PATHS.xlTheme1);
            resolve();
        });
    }
    addOfficeRels() {
        return new Promise(resolve => {
            const xform = new relationships_xform_1.RelationshipsXform();
            const xml = xform.toXml([
                { Id: "rId1", Type: rel_type_1.RelType.OfficeDocument, Target: ooxml_paths_1.OOXML_PATHS.xlWorkbook },
                { Id: "rId2", Type: rel_type_1.RelType.CoreProperties, Target: ooxml_paths_1.OOXML_PATHS.docPropsCore },
                { Id: "rId3", Type: rel_type_1.RelType.ExtenderProperties, Target: ooxml_paths_1.OOXML_PATHS.docPropsApp }
            ]);
            this._addFile(xml, ooxml_paths_1.OOXML_PATHS.rootRels);
            resolve();
        });
    }
    addContentTypes() {
        return new Promise(resolve => {
            const worksheets = this._worksheets.filter(Boolean);
            // In the streaming path, ZIP entries use ws.id which is always sequential.
            // Set fileIndex = id to satisfy the ContentTypesXform contract.
            worksheets.forEach((ws) => {
                ws.fileIndex = ws.id;
            });
            const model = {
                worksheets,
                sharedStrings: this.sharedStrings,
                commentRefs: this.commentRefs,
                media: this.media,
                hasCheckboxes: this.styles.hasCheckboxes
            };
            const xform = new content_types_xform_1.ContentTypesXform();
            this._addFile(xform.toXml(model), ooxml_paths_1.OOXML_PATHS.contentTypes);
            resolve();
        });
    }
    /**
     * Add media files - can be overridden by Node.js for file system support
     */
    addMedia() {
        return Promise.all(this.media.map(async (medium) => {
            if (medium.type === "image") {
                const filename = (0, ooxml_paths_1.mediaPath)(medium.name);
                if (medium.buffer) {
                    this._addFile(medium.buffer, filename);
                    return;
                }
                if (medium.base64) {
                    const content = medium.base64.substring(medium.base64.indexOf(",") + 1);
                    this._addFile(content, filename, true);
                    return;
                }
                if (medium.filename) {
                    throw new Error("Loading images from filename is not supported in browser. Use buffer or base64.");
                }
            }
            throw new Error("Unsupported media");
        }));
    }
    addApp() {
        return new Promise(resolve => {
            const xform = new app_xform_1.AppXform();
            this._addFile(xform.toXml({ worksheets: this._worksheets.filter(Boolean) }), ooxml_paths_1.OOXML_PATHS.docPropsApp);
            resolve();
        });
    }
    addCore() {
        return new Promise(resolve => {
            const xform = new core_xform_1.CoreXform();
            this._addFile(xform.toXml(this), ooxml_paths_1.OOXML_PATHS.docPropsCore);
            resolve();
        });
    }
    addSharedStrings() {
        if (this.sharedStrings.count) {
            return new Promise(resolve => {
                const xform = new shared_strings_xform_1.SharedStringsXform();
                this._addFile(xform.toXml(this.sharedStrings), ooxml_paths_1.OOXML_PATHS.xlSharedStrings);
                resolve();
            });
        }
        return Promise.resolve();
    }
    addFeaturePropertyBag() {
        if (this.styles.hasCheckboxes) {
            const xform = new feature_property_bag_xform_1.FeaturePropertyBagXform();
            this._addFile(xform.toXml({}), ooxml_paths_1.OOXML_PATHS.xlFeaturePropertyBag);
        }
        return Promise.resolve();
    }
    addWorkbookRels() {
        let count = 1;
        const relationships = [
            { Id: `rId${count++}`, Type: rel_type_1.RelType.Styles, Target: ooxml_paths_1.OOXML_REL_TARGETS.workbookStyles },
            { Id: `rId${count++}`, Type: rel_type_1.RelType.Theme, Target: ooxml_paths_1.OOXML_REL_TARGETS.workbookTheme1 }
        ];
        if (this.sharedStrings.count) {
            relationships.push({
                Id: `rId${count++}`,
                Type: rel_type_1.RelType.SharedStrings,
                Target: ooxml_paths_1.OOXML_REL_TARGETS.workbookSharedStrings
            });
        }
        // Add FeaturePropertyBag relationship if checkboxes are used
        if (this.styles.hasCheckboxes) {
            relationships.push({
                Id: `rId${count++}`,
                Type: rel_type_1.RelType.FeaturePropertyBag,
                Target: ooxml_paths_1.OOXML_REL_TARGETS.workbookFeaturePropertyBag
            });
        }
        this._worksheets.forEach(ws => {
            if (ws) {
                ws.rId = `rId${count++}`;
                relationships.push({
                    Id: ws.rId,
                    Type: rel_type_1.RelType.Worksheet,
                    Target: (0, ooxml_paths_1.worksheetRelTarget)(ws.id)
                });
            }
        });
        return new Promise(resolve => {
            const xform = new relationships_xform_1.RelationshipsXform();
            this._addFile(xform.toXml(relationships), ooxml_paths_1.OOXML_PATHS.xlWorkbookRels);
            resolve();
        });
    }
    addWorkbook() {
        const model = {
            worksheets: this._worksheets.filter(Boolean),
            definedNames: this._definedNames.model,
            views: this.views,
            properties: {},
            calcProperties: {}
        };
        return new Promise(resolve => {
            const xform = new workbook_xform_1.WorkbookXform();
            xform.prepare(model);
            this._addFile(xform.toXml(model), ooxml_paths_1.OOXML_PATHS.xlWorkbook);
            resolve();
        });
    }
    _finalize() {
        return new Promise((resolve, reject) => {
            const onError = (err) => {
                this.stream.removeListener("finish", onFinish);
                reject(err);
            };
            const onFinish = () => {
                this.stream.removeListener("error", onError);
                resolve(this);
            };
            this.stream.once("error", onError);
            this.stream.once("finish", onFinish);
            this.zip.end();
        });
    }
}
exports.WorkbookWriterBase = WorkbookWriterBase;
exports.WorkbookWriterOptionsSchema = {
    useSharedStrings: ["boolean"],
    useStyles: ["boolean"],
    trueStreaming: ["boolean"]
};
// ============================================================================
// Browser-compatible WorkbookWriter
// ============================================================================
class WorkbookWriter extends WorkbookWriterBase {
    constructor(options = {}) {
        super(options, worksheet_writer_1.WorksheetWriter);
    }
}
exports.WorkbookWriter = WorkbookWriter;
