"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StylesXform = void 0;
const enums_1 = require("../../../enums.js");
const xml_stream_1 = require("../../../utils/xml-stream.js");
const base_xform_1 = require("../base-xform.js");
const static_xform_1 = require("../static-xform.js");
const list_xform_1 = require("../list-xform.js");
const font_xform_1 = require("./font-xform.js");
const fill_xform_1 = require("./fill-xform.js");
const border_xform_1 = require("./border-xform.js");
const numfmt_xform_1 = require("./numfmt-xform.js");
const style_xform_1 = require("./style-xform.js");
const dxf_xform_1 = require("./dxf-xform.js");
// custom numfmt ids start here
const NUMFMT_BASE = 164;
// =============================================================================
// StylesXform is used to generate and parse the styles.xml file
// it manages the collections of fonts, number formats, alignments, etc
class StylesXform extends base_xform_1.BaseXform {
    constructor(initialise) {
        super();
        this.map = {
            numFmts: new list_xform_1.ListXform({ tag: "numFmts", count: true, childXform: new numfmt_xform_1.NumFmtXform() }),
            fonts: new list_xform_1.ListXform({
                tag: "fonts",
                count: true,
                childXform: new font_xform_1.FontXform(),
                $: { "x14ac:knownFonts": 1 }
            }),
            fills: new list_xform_1.ListXform({ tag: "fills", count: true, childXform: new fill_xform_1.FillXform() }),
            borders: new list_xform_1.ListXform({ tag: "borders", count: true, childXform: new border_xform_1.BorderXform() }),
            cellStyleXfs: new list_xform_1.ListXform({
                tag: "cellStyleXfs",
                count: true,
                childXform: new style_xform_1.StyleXform()
            }),
            cellXfs: new list_xform_1.ListXform({
                tag: "cellXfs",
                count: true,
                childXform: new style_xform_1.StyleXform({ xfId: true })
            }),
            dxfs: new list_xform_1.ListXform({ tag: "dxfs", always: true, count: true, childXform: new dxf_xform_1.DxfXform() }),
            // for style manager
            numFmt: new numfmt_xform_1.NumFmtXform(),
            font: new font_xform_1.FontXform(),
            fill: new fill_xform_1.FillXform(),
            border: new border_xform_1.BorderXform(),
            style: new style_xform_1.StyleXform({ xfId: true }),
            cellStyles: StylesXform.STATIC_XFORMS.cellStyles,
            tableStyles: StylesXform.STATIC_XFORMS.tableStyles,
            extLst: StylesXform.STATIC_XFORMS.extLst
        };
        if (initialise) {
            // StylesXform also acts as style manager and is used to build up styles-model during worksheet processing
            this.init();
        }
    }
    initIndex() {
        this.index = {
            style: {},
            numFmt: {},
            numFmtNextId: 164, // start custom format ids here
            font: {},
            border: {},
            fill: {}
        };
    }
    init() {
        // Prepare for Style Manager role
        this.model = {
            styles: [],
            numFmts: [],
            fonts: [],
            borders: [],
            fills: [],
            dxfs: []
        };
        this.initIndex();
        // default (zero) border
        this._addBorder({});
        // add default (all zero) style
        this._addStyle({ numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0 });
        // add default fills
        this._addFill({ type: "pattern", pattern: "none" });
        this._addFill({ type: "pattern", pattern: "gray125" });
        this.weakMap = new WeakMap();
        this._hasCheckboxes = false;
    }
    /**
     * Set the default font to use when no font is explicitly specified.
     * This preserves the original file's default font during round-trip.
     */
    setDefaultFont(font) {
        this.defaultFont = font;
    }
    render(xmlStream, model) {
        const renderModel = model || this.model;
        //
        //   <fonts count="2" x14ac:knownFonts="1">
        xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
        xmlStream.openNode("styleSheet", StylesXform.STYLESHEET_ATTRIBUTES);
        if (this.index) {
            // model has been built by style manager role (contains xml)
            if (renderModel.numFmts && renderModel.numFmts.length) {
                xmlStream.openNode("numFmts", { count: renderModel.numFmts.length });
                renderModel.numFmts.forEach((numFmtXml) => {
                    xmlStream.writeXml(numFmtXml);
                });
                xmlStream.closeNode();
            }
            if (!renderModel.fonts.length) {
                // default (zero) font - use preserved font or fallback to Calibri
                this._addFont(this.defaultFont || {
                    size: 11,
                    color: { theme: 1 },
                    name: "Calibri",
                    family: 2,
                    scheme: "minor"
                });
            }
            xmlStream.openNode("fonts", { count: renderModel.fonts.length, "x14ac:knownFonts": 1 });
            renderModel.fonts.forEach((fontXml) => {
                xmlStream.writeXml(fontXml);
            });
            xmlStream.closeNode();
            xmlStream.openNode("fills", { count: renderModel.fills.length });
            renderModel.fills.forEach((fillXml) => {
                xmlStream.writeXml(fillXml);
            });
            xmlStream.closeNode();
            xmlStream.openNode("borders", { count: renderModel.borders.length });
            renderModel.borders.forEach((borderXml) => {
                xmlStream.writeXml(borderXml);
            });
            xmlStream.closeNode();
            this.map.cellStyleXfs.render(xmlStream, [
                { numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0 }
            ]);
            xmlStream.openNode("cellXfs", { count: renderModel.styles.length });
            renderModel.styles.forEach((styleXml) => {
                xmlStream.writeXml(styleXml);
            });
            xmlStream.closeNode();
        }
        else {
            // model is plain JSON and needs to be xformed
            this.map.numFmts.render(xmlStream, renderModel.numFmts);
            this.map.fonts.render(xmlStream, renderModel.fonts);
            this.map.fills.render(xmlStream, renderModel.fills);
            this.map.borders.render(xmlStream, renderModel.borders);
            this.map.cellStyleXfs.render(xmlStream, [
                { numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0 }
            ]);
            this.map.cellXfs.render(xmlStream, renderModel.styles);
        }
        StylesXform.STATIC_XFORMS.cellStyles.render(xmlStream);
        this.map.dxfs.render(xmlStream, renderModel.dxfs);
        StylesXform.STATIC_XFORMS.tableStyles.render(xmlStream);
        StylesXform.STATIC_XFORMS.extLst.render(xmlStream);
        xmlStream.closeNode();
    }
    parseOpen(node) {
        if (this.parser) {
            this.parser.parseOpen(node);
            return true;
        }
        switch (node.name) {
            case "styleSheet":
                this.initIndex();
                return true;
            default:
                this.parser = this.map[node.name];
                if (this.parser) {
                    this.parser.parseOpen(node);
                }
                return true;
        }
    }
    parseText(text) {
        if (this.parser) {
            this.parser.parseText(text);
        }
    }
    parseClose(name) {
        if (this.parser) {
            if (!this.parser.parseClose(name)) {
                this.parser = undefined;
            }
            return true;
        }
        switch (name) {
            case "styleSheet": {
                this.model = {};
                const add = (propName, xform) => {
                    if (xform.model && xform.model.length) {
                        this.model[propName] = xform.model;
                    }
                };
                add("numFmts", this.map.numFmts);
                add("fonts", this.map.fonts);
                add("fills", this.map.fills);
                add("borders", this.map.borders);
                add("styles", this.map.cellXfs);
                add("dxfs", this.map.dxfs);
                // preserve the default (first) font from the original file
                if (this.map.fonts.model && this.map.fonts.model.length > 0) {
                    this.defaultFont = this.map.fonts.model[0];
                }
                // index numFmts
                this.index = {
                    model: [],
                    numFmt: {}
                };
                if (this.model.numFmts) {
                    const numFmtIndex = this.index.numFmt;
                    this.model.numFmts.forEach((numFmt) => {
                        numFmtIndex[numFmt.id] = numFmt.formatCode;
                    });
                }
                return false;
            }
            default:
                // not quite sure how we get here!
                return true;
        }
    }
    // add a cell's style model to the collection
    // each style property is processed and cross-referenced, etc.
    // the styleId is returned. Note: cellType is used when numFmt not defined
    addStyleModel(model, cellType) {
        if (!model) {
            return 0;
        }
        // if we have no default font, add it here now
        if (!this.model.fonts.length) {
            // default (zero) font - use preserved font or fallback to Calibri
            this._addFont(this.defaultFont || {
                size: 11,
                color: { theme: 1 },
                name: "Calibri",
                family: 2,
                scheme: "minor"
            });
        }
        const type = cellType || enums_1.Enums.ValueType.Number;
        // If we have seen this style object before, assume it has the same styleId.
        // Do not cache by object identity for checkbox cells because the styleId must
        // include checkbox-specific extLst, and the same style object may be reused
        // for non-checkbox cells.
        if (type !== enums_1.Enums.ValueType.Checkbox && this.weakMap && this.weakMap.has(model)) {
            return this.weakMap.get(model);
        }
        const style = {};
        if (model.numFmt) {
            style.numFmtId = this._addNumFmtStr(model.numFmt);
        }
        else {
            switch (type) {
                case enums_1.Enums.ValueType.Number:
                    style.numFmtId = this._addNumFmtStr("General");
                    break;
                case enums_1.Enums.ValueType.Date:
                    style.numFmtId = this._addNumFmtStr("mm-dd-yy");
                    break;
                default:
                    break;
            }
        }
        if (model.font) {
            style.fontId = this._addFont(model.font);
        }
        if (model.border) {
            style.borderId = this._addBorder(model.border);
        }
        if (model.fill) {
            style.fillId = this._addFill(model.fill);
        }
        if (model.alignment) {
            style.alignment = model.alignment;
        }
        if (model.protection) {
            style.protection = model.protection;
        }
        // Preserve xf-level attributes (pivotButton, apply* flags)
        const xfFlags = [
            "pivotButton",
            "applyNumberFormat",
            "applyFont",
            "applyFill",
            "applyBorder",
            "applyAlignment",
            "applyProtection"
        ];
        for (const flag of xfFlags) {
            if (model[flag]) {
                style[flag] = true;
            }
        }
        if (type === enums_1.Enums.ValueType.Checkbox) {
            // Checkbox rendering relies on style extensions (extLst) and workbook-level parts.
            // Force applyAlignment="1" (without emitting an <alignment/> node) by providing
            // an empty alignment object when none is specified.
            this._hasCheckboxes = true;
            style.alignment = style.alignment || {};
            style.checkbox = true;
            style.xfComplementIndex = 0;
        }
        const styleId = this._addStyle(style);
        if (type !== enums_1.Enums.ValueType.Checkbox && this.weakMap) {
            this.weakMap.set(model, styleId);
        }
        return styleId;
    }
    // given a styleId (i.e. s="n"), get the cell's style model
    // objects are shared where possible.
    getStyleModel(id) {
        // if the style doesn't exist return null
        const style = this.model.styles[id];
        if (!style) {
            return null;
        }
        // have we built this model before?
        let model = this.index.model[id];
        if (model) {
            return model;
        }
        // build a new model
        model = this.index.model[id] = {};
        // -------------------------------------------------------
        // number format
        if (style.numFmtId) {
            const numFmt = this.index.numFmt[style.numFmtId] ||
                numfmt_xform_1.NumFmtXform.getDefaultFmtCode(style.numFmtId);
            if (numFmt) {
                model.numFmt = numFmt;
            }
        }
        function addStyle(name, group, styleId) {
            if (styleId || styleId === 0) {
                const part = group[styleId];
                if (part) {
                    model[name] = part;
                }
            }
        }
        addStyle("font", this.model.fonts, style.fontId);
        addStyle("border", this.model.borders, style.borderId);
        addStyle("fill", this.model.fills, style.fillId);
        // -------------------------------------------------------
        // alignment
        if (style.alignment) {
            model.alignment = style.alignment;
        }
        // -------------------------------------------------------
        // protection
        if (style.protection) {
            model.protection = style.protection;
        }
        // -------------------------------------------------------
        // xf-level attributes (pivotButton, apply* flags)
        const xfFlags = [
            "pivotButton",
            "applyNumberFormat",
            "applyFont",
            "applyFill",
            "applyBorder",
            "applyAlignment",
            "applyProtection"
        ];
        for (const flag of xfFlags) {
            if (style[flag]) {
                model[flag] = true;
            }
        }
        return model;
    }
    addDxfStyle(style) {
        if (style.numFmt) {
            // register numFmtId to use it during dxf-xform rendering
            style.numFmtId = this._addNumFmtStr(style.numFmt);
        }
        this.model.dxfs.push(style);
        return this.model.dxfs.length - 1;
    }
    getDxfStyle(id) {
        return this.model.dxfs[id];
    }
    // Check if workbook uses checkbox feature
    get hasCheckboxes() {
        return !!this._hasCheckboxes;
    }
    // =========================================================================
    // Private Interface
    _addStyle(style) {
        const xml = this.map.style.toXml(style);
        let index = this.index.style[xml];
        if (index === undefined) {
            index = this.index.style[xml] = this.model.styles.length;
            this.model.styles.push(xml);
        }
        return index;
    }
    // =========================================================================
    // Number Formats
    _addNumFmtStr(formatCode) {
        // check if default format
        let index = numfmt_xform_1.NumFmtXform.getDefaultFmtId(formatCode);
        if (index !== undefined) {
            return index;
        }
        // check if already in
        index = this.index.numFmt[formatCode];
        if (index !== undefined) {
            return index;
        }
        index = this.index.numFmt[formatCode] = NUMFMT_BASE + this.model.numFmts.length;
        const xml = this.map.numFmt.toXml({ id: index, formatCode });
        this.model.numFmts.push(xml);
        return index;
    }
    // =========================================================================
    // Fonts
    _addFont(font) {
        const xml = this.map.font.toXml(font);
        let index = this.index.font[xml];
        if (index === undefined) {
            index = this.index.font[xml] = this.model.fonts.length;
            this.model.fonts.push(xml);
        }
        return index;
    }
    // =========================================================================
    // Borders
    _addBorder(border) {
        const xml = this.map.border.toXml(border);
        let index = this.index.border[xml];
        if (index === undefined) {
            index = this.index.border[xml] = this.model.borders.length;
            this.model.borders.push(xml);
        }
        return index;
    }
    // =========================================================================
    // Fills
    _addFill(fill) {
        const xml = this.map.fill.toXml(fill);
        let index = this.index.fill[xml];
        if (index === undefined) {
            index = this.index.fill[xml] = this.model.fills.length;
            this.model.fills.push(xml);
        }
        return index;
    }
}
exports.StylesXform = StylesXform;
// =========================================================================
StylesXform.STYLESHEET_ATTRIBUTES = {
    xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
    "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
    "mc:Ignorable": "x14ac x16r2",
    "xmlns:x14ac": "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac",
    "xmlns:x16r2": "http://schemas.microsoft.com/office/spreadsheetml/2015/02/main"
};
StylesXform.STATIC_XFORMS = {
    cellStyles: new static_xform_1.StaticXform({
        tag: "cellStyles",
        $: { count: 1 },
        c: [{ tag: "cellStyle", $: { name: "Normal", xfId: 0, builtinId: 0 } }]
    }),
    dxfs: new static_xform_1.StaticXform({ tag: "dxfs", $: { count: 0 } }),
    tableStyles: new static_xform_1.StaticXform({
        tag: "tableStyles",
        $: {
            count: 0,
            defaultTableStyle: "TableStyleMedium2",
            defaultPivotStyle: "PivotStyleLight16"
        }
    }),
    extLst: new static_xform_1.StaticXform({
        tag: "extLst",
        c: [
            {
                tag: "ext",
                $: {
                    uri: "{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}",
                    "xmlns:x14": "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"
                },
                c: [{ tag: "x14:slicerStyles", $: { defaultSlicerStyle: "SlicerStyleLight1" } }]
            },
            {
                tag: "ext",
                $: {
                    uri: "{9260A510-F301-46a8-8635-F512D64BE5F5}",
                    "xmlns:x15": "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"
                },
                c: [{ tag: "x15:timelineStyles", $: { defaultTimelineStyle: "TimeSlicerStyleLight1" } }]
            }
        ]
    })
};
// the stylemanager mock acts like StyleManager except that it always returns 0 or {}
class StylesXformMock extends StylesXform {
    constructor() {
        super();
        this.model = {
            styles: [{ numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0 }],
            numFmts: [],
            fonts: [{ size: 11, color: { theme: 1 }, name: "Calibri", family: 2, scheme: "minor" }],
            borders: [{}],
            fills: [
                { type: "pattern", pattern: "none" },
                { type: "pattern", pattern: "gray125" }
            ]
        };
    }
    // =========================================================================
    // Style Manager Interface
    // override normal behaviour - consume and dispose
    parseStream(stream) {
        stream.autodrain();
        return Promise.resolve();
    }
    // add a cell's style model to the collection
    // each style property is processed and cross-referenced, etc.
    // the styleId is returned. Note: cellType is used when numFmt not defined
    addStyleModel(model, cellType) {
        switch (cellType) {
            case enums_1.Enums.ValueType.Checkbox:
                // Checkbox rendering relies on style extensions (extLst) and workbook-level parts.
                // The mock style manager intentionally does not build those structures.
                throw new Error("Checkbox cells require styles to be enabled (useStyles: true)");
            case enums_1.Enums.ValueType.Date:
                return this.dateStyleId;
            default:
                return 0;
        }
    }
    get hasCheckboxes() {
        return false;
    }
    get dateStyleId() {
        if (!this._dateStyleId) {
            const dateStyle = {
                numFmtId: numfmt_xform_1.NumFmtXform.getDefaultFmtId("mm-dd-yy")
            };
            this._dateStyleId = this.model.styles.length;
            this.model.styles.push(dateStyle);
        }
        return this._dateStyleId;
    }
    // given a styleId (i.e. s="n"), get the cell's style model
    // objects are shared where possible.
    getStyleModel( /* id */) {
        return {};
    }
}
// Assign Mock after class declaration to avoid "used before declaration" error
StylesXform.Mock = StylesXformMock;
