/**
 * 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).
 */
import { Zip, ZipDeflate } from "../../archive/zip/stream.js";
import { StreamBuf } from "../utils/stream-buf.js";
import { base64ToUint8Array } from "../../../utils/utils.browser.js";
import { RelType } from "../xlsx/rel-type.js";
import { StylesXform } from "../xlsx/xform/style/styles-xform.js";
import { SharedStrings } from "../utils/shared-strings.js";
import { DefinedNames } from "../defined-names.js";
import { CoreXform } from "../xlsx/xform/core/core-xform.js";
import { RelationshipsXform } from "../xlsx/xform/core/relationships-xform.js";
import { ContentTypesXform } from "../xlsx/xform/core/content-types-xform.js";
import { AppXform } from "../xlsx/xform/core/app-xform.js";
import { WorkbookXform } from "../xlsx/xform/book/workbook-xform.js";
import { SharedStringsXform } from "../xlsx/xform/strings/shared-strings-xform.js";
import { FeaturePropertyBagXform } from "../xlsx/xform/core/feature-property-bag-xform.js";
import { theme1Xml } from "../xlsx/xml/theme1.js";
import { Writeable, stringToUint8Array } from "../../stream/index.browser.js";
import { mediaPath, OOXML_PATHS, OOXML_REL_TARGETS, worksheetRelTarget } from "../utils/ooxml-paths.js";
import { WorksheetWriter } from "./worksheet-writer.js";
const EMPTY_U8 = new Uint8Array(0);
const TEXT_DECODER = new TextDecoder();
// ============================================================================
// Base Class
// ============================================================================
export 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 SharedStrings();
        this.styles = options.useStyles ? new StylesXform(true) : new StylesXform.Mock(true);
        this._definedNames = new 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 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 Writeable(options.stream);
        }
        return new StreamBuf();
    }
    get definedNames() {
        return this._definedNames;
    }
    _openStream(path) {
        const stream = new StreamBuf({
            bufSize: this._trueStreaming ? 4096 : 65536,
            batch: !this._trueStreaming
        });
        const zipFile = new 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 ZipDeflate(name, { level: this.compressionLevel });
        this.zip.add(zipFile);
        let buffer;
        if (base64) {
            const base64Data = typeof data === "string" ? data : TEXT_DECODER.decode(data);
            buffer = base64ToUint8Array(base64Data);
        }
        else if (typeof data === "string") {
            buffer = 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.xlStyles);
            resolve();
        });
    }
    addThemes() {
        return new Promise(resolve => {
            this._addFile(theme1Xml, OOXML_PATHS.xlTheme1);
            resolve();
        });
    }
    addOfficeRels() {
        return new Promise(resolve => {
            const xform = new RelationshipsXform();
            const xml = xform.toXml([
                { Id: "rId1", Type: RelType.OfficeDocument, Target: OOXML_PATHS.xlWorkbook },
                { Id: "rId2", Type: RelType.CoreProperties, Target: OOXML_PATHS.docPropsCore },
                { Id: "rId3", Type: RelType.ExtenderProperties, Target: OOXML_PATHS.docPropsApp }
            ]);
            this._addFile(xml, OOXML_PATHS.rootRels);
            resolve();
        });
    }
    addContentTypes() {
        return new Promise(resolve => {
            const model = {
                worksheets: this._worksheets.filter(Boolean),
                sharedStrings: this.sharedStrings,
                commentRefs: this.commentRefs,
                media: this.media,
                hasCheckboxes: this.styles.hasCheckboxes
            };
            const xform = new ContentTypesXform();
            this._addFile(xform.toXml(model), 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 = 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 AppXform();
            this._addFile(xform.toXml({ worksheets: this._worksheets.filter(Boolean) }), OOXML_PATHS.docPropsApp);
            resolve();
        });
    }
    addCore() {
        return new Promise(resolve => {
            const xform = new CoreXform();
            this._addFile(xform.toXml(this), OOXML_PATHS.docPropsCore);
            resolve();
        });
    }
    addSharedStrings() {
        if (this.sharedStrings.count) {
            return new Promise(resolve => {
                const xform = new SharedStringsXform();
                this._addFile(xform.toXml(this.sharedStrings), OOXML_PATHS.xlSharedStrings);
                resolve();
            });
        }
        return Promise.resolve();
    }
    addFeaturePropertyBag() {
        if (this.styles.hasCheckboxes) {
            const xform = new FeaturePropertyBagXform();
            this._addFile(xform.toXml({}), OOXML_PATHS.xlFeaturePropertyBag);
        }
        return Promise.resolve();
    }
    addWorkbookRels() {
        let count = 1;
        const relationships = [
            { Id: `rId${count++}`, Type: RelType.Styles, Target: OOXML_REL_TARGETS.workbookStyles },
            { Id: `rId${count++}`, Type: RelType.Theme, Target: OOXML_REL_TARGETS.workbookTheme1 }
        ];
        if (this.sharedStrings.count) {
            relationships.push({
                Id: `rId${count++}`,
                Type: RelType.SharedStrings,
                Target: OOXML_REL_TARGETS.workbookSharedStrings
            });
        }
        // Add FeaturePropertyBag relationship if checkboxes are used
        if (this.styles.hasCheckboxes) {
            relationships.push({
                Id: `rId${count++}`,
                Type: RelType.FeaturePropertyBag,
                Target: OOXML_REL_TARGETS.workbookFeaturePropertyBag
            });
        }
        this._worksheets.forEach(ws => {
            if (ws) {
                ws.rId = `rId${count++}`;
                relationships.push({
                    Id: ws.rId,
                    Type: RelType.Worksheet,
                    Target: worksheetRelTarget(ws.id)
                });
            }
        });
        return new Promise(resolve => {
            const xform = new RelationshipsXform();
            this._addFile(xform.toXml(relationships), 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 WorkbookXform();
            xform.prepare(model);
            this._addFile(xform.toXml(model), 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();
        });
    }
}
export const WorkbookWriterOptionsSchema = {
    useSharedStrings: ["boolean"],
    useStyles: ["boolean"],
    trueStreaming: ["boolean"]
};
// ============================================================================
// Browser-compatible WorkbookWriter
// ============================================================================
class WorkbookWriter extends WorkbookWriterBase {
    constructor(options = {}) {
        super(options, WorksheetWriter);
    }
}
export { WorkbookWriter };
