
/**
 * This file is generated by o-spreadsheet build tools. Do not edit it.
 * @see https://github.com/odoo/o-spreadsheet
 * @version 17.0.12
 * @date 2024-02-16T15:05:31.161Z
 * @hash 8fa2bab99
 */

(function (exports, owl) {
    'use strict';

    // define a mock translation function, when o-spreadsheet runs in standalone it doesn't translate any string
    let _translate = (s) => s;
    let _loaded = () => false;
    function sprintf(s, ...values) {
        if (values.length === 1 && typeof values[0] === "object" && !(values[0] instanceof String)) {
            const valuesDict = values[0];
            s = s.replace(/\%\(([^\)]+)\)s/g, (match, value) => valuesDict[value]);
        }
        else if (values.length > 0) {
            s = s.replace(/\%s/g, () => values.shift());
        }
        return s;
    }
    /***
     * Allow to inject a translation function from outside o-spreadsheet.
     * @param tfn the function that will do the translation
     * @param loaded a function that returns true when the translation is loaded
     */
    function setTranslationMethod(tfn, loaded = () => true) {
        _translate = tfn;
        _loaded = loaded;
    }
    const _t = function (s, ...values) {
        if (!_loaded()) {
            return new LazyTranslatedString(s, values);
        }
        return sprintf(_translate(s), ...values);
    };
    class LazyTranslatedString extends String {
        values;
        constructor(str, values) {
            super(str);
            this.values = values;
        }
        valueOf() {
            const str = super.valueOf();
            return _loaded() ? sprintf(_translate(str), ...this.values) : str;
        }
        toString() {
            return this.valueOf();
        }
    }

    var CellErrorType;
    (function (CellErrorType) {
        CellErrorType["NotAvailable"] = "#N/A";
        CellErrorType["InvalidReference"] = "#REF";
        CellErrorType["BadExpression"] = "#BAD_EXPR";
        CellErrorType["CircularDependency"] = "#CYCLE";
        CellErrorType["UnknownFunction"] = "#NAME?";
        CellErrorType["GenericError"] = "#ERROR";
    })(CellErrorType || (CellErrorType = {}));
    var CellErrorLevel;
    (function (CellErrorLevel) {
        CellErrorLevel[CellErrorLevel["silent"] = 0] = "silent";
        CellErrorLevel[CellErrorLevel["error"] = 1] = "error";
    })(CellErrorLevel || (CellErrorLevel = {}));
    class EvaluationError extends Error {
        errorType;
        logLevel;
        constructor(errorType, message, logLevel = CellErrorLevel.error) {
            super(message);
            this.errorType = errorType;
            this.logLevel = logLevel;
        }
        get isVerbose() {
            return this.logLevel > CellErrorLevel.silent;
        }
    }
    class BadExpressionError extends EvaluationError {
        constructor(errorMessage) {
            super(CellErrorType.BadExpression, errorMessage);
        }
    }
    class CircularDependencyError extends EvaluationError {
        constructor() {
            super(CellErrorType.CircularDependency, _t("Circular reference"));
        }
    }
    class InvalidReferenceError extends EvaluationError {
        constructor() {
            super(CellErrorType.InvalidReference, _t("Invalid reference"));
        }
    }
    class NotAvailableError extends EvaluationError {
        constructor(errorMessage = undefined) {
            super(CellErrorType.NotAvailable, errorMessage || _t("Data not available"), errorMessage ? CellErrorLevel.error : CellErrorLevel.silent);
        }
    }
    class UnknownFunctionError extends EvaluationError {
        constructor(fctName) {
            super(CellErrorType.UnknownFunction, _t('Unknown function: "%s"', fctName));
        }
    }

    const CANVAS_SHIFT = 0.5;
    // Colors
    const BACKGROUND_GRAY_COLOR = "#f5f5f5";
    const BACKGROUND_HEADER_COLOR = "#F8F9FA";
    const BACKGROUND_HEADER_SELECTED_COLOR = "#E8EAED";
    const BACKGROUND_HEADER_ACTIVE_COLOR = "#595959";
    const TEXT_HEADER_COLOR = "#666666";
    const FIGURE_BORDER_COLOR = "#c9ccd2";
    const SELECTION_BORDER_COLOR = "#3266ca";
    const HEADER_BORDER_COLOR = "#C0C0C0";
    const CELL_BORDER_COLOR = "#E2E3E3";
    const BACKGROUND_CHART_COLOR = "#FFFFFF";
    const BG_HOVER_COLOR = "#EBEBEB";
    const DISABLED_TEXT_COLOR = "#CACACA";
    const DEFAULT_COLOR_SCALE_MIDPOINT_COLOR = 0xb6d7a8;
    const LINK_COLOR = "#01666b";
    const FILTERS_COLOR = "#188038";
    const BACKGROUND_HEADER_FILTER_COLOR = "#E6F4EA";
    const BACKGROUND_HEADER_SELECTED_FILTER_COLOR = "#CEEAD6";
    const SEPARATOR_COLOR = "#E0E2E4";
    const ICONS_COLOR = "#4A4F59";
    const HEADER_GROUPING_BACKGROUND_COLOR = "#F5F5F5";
    const HEADER_GROUPING_BORDER_COLOR = "#999";
    const GRID_BORDER_COLOR = "#E2E3E3";
    const FROZEN_PANE_HEADER_BORDER_COLOR = "#BCBCBC";
    const FROZEN_PANE_BORDER_COLOR = "#DADFE8";
    const COMPOSER_ASSISTANT_COLOR = "#9B359B";
    // Color picker defaults as upper case HEX to match `toHex`helper
    const COLOR_PICKER_DEFAULTS = [
        "#000000",
        "#434343",
        "#666666",
        "#999999",
        "#B7B7B7",
        "#CCCCCC",
        "#D9D9D9",
        "#EFEFEF",
        "#F3F3F3",
        "#FFFFFF",
        "#980000",
        "#FF0000",
        "#FF9900",
        "#FFFF00",
        "#00FF00",
        "#00FFFF",
        "#4A86E8",
        "#0000FF",
        "#9900FF",
        "#FF00FF",
        "#E6B8AF",
        "#F4CCCC",
        "#FCE5CD",
        "#FFF2CC",
        "#D9EAD3",
        "#D0E0E3",
        "#C9DAF8",
        "#CFE2F3",
        "#D9D2E9",
        "#EAD1DC",
        "#DD7E6B",
        "#EA9999",
        "#F9CB9C",
        "#FFE599",
        "#B6D7A8",
        "#A2C4C9",
        "#A4C2F4",
        "#9FC5E8",
        "#B4A7D6",
        "#D5A6BD",
        "#CC4125",
        "#E06666",
        "#F6B26B",
        "#FFD966",
        "#93C47D",
        "#76A5AF",
        "#6D9EEB",
        "#6FA8DC",
        "#8E7CC3",
        "#C27BA0",
        "#A61C00",
        "#CC0000",
        "#E69138",
        "#F1C232",
        "#6AA84F",
        "#45818E",
        "#3C78D8",
        "#3D85C6",
        "#674EA7",
        "#A64D79",
        "#85200C",
        "#990000",
        "#B45F06",
        "#BF9000",
        "#38761D",
        "#134F5C",
        "#1155CC",
        "#0B5394",
        "#351C75",
        "#741B47",
        "#5B0F00",
        "#660000",
        "#783F04",
        "#7F6000",
        "#274E13",
        "#0C343D",
        "#1C4587",
        "#073763",
        "#20124D",
        "#4C1130",
    ];
    // Dimensions
    const MIN_ROW_HEIGHT = 10;
    const MIN_COL_WIDTH = 5;
    const HEADER_HEIGHT = 26;
    const HEADER_WIDTH = 48;
    const TOPBAR_HEIGHT = 63;
    const TOPBAR_TOOLBAR_HEIGHT = 34;
    const BOTTOMBAR_HEIGHT = 36;
    const DEFAULT_CELL_WIDTH = 96;
    const DEFAULT_CELL_HEIGHT = 23;
    const SCROLLBAR_WIDTH = 15;
    const AUTOFILL_EDGE_LENGTH = 8;
    const ICON_EDGE_LENGTH = 18;
    const UNHIDE_ICON_EDGE_LENGTH = 14;
    const MIN_CF_ICON_MARGIN = 4;
    const MIN_CELL_TEXT_MARGIN = 4;
    const CF_ICON_EDGE_LENGTH = 15;
    const PADDING_AUTORESIZE_VERTICAL = 3;
    const PADDING_AUTORESIZE_HORIZONTAL = MIN_CELL_TEXT_MARGIN;
    const GROUP_LAYER_WIDTH = 21;
    const GRID_ICON_MARGIN = 2;
    const GRID_ICON_EDGE_LENGTH = 17;
    const FOOTER_HEIGHT = 2 * DEFAULT_CELL_HEIGHT;
    // Menus
    const MENU_WIDTH = 250;
    const MENU_VERTICAL_PADDING = 6;
    const MENU_ITEM_HEIGHT = 26;
    const MENU_ITEM_PADDING_HORIZONTAL = 11;
    const MENU_ITEM_PADDING_VERTICAL = 4;
    const MENU_SEPARATOR_BORDER_WIDTH = 1;
    const MENU_SEPARATOR_PADDING = 5;
    // Style
    const DEFAULT_STYLE = {
        align: "left",
        verticalAlign: "bottom",
        wrapping: "overflow",
        bold: false,
        italic: false,
        strikethrough: false,
        underline: false,
        fontSize: 10,
        fillColor: "",
        textColor: "#000000",
    };
    const DEFAULT_VERTICAL_ALIGN = DEFAULT_STYLE.verticalAlign;
    const DEFAULT_WRAPPING_MODE = DEFAULT_STYLE.wrapping;
    // Fonts
    const DEFAULT_FONT_WEIGHT = "400";
    const DEFAULT_FONT_SIZE = DEFAULT_STYLE.fontSize;
    const HEADER_FONT_SIZE = 11;
    const DEFAULT_FONT = "'Roboto', arial";
    // Borders
    const DEFAULT_BORDER_DESC = { style: "thin", color: "#000000" };
    const DEFAULT_FILTER_BORDER_DESC = { style: "thin", color: FILTERS_COLOR };
    // Ranges
    const INCORRECT_RANGE_STRING = CellErrorType.InvalidReference;
    // Max Number of history steps kept in memory
    const MAX_HISTORY_STEPS = 99;
    // Id of the first revision
    const DEFAULT_REVISION_ID = "START_REVISION";
    // Figure
    const DEFAULT_FIGURE_HEIGHT = 335;
    const DEFAULT_FIGURE_WIDTH = 536;
    const FIGURE_BORDER_WIDTH = 1;
    const MIN_FIG_SIZE = 80;
    // Chart
    const MAX_CHAR_LABEL = 20;
    const FIGURE_ID_SPLITTER = "??";
    const DEFAULT_GAUGE_LOWER_COLOR = "#cc0000";
    const DEFAULT_GAUGE_MIDDLE_COLOR = "#f1c232";
    const DEFAULT_GAUGE_UPPER_COLOR = "#6aa84f";
    const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
    const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#00A04A";
    const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#DC6965";
    const LINE_FILL_TRANSPARENCY = 0.4;
    // session
    const DEBOUNCE_TIME = 200;
    const MESSAGE_VERSION = 1;
    // Sheets
    const FORBIDDEN_SHEET_CHARS = ["'", "*", "?", "/", "\\", "[", "]"];
    const FORBIDDEN_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
    // Cells
    const FORMULA_REF_IDENTIFIER = "|";
    const DEFAULT_ERROR_MESSAGE = _t("Invalid expression");
    // Components
    var ComponentsImportance;
    (function (ComponentsImportance) {
        ComponentsImportance[ComponentsImportance["Grid"] = 0] = "Grid";
        ComponentsImportance[ComponentsImportance["Highlight"] = 5] = "Highlight";
        ComponentsImportance[ComponentsImportance["HeaderGroupingButton"] = 6] = "HeaderGroupingButton";
        ComponentsImportance[ComponentsImportance["Figure"] = 10] = "Figure";
        ComponentsImportance[ComponentsImportance["ScrollBar"] = 15] = "ScrollBar";
        ComponentsImportance[ComponentsImportance["GridPopover"] = 19] = "GridPopover";
        ComponentsImportance[ComponentsImportance["GridComposer"] = 20] = "GridComposer";
        ComponentsImportance[ComponentsImportance["Dropdown"] = 21] = "Dropdown";
        ComponentsImportance[ComponentsImportance["IconPicker"] = 25] = "IconPicker";
        ComponentsImportance[ComponentsImportance["TopBarComposer"] = 30] = "TopBarComposer";
        ComponentsImportance[ComponentsImportance["Popover"] = 35] = "Popover";
        ComponentsImportance[ComponentsImportance["FigureAnchor"] = 1000] = "FigureAnchor";
        ComponentsImportance[ComponentsImportance["FigureSnapLine"] = 1001] = "FigureSnapLine";
    })(ComponentsImportance || (ComponentsImportance = {}));
    let DEFAULT_SHEETVIEW_SIZE = 0;
    function getDefaultSheetViewSize() {
        return DEFAULT_SHEETVIEW_SIZE;
    }
    function setDefaultSheetViewSize(size) {
        DEFAULT_SHEETVIEW_SIZE = size;
    }
    const MAXIMAL_FREEZABLE_RATIO = 0.85;
    const NEWLINE = "\n";
    const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];

    //------------------------------------------------------------------------------
    // Miscellaneous
    //------------------------------------------------------------------------------
    /**
     * Remove quotes from a quoted string
     * ```js
     * removeStringQuotes('"Hello"')
     * > 'Hello'
     * ```
     */
    function removeStringQuotes(str) {
        if (str[0] === '"') {
            str = str.slice(1);
        }
        if (str[str.length - 1] === '"' && str[str.length - 2] !== "\\") {
            return str.slice(0, str.length - 1);
        }
        return str;
    }
    function isCloneable(obj) {
        return "clone" in obj && obj.clone instanceof Function;
    }
    /**
     * Escapes a string to use as a literal string in a RegExp.
     * @url https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
     */
    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }
    /**
     * Deep copy arrays, plain objects and primitive values.
     * Throws an error for other types such as class instances.
     * Sparse arrays remain sparse.
     */
    function deepCopy(obj) {
        const result = Array.isArray(obj) ? [] : {};
        switch (typeof obj) {
            case "object": {
                if (obj === null) {
                    return obj;
                }
                else if (isCloneable(obj)) {
                    return obj.clone();
                }
                else if (!(isPlainObject(obj) || obj instanceof Array)) {
                    throw new Error("Unsupported type: only objects and arrays are supported");
                }
                for (const key in obj) {
                    result[key] = deepCopy(obj[key]);
                }
                return result;
            }
            case "number":
            case "string":
            case "boolean":
            case "function":
            case "undefined":
                return obj;
            default:
                throw new Error(`Unsupported type: ${typeof obj}`);
        }
    }
    /**
     * Check if the object is a plain old javascript object.
     */
    function isPlainObject(obj) {
        return typeof obj === "object" && obj?.constructor === Object;
    }
    /**
     * Sanitize the name of a sheet, by eventually removing quotes
     * @param sheetName name of the sheet, potentially quoted with single quotes
     */
    function getUnquotedSheetName(sheetName) {
        if (sheetName.startsWith("'")) {
            sheetName = sheetName.slice(1, -1).replace(/''/g, "'");
        }
        return sheetName;
    }
    /**
     * Add quotes around the sheet name if it contains at least one non alphanumeric character
     * '\w' captures [0-9][a-z][A-Z] and _.
     * @param sheetName Name of the sheet
     */
    function getCanonicalSheetName(sheetName) {
        if (sheetName.match(/\w/g)?.length !== sheetName.length) {
            sheetName = `'${sheetName}'`;
        }
        return sheetName;
    }
    function clip(val, min, max) {
        return val < min ? min : val > max ? max : val;
    }
    /**
     * Create a range from start (included) to end (excluded).
     * range(10, 13) => [10, 11, 12]
     * range(2, 8, 2) => [2, 4, 6]
     */
    function range(start, end, step = 1) {
        if (end <= start && step > 0) {
            return [];
        }
        if (step === 0) {
            throw new Error("range() step must not be zero");
        }
        const length = Math.ceil(Math.abs((end - start) / step));
        const array = Array(length);
        for (let i = 0; i < length; i++) {
            array[i] = start + i * step;
        }
        return array;
    }
    /**
     * Groups consecutive numbers.
     * The input array is assumed to be sorted
     * @param numbers
     */
    function groupConsecutive(numbers) {
        return numbers.reduce((groups, currentRow, index, rows) => {
            if (Math.abs(currentRow - rows[index - 1]) === 1) {
                const lastGroup = groups[groups.length - 1];
                lastGroup.push(currentRow);
            }
            else {
                groups.push([currentRow]);
            }
            return groups;
        }, []);
    }
    /**
     * Create one generator from two generators by linking
     * each item of the first generator to the next item of
     * the second generator.
     *
     * Let's say generator G1 yields A, B, C and generator G2 yields X, Y, Z.
     * The resulting generator of `linkNext(G1, G2)` will yield A', B', C'
     * where `A' = A & {next: Y}`, `B' = B & {next: Z}` and `C' = C & {next: undefined}`
     * @param generator
     * @param nextGenerator
     */
    function* linkNext(generator, nextGenerator) {
        nextGenerator.next();
        for (const item of generator) {
            const nextItem = nextGenerator.next();
            yield {
                ...item,
                next: nextItem.done ? undefined : nextItem.value,
            };
        }
    }
    function isBoolean(str) {
        const upperCased = str.toUpperCase();
        return upperCased === "TRUE" || upperCased === "FALSE";
    }
    const MARKDOWN_LINK_REGEX = /^\[(.+)\]\((.+)\)$/;
    //link must start with http or https
    //https://stackoverflow.com/a/3809435/4760614
    const WEB_LINK_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/;
    function isMarkdownLink(str) {
        return MARKDOWN_LINK_REGEX.test(str);
    }
    /**
     * Check if the string is a web link.
     * e.g. http://odoo.com
     */
    function isWebLink(str) {
        return WEB_LINK_REGEX.test(str);
    }
    /**
     * Build a markdown link from a label and an url
     */
    function markdownLink(label, url) {
        return `[${label}](${url})`;
    }
    function parseMarkdownLink(str) {
        const matches = str.match(MARKDOWN_LINK_REGEX) || [];
        const label = matches[1];
        const url = matches[2];
        if (!label || !url) {
            throw new Error(`Could not parse markdown link ${str}.`);
        }
        return {
            label,
            url,
        };
    }
    const O_SPREADSHEET_LINK_PREFIX = "o-spreadsheet://";
    function isSheetUrl(url) {
        return url.startsWith(O_SPREADSHEET_LINK_PREFIX);
    }
    function buildSheetLink(sheetId) {
        return `${O_SPREADSHEET_LINK_PREFIX}${sheetId}`;
    }
    /**
     * Parse a sheet link and return the sheet id
     */
    function parseSheetUrl(sheetLink) {
        if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) {
            return sheetLink.substr(O_SPREADSHEET_LINK_PREFIX.length);
        }
        throw new Error(`${sheetLink} is not a valid sheet link`);
    }
    /**
     * This helper function can be used as a type guard when filtering arrays.
     * const foo: number[] = [1, 2, undefined, 4].filter(isDefined)
     */
    function isDefined$1(argument) {
        return argument !== undefined;
    }
    function isNotNull(argument) {
        return argument !== null;
    }
    /**
     * Check if all the values of an object, and all the values of the objects inside of it, are undefined.
     */
    function isObjectEmptyRecursive(argument) {
        if (argument === undefined)
            return true;
        return Object.values(argument).every((value) => typeof value === "object" ? isObjectEmptyRecursive(value) : !value);
    }
    /**
     * Get the id of the given item (its key in the given dictionnary).
     * If the given item does not exist in the dictionary, it creates one with a new id.
     */
    function getItemId(item, itemsDic) {
        for (const key in itemsDic) {
            if (deepEquals(itemsDic[key], item)) {
                return parseInt(key, 10);
            }
        }
        // Generate new Id if the item didn't exist in the dictionary
        const ids = Object.keys(itemsDic);
        const maxId = ids.length === 0 ? 0 : Math.max(...ids.map((id) => parseInt(id, 10)));
        itemsDic[maxId + 1] = item;
        return maxId + 1;
    }
    /**
     * This method comes from owl 1 as it was removed in owl 2
     *
     * Returns a function, that, as long as it continues to be invoked, will not
     * be triggered. The function will be called after it stops being called for
     * N milliseconds. If `immediate` is passed, trigger the function on the
     * leading edge, instead of the trailing.
     *
     * Inspired by https://davidwalsh.name/javascript-debounce-function
     */
    function debounce(func, wait, immediate) {
        let timeout;
        return function () {
            const context = this;
            const args = arguments;
            function later() {
                timeout = null;
                if (!immediate) {
                    func.apply(context, args);
                }
            }
            const callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) {
                func.apply(context, args);
            }
        };
    }
    /*
     * Concatenate an array of strings.
     */
    function concat(chars) {
        // ~40% faster than chars.join("")
        let output = "";
        for (let i = 0, len = chars.length; i < len; i++) {
            output += chars[i];
        }
        return output;
    }
    /**
     * Lazy value computed by the provided function.
     */
    function lazy(fn) {
        let isMemoized = false;
        let memo;
        const lazyValue = () => {
            if (!isMemoized) {
                memo = fn instanceof Function ? fn() : fn;
                isMemoized = true;
            }
            return memo;
        };
        lazyValue.map = (callback) => lazy(() => callback(lazyValue()));
        return lazyValue;
    }
    /**
     * Find the next defined value after the given index in an array of strings. If there is no defined value
     * after the index, return the closest defined value before the index. Return an empty string if no
     * defined value was found.
     *
     */
    function findNextDefinedValue(arr, index) {
        let value = arr.slice(index).find((val) => val);
        if (!value) {
            value = arr
                .slice(0, index)
                .reverse()
                .find((val) => val);
        }
        return value || "";
    }
    /** Get index of first header added by an ADD_COLUMNS_ROWS command */
    function getAddHeaderStartIndex(position, base) {
        return position === "after" ? base + 1 : base;
    }
    /**
     * Compares two objects.
     */
    function deepEquals(o1, o2) {
        if (o1 === o2)
            return true;
        if ((o1 && !o2) || (o2 && !o1))
            return false;
        if (typeof o1 !== typeof o2)
            return false;
        if (typeof o1 !== "object")
            return o1 === o2;
        // Objects can have different keys if the values are undefined
        for (const key in o2) {
            if (!(key in o1) && o2[key] !== undefined) {
                return false;
            }
        }
        for (const key in o1) {
            if (typeof o1[key] !== typeof o2[key])
                return false;
            if (typeof o1[key] === "object") {
                if (!deepEquals(o1[key], o2[key]))
                    return false;
            }
            else {
                if (o1[key] !== o2[key])
                    return false;
            }
        }
        return true;
    }
    /** Check if the given array contains all the values of the other array. */
    function includesAll(arr, values) {
        return values.every((value) => arr.includes(value));
    }
    /**
     * Return an object with all the keys in the object that have a falsy value removed.
     */
    function removeFalsyAttributes(obj) {
        const cleanObject = { ...obj };
        Object.keys(cleanObject).forEach((key) => !cleanObject[key] && delete cleanObject[key]);
        return cleanObject;
    }
    /**
     * Equivalent to "\s" in regexp, minus the new lines characters
     *
     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
     */
    const whiteSpaceSpecialCharacters = [
        "\t",
        "\f",
        "\v",
        String.fromCharCode(parseInt("00a0", 16)),
        String.fromCharCode(parseInt("1680", 16)),
        String.fromCharCode(parseInt("2000", 16)),
        String.fromCharCode(parseInt("200a", 16)),
        String.fromCharCode(parseInt("2028", 16)),
        String.fromCharCode(parseInt("2029", 16)),
        String.fromCharCode(parseInt("202f", 16)),
        String.fromCharCode(parseInt("205f", 16)),
        String.fromCharCode(parseInt("3000", 16)),
        String.fromCharCode(parseInt("feff", 16)),
    ];
    const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|") + "|(\r\n|\r|\n)", "g");
    /**
     * Replace all the special spaces in a string (non-breaking, tabs, ...) by normal spaces, and all the
     * different newlines types by \n.
     */
    function replaceSpecialSpaces(text) {
        if (!text)
            return "";
        if (!whiteSpaceRegexp.test(text))
            return text;
        return text.replace(whiteSpaceRegexp, (match, newLine) => (newLine ? NEWLINE : " "));
    }
    /**
     * Determine if the numbers are consecutive.
     */
    function isConsecutive(iterable) {
        const array = Array.from(iterable).sort((a, b) => a - b); // sort numerically rather than lexicographically
        for (let i = 1; i < array.length; i++) {
            if (array[i] - array[i - 1] !== 1) {
                return false;
            }
        }
        return true;
    }
    class JetSet extends Set {
        addMany(iterable) {
            for (const element of iterable) {
                super.add(element);
            }
            return this;
        }
        deleteMany(iterable) {
            let wasDeleted = false;
            for (const element of iterable) {
                wasDeleted ||= super.delete(element);
            }
            return wasDeleted;
        }
    }
    /**
     * Creates a version of the function that's memoized on the value of its first
     * argument, if any.
     */
    function memoize(func) {
        const cache = new Map();
        const funcName = func.name ? func.name + " (memoized)" : "memoized";
        return {
            [funcName](...args) {
                if (!cache.has(args[0])) {
                    cache.set(args[0], func(...args));
                }
                return cache.get(args[0]);
            },
        }[funcName];
    }
    function removeIndexesFromArray(array, indexes) {
        return array.filter((_, index) => !indexes.includes(index));
    }
    function insertItemsAtIndex(array, items, index) {
        const newArray = [...array];
        newArray.splice(index, 0, ...items);
        return newArray;
    }
    function trimContent(content) {
        const contentLines = content.split("\n");
        return contentLines.map((line) => line.replace(/\s+/g, " ").trim()).join("\n");
    }
    function isNumberBetween(value, min, max) {
        if (min > max) {
            return isNumberBetween(value, max, min);
        }
        return value >= min && value <= max;
    }

    const RBA_REGEX = /rgba?\(|\s+|\)/gi;
    const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
    const colors$1 = [
        "#eb6d00",
        "#0074d9",
        "#ad8e00",
        "#169ed4",
        "#b10dc9",
        "#00a82d",
        "#00a3a3",
        "#f012be",
        "#3d9970",
        "#111111",
        "#62A300",
        "#ff4136",
        "#949494",
        "#85144b",
        "#001f3f",
    ];
    /*
     * transform a color number (R * 256^2 + G * 256 + B) into classic hex6 value
     * */
    function colorNumberString(color) {
        return toHex(color.toString(16).padStart(6, "0"));
    }
    /**
     * Converts any CSS color value to a standardized hex6 value.
     * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
     *
     * [1] under the form rgb(r, g, b, a?) or rgba(r, g, b, a?)
     * with r,g,b ∈ [0, 255] and a ∈ [0, 1]
     *
     * toHex("#ABC")
     * >> "#AABBCC"
     *
     * toHex("#AAAFFF")
     * >> "#AAAFFF"
     *
     * toHex("rgb(30, 80, 16)")
     * >> "#1E5010"
     *
     *  * toHex("rgb(30, 80, 16, 0.5)")
     * >> "#1E501080"
     *
     */
    function toHex(color) {
        let hexColor = color;
        if (color.startsWith("rgb")) {
            hexColor = rgbaStringToHex(color);
        }
        else {
            hexColor = color.replace("#", "").toUpperCase();
            if (hexColor.length === 3 || hexColor.length === 4) {
                hexColor = hexColor.split("").reduce((acc, h) => acc + h + h, "");
            }
            hexColor = `#${hexColor}`;
        }
        if (!HEX_MATCH.test(hexColor)) {
            throw new Error(`invalid color input: ${color}`);
        }
        return hexColor;
    }
    function isColorValid(color) {
        try {
            toHex(color);
            return true;
        }
        catch (error) {
            return false;
        }
    }
    function isHSLAValid(color) {
        try {
            hslaToHex(color);
            return true;
        }
        catch (error) {
            return false;
        }
    }
    const isColorValueValid = (v) => v >= 0 && v <= 255;
    function rgba(r, g, b, a = 1) {
        const isInvalid = !isColorValueValid(r) || !isColorValueValid(g) || !isColorValueValid(b) || a < 0 || a > 1;
        if (isInvalid) {
            throw new Error(`Invalid RGBA values ${[r, g, b, a]}`);
        }
        return { a, b, g, r };
    }
    /**
     * The relative brightness of a point in the colorspace, normalized to 0 for
     * darkest black and 1 for lightest white.
     * https://www.w3.org/TR/WCAG20/#relativeluminancedef
     */
    function relativeLuminance(color) {
        let { r, g, b } = colorToRGBA(color);
        r /= 255;
        g /= 255;
        b /= 255;
        const toLinearValue = (c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
        const R = toLinearValue(r);
        const G = toLinearValue(g);
        const B = toLinearValue(b);
        return 0.2126 * R + 0.7152 * G + 0.0722 * B;
    }
    /**
     * Convert a CSS rgb color string to a standardized hex6 color value.
     *
     * rgbaStringToHex("rgb(30, 80, 16)")
     * >> "#1E5010"
     *
     * rgbaStringToHex("rgba(30, 80, 16, 0.5)")
     * >> "#1E501080"
     *
     * DOES NOT SUPPORT NON INTEGER RGB VALUES
     */
    function rgbaStringToHex(color) {
        const stringVals = color.replace(RBA_REGEX, "").split(",");
        let alphaHex = 255;
        if (stringVals.length !== 3 && stringVals.length !== 4) {
            throw new Error("invalid color");
        }
        else if (stringVals.length === 4) {
            const alpha = parseFloat(stringVals.pop() || "1");
            alphaHex = Math.round((alpha || 1) * 255);
        }
        const vals = stringVals.map((val) => parseInt(val, 10));
        if (alphaHex !== 255) {
            vals.push(alphaHex);
        }
        return "#" + concat(vals.map((value) => value.toString(16).padStart(2, "0"))).toUpperCase();
    }
    /**
     * RGBA to HEX representation (#RRGGBBAA).
     *
     * https://css-tricks.com/converting-color-spaces-in-javascript/
     */
    function rgbaToHex(rgba) {
        let r = rgba.r.toString(16);
        let g = rgba.g.toString(16);
        let b = rgba.b.toString(16);
        let a = Math.round(rgba.a * 255).toString(16);
        if (r.length === 1)
            r = "0" + r;
        if (g.length === 1)
            g = "0" + g;
        if (b.length === 1)
            b = "0" + b;
        if (a.length === 1)
            a = "0" + a;
        if (a === "ff")
            a = "";
        return ("#" + r + g + b + a).toUpperCase();
    }
    /**
     * Color string to RGBA representation
     */
    function colorToRGBA(color) {
        color = toHex(color);
        let r;
        let g;
        let b;
        let a;
        if (color.length === 7) {
            r = parseInt(color[1] + color[2], 16);
            g = parseInt(color[3] + color[4], 16);
            b = parseInt(color[5] + color[6], 16);
            a = 255;
        }
        else if (color.length === 9) {
            r = parseInt(color[1] + color[2], 16);
            g = parseInt(color[3] + color[4], 16);
            b = parseInt(color[5] + color[6], 16);
            a = parseInt(color[7] + color[8], 16);
        }
        else {
            throw new Error("Invalid color");
        }
        a = +(a / 255).toFixed(3);
        return { a, r, g, b };
    }
    /**
     * HSLA to RGBA.
     *
     * https://css-tricks.com/converting-color-spaces-in-javascript/
     */
    function hslaToRGBA(hsla) {
        hsla = { ...hsla };
        // Must be fractions of 1
        hsla.s /= 100;
        hsla.l /= 100;
        let c = (1 - Math.abs(2 * hsla.l - 1)) * hsla.s;
        let x = c * (1 - Math.abs(((hsla.h / 60) % 2) - 1));
        let m = hsla.l - c / 2;
        let r = 0;
        let g = 0;
        let b = 0;
        if (0 <= hsla.h && hsla.h < 60) {
            r = c;
            g = x;
            b = 0;
        }
        else if (60 <= hsla.h && hsla.h < 120) {
            r = x;
            g = c;
            b = 0;
        }
        else if (120 <= hsla.h && hsla.h < 180) {
            r = 0;
            g = c;
            b = x;
        }
        else if (180 <= hsla.h && hsla.h < 240) {
            r = 0;
            g = x;
            b = c;
        }
        else if (240 <= hsla.h && hsla.h < 300) {
            r = x;
            g = 0;
            b = c;
        }
        else if (300 <= hsla.h && hsla.h < 360) {
            r = c;
            g = 0;
            b = x;
        }
        r = Math.round((r + m) * 255);
        g = Math.round((g + m) * 255);
        b = Math.round((b + m) * 255);
        return { a: hsla.a, r, g, b };
    }
    /**
     * HSLA to RGBA.
     *
     * https://css-tricks.com/converting-color-spaces-in-javascript/
     */
    function rgbaToHSLA(rgba) {
        // Make r, g, and b fractions of 1
        const r = rgba.r / 255;
        const g = rgba.g / 255;
        const b = rgba.b / 255;
        // Find greatest and smallest channel values
        let cMin = Math.min(r, g, b);
        let cMax = Math.max(r, g, b);
        let delta = cMax - cMin;
        let h = 0;
        let s = 0;
        let l = 0;
        // Calculate hue
        // No difference
        if (delta === 0)
            h = 0;
        // Red is max
        else if (cMax === r)
            h = ((g - b) / delta) % 6;
        // Green is max
        else if (cMax === g)
            h = (b - r) / delta + 2;
        // Blue is max
        else
            h = (r - g) / delta + 4;
        h = Math.round(h * 60);
        // Make negative hues positive behind 360°
        if (h < 0)
            h += 360;
        l = (cMax + cMin) / 2;
        // Calculate saturation
        s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
        // Multiply l and s by 100
        s = +(s * 100).toFixed(1);
        l = +(l * 100).toFixed(1);
        return { a: rgba.a, h, s, l };
    }
    function hslaToHex(hsla) {
        return rgbaToHex(hslaToRGBA(hsla));
    }
    function hexToHSLA(hex) {
        return rgbaToHSLA(colorToRGBA(hex));
    }
    /**
     * Will compare two color strings
     * A tolerance can be provided to account for small differences that could
     * be introduced by non-bijective transformations between color spaces.
     *
     * E.g. HSV <-> RGB is not a bijection
     *
     * Note that the tolerance is applied on the euclidean distance between
     * the two **normalized** color values.
     */
    function isSameColor(color1, color2, tolerance = 0) {
        if (!(isColorValid(color1) && isColorValid(color2))) {
            return false;
        }
        const rgb1 = colorToRGBA(color1);
        const rgb2 = colorToRGBA(color2);
        // alpha cannot differ as it is not impacted by transformations
        if (rgb1.a !== rgb2.a) {
            return false;
        }
        const diff = Math.sqrt(((rgb1.r - rgb2.r) / 255) ** 2 + ((rgb1.g - rgb2.g) / 255) ** 2 + ((rgb1.b - rgb2.b) / 255) ** 2);
        return diff <= tolerance;
    }

    //------------------------------------------------------------------------------
    // Coordinate
    //------------------------------------------------------------------------------
    /**
     * Convert a (col) number to the corresponding letter.
     *
     * Examples:
     *     0 => 'A'
     *     25 => 'Z'
     *     26 => 'AA'
     *     27 => 'AB'
     */
    function numberToLetters(n) {
        if (n < 0) {
            throw new Error(`number must be positive. Got ${n}`);
        }
        if (n < 26) {
            return String.fromCharCode(65 + n);
        }
        else {
            return numberToLetters(Math.floor(n / 26) - 1) + numberToLetters(n % 26);
        }
    }
    const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const LETTERS_NUMBER_MAPPING = {};
    for (const letter of LETTERS) {
        const colIndex = letter.charCodeAt(0) - 64;
        LETTERS_NUMBER_MAPPING[letter] = colIndex;
        LETTERS_NUMBER_MAPPING[letter.toLowerCase()] = colIndex;
    }
    /**
     * Convert a string (describing a column) to its number value.
     *
     * Examples:
     *     'A' => 0
     *     'Z' => 25
     *     'AA' => 26
     */
    function lettersToNumber(letters) {
        let result = -1;
        const l = letters.length;
        let pow = 1;
        for (let i = l - 1; i >= 0; i--) {
            const charCode = LETTERS_NUMBER_MAPPING[letters[i]];
            result += charCode * pow;
            pow *= 26;
        }
        return result;
    }
    function isCharALetter(char) {
        return (char >= "A" && char <= "Z") || (char >= "a" && char <= "z");
    }
    function isCharADigit(char) {
        return char >= "0" && char <= "9";
    }
    /**
     * Convert a "XC" coordinate to cartesian coordinates.
     *
     * Examples:
     *   A1 => [0,0]
     *   B3 => [1,2]
     *
     * Note: it also accepts lowercase coordinates, but not fixed references
     */
    function toCartesian(xc) {
        xc = xc.trim();
        let numberPartStart = undefined;
        // Note: looping by hand is uglier but ~2x faster than using a regex to match number/letter parts
        for (let i = 0; i < xc.length; i++) {
            const char = xc[i];
            // as long as we haven't found the number part, keep advancing
            if (!numberPartStart) {
                if ((char === "$" && i === 0) || isCharALetter(char)) {
                    continue;
                }
                numberPartStart = i;
            }
            // Number part
            if (!isCharADigit(char)) {
                if (char === "$" && i === numberPartStart) {
                    continue;
                }
                throw new Error(`Invalid cell description: ${xc}`);
            }
        }
        if (!numberPartStart || numberPartStart === xc.length) {
            throw new Error(`Invalid cell description: ${xc}`);
        }
        const letterPart = xc[0] === "$" ? xc.slice(1, numberPartStart) : xc.slice(0, numberPartStart);
        const numberPart = xc[numberPartStart] === "$" ? xc.slice(numberPartStart + 1) : xc.slice(numberPartStart);
        // limit to max 3 letters and 7 numbers to avoid
        // gigantic numbers that would be a performance killer
        // down the road
        if (letterPart.length < 1 ||
            letterPart.length > 3 ||
            numberPart.length < 1 ||
            numberPart.length > 7) {
            throw new Error(`Invalid cell description: ${xc}`);
        }
        const col = lettersToNumber(letterPart);
        const row = Number(numberPart) - 1;
        if (isNaN(row)) {
            throw new Error(`Invalid cell description: ${xc}`);
        }
        return { col, row };
    }
    /**
     * Convert from cartesian coordinate to the "XC" coordinate system.
     *
     * Examples:
     *   - 0,0 => A1
     *   - 1,2 => B3
     *   - 0,0, {colFixed: false, rowFixed: true} => A$1
     *   - 1,2, {colFixed: true, rowFixed: false} => $B3
     */
    function toXC(col, row, rangePart = { colFixed: false, rowFixed: false }) {
        return ((rangePart.colFixed ? "$" : "") +
            numberToLetters(col) +
            (rangePart.rowFixed ? "$" : "") +
            String(row + 1));
    }

    // -----------------------------------------------------------------------------
    // Date Type
    // -----------------------------------------------------------------------------
    /**
     * A DateTime object that can be used to manipulate spreadsheet dates.
     * Conceptually, a spreadsheet date is simply a number with a date format,
     * and it is timezone-agnostic.
     * This DateTime object consistently uses UTC time to represent a naive date and time.
     */
    class DateTime {
        jsDate;
        constructor(year, month, day, hours = 0, minutes = 0, seconds = 0) {
            this.jsDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0));
        }
        static fromTimestamp(timestamp) {
            const date = new Date(timestamp);
            return new DateTime(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
        }
        static now() {
            const now = new Date();
            return new DateTime(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds());
        }
        toString() {
            return this.jsDate.toString();
        }
        toLocaleDateString() {
            return this.jsDate.toLocaleDateString();
        }
        getTime() {
            return this.jsDate.getTime();
        }
        getFullYear() {
            return this.jsDate.getUTCFullYear();
        }
        getMonth() {
            return this.jsDate.getUTCMonth();
        }
        getDate() {
            return this.jsDate.getUTCDate();
        }
        getDay() {
            return this.jsDate.getUTCDay();
        }
        getHours() {
            return this.jsDate.getUTCHours();
        }
        getMinutes() {
            return this.jsDate.getUTCMinutes();
        }
        getSeconds() {
            return this.jsDate.getUTCSeconds();
        }
        setFullYear(year) {
            return this.jsDate.setUTCFullYear(year);
        }
        setMonth(month) {
            return this.jsDate.setUTCMonth(month);
        }
        setDate(date) {
            return this.jsDate.setUTCDate(date);
        }
        setHours(hours) {
            return this.jsDate.setUTCHours(hours);
        }
        setMinutes(minutes) {
            return this.jsDate.setUTCMinutes(minutes);
        }
        setSeconds(seconds) {
            return this.jsDate.setUTCSeconds(seconds);
        }
    }
    // -----------------------------------------------------------------------------
    // Parsing
    // -----------------------------------------------------------------------------
    const INITIAL_1900_DAY = new DateTime(1899, 11, 30);
    const MS_PER_DAY = 24 * 60 * 60 * 1000;
    const CURRENT_MILLENIAL = 2000; // note: don't forget to update this in 2999
    const CURRENT_YEAR = DateTime.now().getFullYear();
    const CURRENT_MONTH = DateTime.now().getMonth();
    const INITIAL_JS_DAY = DateTime.fromTimestamp(0);
    const DATE_JS_1900_OFFSET = INITIAL_JS_DAY.getTime() - INITIAL_1900_DAY.getTime();
    const mdyDateRegexp = /^\d{1,2}(\/|-|\s)\d{1,2}((\/|-|\s)\d{1,4})?$/;
    const ymdDateRegexp = /^\d{3,4}(\/|-|\s)\d{1,2}(\/|-|\s)\d{1,2}$/;
    const dateSeparatorsRegex = /\/|-|\s/;
    const dateRegexp = /^(\d{1,4})[\/-\s](\d{1,4})([\/-\s](\d{1,4}))?$/;
    const timeRegexp = /((\d+(:\d+)?(:\d+)?\s*(AM|PM))|(\d+:\d+(:\d+)?))$/;
    /** Convert a value number representing a date, or return undefined if it isn't possible */
    function valueToDateNumber(value, locale) {
        switch (typeof value) {
            case "number":
                return value;
            case "string":
                if (isDateTime(value, locale)) {
                    return parseDateTime(value, locale)?.value;
                }
                return !value || isNaN(Number(value)) ? undefined : Number(value);
            default:
                return undefined;
        }
    }
    function isDateTime(str, locale) {
        return parseDateTime(str, locale) !== null;
    }
    const CACHE = new Map();
    function parseDateTime(str, locale) {
        if (!CACHE.has(locale)) {
            CACHE.set(locale, new Map());
        }
        if (!CACHE.get(locale).has(str)) {
            CACHE.get(locale).set(str, _parseDateTime(str, locale));
        }
        return CACHE.get(locale).get(str);
    }
    function _parseDateTime(str, locale) {
        str = str.trim();
        let time = null;
        const timeMatch = str.match(timeRegexp);
        if (timeMatch) {
            time = parseTime(timeMatch[0]);
            if (time === null) {
                return null;
            }
            str = str.replace(timeMatch[0], "").trim();
        }
        let date = null;
        const dateParts = getDateParts(str, locale);
        if (dateParts) {
            const separator = dateParts.dateString.match(dateSeparatorsRegex)[0];
            date = parseDate(dateParts, separator);
            if (date === null) {
                return null;
            }
            str = str.replace(dateParts.dateString, "").trim();
        }
        if (str !== "" || !(date || time)) {
            return null;
        }
        if (date && date.jsDate && time && time.jsDate) {
            return {
                value: date.value + time.value,
                format: date.format + " " + (time.format === "hhhh:mm:ss" ? "hh:mm:ss" : time.format),
                jsDate: new DateTime(date.jsDate.getFullYear() + time.jsDate.getFullYear() - 1899, date.jsDate.getMonth() + time.jsDate.getMonth() - 11, date.jsDate.getDate() + time.jsDate.getDate() - 30, date.jsDate.getHours() + time.jsDate.getHours(), date.jsDate.getMinutes() + time.jsDate.getMinutes(), date.jsDate.getSeconds() + time.jsDate.getSeconds()),
            };
        }
        return date || time;
    }
    /**
     * Returns the parts (day/month/year) of a date string corresponding to the given locale.
     *
     * - A string "xxxx-xx-xx" will be parsed as "y-m-d" no matter the locale.
     * - A string "xx-xx-xxxx" will be parsed as "m-d-y" for mdy locale, and "d-m-y" for ymd and dmy locales.
     * - A string "xx-xx-xx" will be "y-m-d" for ymd locale, "d-m-y" for dmy locale, "m-d-y" for mdy locale.
     * - A string "xxxx-xx" will be parsed as "y-m" no matter the locale.
     * - A string "xx-xx" will be parsed as "m-d" for mdy and ymd locales, and "d-m" for dmy locale.
     */
    function getDateParts(dateString, locale) {
        const match = dateString.match(dateRegexp);
        if (!match) {
            return null;
        }
        const [, part1, part2, , part3] = match;
        if (part1.length > 2 && part3 && part3.length > 2) {
            return null;
        }
        if (part1.length > 2) {
            return { year: part1, month: part2, day: part3, dateString, type: "ymd" };
        }
        const localeDateType = getLocaleDateFormatType(locale);
        if (!part3) {
            if (part2.length > 2) {
                // e.g. 11/2023
                return { month: part1, year: part2, day: undefined, dateString, type: localeDateType };
            }
            if (localeDateType === "dmy") {
                return { day: part1, month: part2, year: part3, dateString, type: "dmy" };
            }
            return { month: part1, day: part2, year: part3, dateString, type: "mdy" };
        }
        if (part3.length > 2) {
            if (localeDateType === "mdy") {
                return { month: part1, day: part2, year: part3, dateString, type: "mdy" };
            }
            return { day: part1, month: part2, year: part3, dateString, type: "dmy" };
        }
        if (localeDateType === "mdy") {
            return { month: part1, day: part2, year: part3, dateString, type: "mdy" };
        }
        if (localeDateType === "ymd") {
            return { year: part1, month: part2, day: part3, dateString, type: "ymd" };
        }
        if (localeDateType === "dmy") {
            return { day: part1, month: part2, year: part3, dateString, type: "dmy" };
        }
        return null;
    }
    function getLocaleDateFormatType(locale) {
        switch (locale.dateFormat[0]) {
            case "d":
                return "dmy";
            case "m":
                return "mdy";
            case "y":
                return "ymd";
        }
        throw new Error("Invalid date format in locale");
    }
    function parseDate(parts, separator) {
        let { year: yearStr, month: monthStr, day: dayStr } = parts;
        const month = inferMonth(monthStr);
        const day = inferDay(dayStr);
        const year = inferYear(yearStr);
        if (year === null || month === null || day === null) {
            return null;
        }
        // month + 1: months are 0-indexed in JS
        const leadingZero = (monthStr?.length === 2 && month + 1 < 10) || (dayStr?.length === 2 && day < 10);
        const fullYear = yearStr?.length !== 2;
        const jsDate = new DateTime(year, month, day);
        if (jsDate.getMonth() !== month || jsDate.getDate() !== day) {
            // invalid date
            return null;
        }
        const delta = jsDate.getTime() - INITIAL_1900_DAY.getTime();
        const format = getFormatFromDateParts(parts, separator, leadingZero, fullYear);
        return {
            value: Math.round(delta / MS_PER_DAY),
            format: format,
            jsDate,
        };
    }
    function getFormatFromDateParts(parts, sep, leadingZero, fullYear) {
        const yearFmt = parts.year ? (fullYear ? "yyyy" : "yy") : undefined;
        const monthFmt = parts.month ? (leadingZero ? "mm" : "m") : undefined;
        const dayFmt = parts.day ? (leadingZero ? "dd" : "d") : undefined;
        switch (parts.type) {
            case "mdy":
                return [monthFmt, dayFmt, yearFmt].filter(isDefined$1).join(sep);
            case "ymd":
                return [yearFmt, monthFmt, dayFmt].filter(isDefined$1).join(sep);
            case "dmy":
                return [dayFmt, monthFmt, yearFmt].filter(isDefined$1).join(sep);
        }
    }
    function inferYear(yearStr) {
        if (!yearStr) {
            return CURRENT_YEAR;
        }
        const nbr = Number(yearStr);
        switch (yearStr.length) {
            case 1:
                return CURRENT_MILLENIAL + nbr;
            case 2:
                const offset = CURRENT_MILLENIAL + nbr > CURRENT_YEAR + 10 ? -100 : 0;
                const base = CURRENT_MILLENIAL + offset;
                return base + nbr;
            case 3:
            case 4:
                return nbr;
        }
        return null;
    }
    function inferMonth(monthStr) {
        if (!monthStr) {
            return CURRENT_MONTH;
        }
        const nbr = Number(monthStr);
        if (nbr >= 1 && nbr <= 12) {
            return nbr - 1;
        }
        return null;
    }
    function inferDay(dayStr) {
        if (!dayStr) {
            return 1;
        }
        const nbr = Number(dayStr);
        if (nbr >= 0 && nbr <= 31) {
            return nbr;
        }
        return null;
    }
    function parseTime(str) {
        str = str.trim();
        if (timeRegexp.test(str)) {
            const isAM = /AM/i.test(str);
            const isPM = /PM/i.test(str);
            const strTime = isAM || isPM ? str.substring(0, str.length - 2).trim() : str;
            const parts = strTime.split(/:/);
            const isMinutes = parts.length >= 2;
            const isSeconds = parts.length === 3;
            let hours = Number(parts[0]);
            let minutes = isMinutes ? Number(parts[1]) : 0;
            let seconds = isSeconds ? Number(parts[2]) : 0;
            let format = isSeconds ? "hh:mm:ss" : "hh:mm";
            if (isAM || isPM) {
                format += " a";
            }
            else if (!isMinutes) {
                return null;
            }
            if (hours >= 12 && isAM) {
                hours -= 12;
            }
            else if (hours < 12 && isPM) {
                hours += 12;
            }
            minutes += Math.floor(seconds / 60);
            seconds %= 60;
            hours += Math.floor(minutes / 60);
            minutes %= 60;
            if (hours >= 24) {
                format = "hhhh:mm:ss";
            }
            const jsDate = new DateTime(1899, 11, 30, hours, minutes, seconds);
            return {
                value: hours / 24 + minutes / 1440 + seconds / 86400,
                format: format,
                jsDate: jsDate,
            };
        }
        return null;
    }
    // -----------------------------------------------------------------------------
    // Conversion
    // -----------------------------------------------------------------------------
    function numberToJsDate(value) {
        const truncValue = Math.trunc(value);
        let date = DateTime.fromTimestamp(truncValue * MS_PER_DAY - DATE_JS_1900_OFFSET);
        let time = value - truncValue;
        time = time < 0 ? 1 + time : time;
        const hours = Math.round(time * 24);
        const minutes = Math.round((time - hours / 24) * 24 * 60);
        const seconds = Math.round((time - hours / 24 - minutes / 24 / 60) * 24 * 60 * 60);
        date.setHours(hours);
        date.setMinutes(minutes);
        date.setSeconds(seconds);
        return date;
    }
    function jsDateToRoundNumber(date) {
        return Math.round(jsDateToNumber(date));
    }
    function jsDateToNumber(date) {
        const delta = date.getTime() - INITIAL_1900_DAY.getTime();
        return delta / MS_PER_DAY;
    }
    /** Return the number of days in the current month of the given date */
    function getDaysInMonth(date) {
        return new DateTime(date.getFullYear(), date.getMonth() + 1, 0).getDate();
    }
    function isLastDayOfMonth(date) {
        return getDaysInMonth(date) === date.getDate();
    }
    /**
     * Add a certain number of months to a date. This will adapt the month number, and possibly adapt
     * the day of the month to keep it in the month.
     *
     * For example "31/12/2020" minus one month will be "30/11/2020", and not "31/11/2020"
     *
     * @param keepEndOfMonth if true, if the given date was the last day of a month, the returned date will
     *          also always be the last day of a month.
     */
    function addMonthsToDate(date, months, keepEndOfMonth) {
        const yStart = date.getFullYear();
        const mStart = date.getMonth();
        const dStart = date.getDate();
        const jsDate = new DateTime(yStart, mStart + months, 1);
        if (keepEndOfMonth && dStart === getDaysInMonth(date)) {
            jsDate.setDate(getDaysInMonth(jsDate));
        }
        else if (dStart > getDaysInMonth(jsDate)) {
            // 31/03 minus one month should be 28/02, not 31/02
            jsDate.setDate(getDaysInMonth(jsDate));
        }
        else {
            jsDate.setDate(dStart);
        }
        return jsDate;
    }
    function isLeapYear(year) {
        const _year = Math.trunc(year);
        return (_year % 4 === 0 && _year % 100 != 0) || _year % 400 === 0;
    }
    function getYearFrac(startDate, endDate, _dayCountConvention) {
        if (startDate === endDate) {
            return 0;
        }
        if (startDate > endDate) {
            const stack = endDate;
            endDate = startDate;
            startDate = stack;
        }
        const jsStartDate = numberToJsDate(startDate);
        const jsEndDate = numberToJsDate(endDate);
        let dayStart = jsStartDate.getDate();
        let dayEnd = jsEndDate.getDate();
        const monthStart = jsStartDate.getMonth(); // january is 0
        const monthEnd = jsEndDate.getMonth(); // january is 0
        const yearStart = jsStartDate.getFullYear();
        const yearEnd = jsEndDate.getFullYear();
        let yearsStart = 0;
        let yearsEnd = 0;
        switch (_dayCountConvention) {
            // 30/360 US convention --------------------------------------------------
            case 0:
                if (dayStart === 31)
                    dayStart = 30;
                if (dayStart === 30 && dayEnd === 31)
                    dayEnd = 30;
                // If jsStartDate is the last day of February
                if (monthStart === 1 && dayStart === (isLeapYear(yearStart) ? 29 : 28)) {
                    dayStart = 30;
                    // If jsEndDate is the last day of February
                    if (monthEnd === 1 && dayEnd === (isLeapYear(yearEnd) ? 29 : 28)) {
                        dayEnd = 30;
                    }
                }
                yearsStart = yearStart + (monthStart * 30 + dayStart) / 360;
                yearsEnd = yearEnd + (monthEnd * 30 + dayEnd) / 360;
                break;
            // actual/actual convention ----------------------------------------------
            case 1:
                let daysInYear = 365;
                const isSameYear = yearStart === yearEnd;
                const isOneDeltaYear = yearStart + 1 === yearEnd;
                const isMonthEndBigger = monthStart < monthEnd;
                const isSameMonth = monthStart === monthEnd;
                const isDayEndBigger = dayStart < dayEnd;
                // |-----|  <-- one Year
                // 'A' is start date
                // 'B' is end date
                if ((!isSameYear && !isOneDeltaYear) ||
                    (!isSameYear && isMonthEndBigger) ||
                    (!isSameYear && isSameMonth && isDayEndBigger)) {
                    // |---A-|-----|-B---|  <-- !isSameYear && !isOneDeltaYear
                    // |---A-|----B|-----|  <-- !isSameYear && isMonthEndBigger
                    // |---A-|---B-|-----|  <-- !isSameYear && isSameMonth && isDayEndBigger
                    let countYears = 0;
                    let countDaysInYears = 0;
                    for (let y = yearStart; y <= yearEnd; y++) {
                        countYears++;
                        countDaysInYears += isLeapYear(y) ? 366 : 365;
                    }
                    daysInYear = countDaysInYears / countYears;
                }
                else if (!isSameYear) {
                    // |-AF--|B----|-----|
                    if (isLeapYear(yearStart) && monthStart < 2) {
                        daysInYear = 366;
                    }
                    // |--A--|FB---|-----|
                    if (isLeapYear(yearEnd) && (monthEnd > 1 || (monthEnd === 1 && dayEnd === 29))) {
                        daysInYear = 366;
                    }
                }
                else {
                    // remaining cases:
                    //
                    // |-F-AB|-----|-----|
                    // |AB-F-|-----|-----|
                    // |A-F-B|-----|-----|
                    // if February 29 occurs between date1 (exclusive) and date2 (inclusive)
                    // daysInYear --> 366
                    if (isLeapYear(yearStart)) {
                        daysInYear = 366;
                    }
                }
                yearsStart = startDate / daysInYear;
                yearsEnd = endDate / daysInYear;
                break;
            // actual/360 convention -------------------------------------------------
            case 2:
                yearsStart = startDate / 360;
                yearsEnd = endDate / 360;
                break;
            // actual/365 convention -------------------------------------------------
            case 3:
                yearsStart = startDate / 365;
                yearsEnd = endDate / 365;
                break;
            // 30/360 European convention --------------------------------------------
            case 4:
                if (dayStart === 31)
                    dayStart = 30;
                if (dayEnd === 31)
                    dayEnd = 30;
                yearsStart = yearStart + (monthStart * 30 + dayStart) / 360;
                yearsEnd = yearEnd + (monthEnd * 30 + dayEnd) / 360;
                break;
        }
        return yearsEnd - yearsStart;
    }
    /**
     * Get the number of whole months between two dates.
     * e.g.
     *  2002/01/01 -> 2002/02/01 = 1 month,
     *  2002/01/01 -> 2003/02/01 = 13 months
     * @param startDate
     * @param endDate
     * @returns
     */
    function getTimeDifferenceInWholeMonths(startDate, endDate) {
        const months = (endDate.getFullYear() - startDate.getFullYear()) * 12 +
            endDate.getMonth() -
            startDate.getMonth();
        return startDate.getDate() > endDate.getDate() ? months - 1 : months;
    }
    function getTimeDifferenceInWholeDays(startDate, endDate) {
        const startUtc = startDate.getTime();
        const endUtc = endDate.getTime();
        return Math.floor((endUtc - startUtc) / MS_PER_DAY);
    }
    function getTimeDifferenceInWholeYears(startDate, endDate) {
        const years = endDate.getFullYear() - startDate.getFullYear();
        const monthStart = startDate.getMonth();
        const monthEnd = endDate.getMonth();
        const dateStart = startDate.getDate();
        const dateEnd = endDate.getDate();
        const isEndMonthDateBigger = monthEnd > monthStart || (monthEnd === monthStart && dateEnd >= dateStart);
        return isEndMonthDateBigger ? years : years - 1;
    }
    function areTwoDatesWithinOneYear(startDate, endDate) {
        return getYearFrac(startDate, endDate, 1) < 1;
    }
    function areDatesSameDay(startDate, endDate) {
        return Math.trunc(startDate) === Math.trunc(endDate);
    }
    function isDateBetween(date, startDate, endDate) {
        if (startDate > endDate) {
            return isDateBetween(date, endDate, startDate);
        }
        date = Math.trunc(date);
        startDate = Math.trunc(startDate);
        endDate = Math.trunc(endDate);
        return date >= startDate && date <= endDate;
    }
    /** Check if the first date is strictly before the second date */
    function isDateStrictlyBefore(date, dateBefore) {
        return Math.trunc(date) < Math.trunc(dateBefore);
    }
    /** Check if the first date is before or equal to the second date */
    function isDateBefore(date, dateBefore) {
        return Math.trunc(date) <= Math.trunc(dateBefore);
    }
    /** Check if the first date is strictly after the second date */
    function isDateStrictlyAfter(date, dateAfter) {
        return Math.trunc(date) > Math.trunc(dateAfter);
    }
    /** Check if the first date is after or equal to the second date */
    function isDateAfter(date, dateAfter) {
        return Math.trunc(date) >= Math.trunc(dateAfter);
    }

    /**
     * This function returns a regexp that is supposed to be as close as possible as the numberRegexp,
     * but its purpose is to be used by the tokenizer.
     *
     * - it tolerates extra characters at the end. This is useful because the tokenizer
     *   only needs to find the number at the start of a string
     * - it does not support % symbol, in formulas % is an operator
     */
    const getFormulaNumberRegex = memoize(function getFormulaNumberRegex(decimalSeparator) {
        decimalSeparator = escapeRegExp(decimalSeparator);
        return new RegExp(`(^-?\\d+(${decimalSeparator}?\\d*(e\\d+)?)?|^-?${decimalSeparator}\\d+)(?!\\w|!)`);
    });
    const getNumberRegex = memoize(function getNumberRegex(locale) {
        const decimalSeparator = escapeRegExp(locale.decimalSeparator);
        const thousandsSeparator = escapeRegExp(locale.thousandsSeparator);
        const pIntegerAndDecimals = `(\\d+(${thousandsSeparator}\\d{3,})*(${decimalSeparator}\\d*)?)`; // pattern that match integer number with or without decimal digits
        const pOnlyDecimals = `(${decimalSeparator}\\d+)`; // pattern that match only expression with decimal digits
        const pScientificFormat = "(e(\\+|-)?\\d+)?"; // pattern that match scientific format between zero and one time (should be placed before pPercentFormat)
        const pPercentFormat = "(\\s*%)?"; // pattern that match percent symbol between zero and one time
        const pNumber = "(\\s*" + pIntegerAndDecimals + "|" + pOnlyDecimals + ")" + pScientificFormat + pPercentFormat;
        const pMinus = "(\\s*-)?"; // pattern that match negative symbol between zero and one time
        const pCurrencyFormat = "(\\s*[\\$€])?";
        const p1 = pMinus + pCurrencyFormat + pNumber;
        const p2 = pMinus + pNumber + pCurrencyFormat;
        const p3 = pCurrencyFormat + pMinus + pNumber;
        const pNumberExp = "^((" + [p1, p2, p3].join(")|(") + "))$";
        const numberRegexp = new RegExp(pNumberExp, "i");
        return numberRegexp;
    });
    /**
     * Return true if the argument is a "number string".
     *
     * Note that "" (empty string) does not count as a number string
     */
    function isNumber(value, locale) {
        if (!value)
            return false;
        // TO DO: add regexp for DATE string format (ex match: "28 02 2020")
        return getNumberRegex(locale).test(value.trim());
    }
    const getInvaluableSymbolsRegexp = memoize(function getInvaluableSymbolsRegexp(locale) {
        return new RegExp(`[\$€${escapeRegExp(locale.thousandsSeparator)}]`, "g");
    });
    /**
     * Convert a string into a number. It assumes that the string actually represents
     * a number (as determined by the isNumber function)
     *
     * Note that it accepts "" (empty string), even though it does not count as a
     * number from the point of view of the isNumber function.
     */
    function parseNumber(str, locale) {
        if (locale.decimalSeparator !== ".") {
            str = str.replace(locale.decimalSeparator, ".");
        }
        // remove invaluable characters
        str = str.replace(getInvaluableSymbolsRegexp(locale), "");
        let n = Number(str);
        if (isNaN(n) && str.includes("%")) {
            n = Number(str.split("%")[0]);
            if (!isNaN(n)) {
                return n / 100;
            }
        }
        return n;
    }
    function percentile(values, percent, isInclusive) {
        const sortedValues = [...values].sort((a, b) => a - b);
        let percentIndex = (sortedValues.length + (isInclusive ? -1 : 1)) * percent;
        if (!isInclusive) {
            percentIndex--;
        }
        if (Number.isInteger(percentIndex)) {
            return sortedValues[percentIndex];
        }
        const indexSup = Math.ceil(percentIndex);
        const indexLow = Math.floor(percentIndex);
        return (sortedValues[indexSup] * (percentIndex - indexLow) +
            sortedValues[indexLow] * (indexSup - percentIndex));
    }

    var CellValueType;
    (function (CellValueType) {
        CellValueType["boolean"] = "boolean";
        CellValueType["number"] = "number";
        CellValueType["text"] = "text";
        CellValueType["empty"] = "empty";
        CellValueType["error"] = "error";
    })(CellValueType || (CellValueType = {}));

    var ClipboardMIMEType;
    (function (ClipboardMIMEType) {
        ClipboardMIMEType["PlainText"] = "text/plain";
        ClipboardMIMEType["Html"] = "text/html";
    })(ClipboardMIMEType || (ClipboardMIMEType = {}));

    function isSheetDependent(cmd) {
        return "sheetId" in cmd;
    }
    function isHeadersDependant(cmd) {
        return "dimension" in cmd && "sheetId" in cmd && "elements" in cmd;
    }
    function isTargetDependent(cmd) {
        return "target" in cmd && "sheetId" in cmd;
    }
    function isRangeDependant(cmd) {
        return "ranges" in cmd;
    }
    function isZoneDependent(cmd) {
        return "zone" in cmd;
    }
    function isPositionDependent(cmd) {
        return "col" in cmd && "row" in cmd && "sheetId" in cmd;
    }
    const invalidateEvaluationCommands = new Set([
        "RENAME_SHEET",
        "DELETE_SHEET",
        "CREATE_SHEET",
        "ADD_COLUMNS_ROWS",
        "REMOVE_COLUMNS_ROWS",
        "UNDO",
        "REDO",
        "ADD_MERGE",
        "UPDATE_LOCALE",
    ]);
    const invalidateDependenciesCommands = new Set([
        ...invalidateEvaluationCommands,
        "MOVE_RANGES",
    ]);
    const invalidateCFEvaluationCommands = new Set([
        ...invalidateEvaluationCommands,
        "DUPLICATE_SHEET",
        "EVALUATE_CELLS",
        "ADD_CONDITIONAL_FORMAT",
        "REMOVE_CONDITIONAL_FORMAT",
        "CHANGE_CONDITIONAL_FORMAT_PRIORITY",
    ]);
    const readonlyAllowedCommands = new Set([
        "START",
        "ACTIVATE_SHEET",
        "COPY",
        "RESIZE_SHEETVIEW",
        "SET_VIEWPORT_OFFSET",
        "SELECT_SEARCH_NEXT_MATCH",
        "SELECT_SEARCH_PREVIOUS_MATCH",
        "UPDATE_SEARCH",
        "CLEAR_SEARCH",
        "EVALUATE_CELLS",
        "SET_CURRENT_CONTENT",
        "SET_FORMULA_VISIBILITY",
        "OPEN_CELL_POPOVER",
        "CLOSE_CELL_POPOVER",
        "UPDATE_FILTER",
    ]);
    const coreTypes = new Set([
        /** CELLS */
        "UPDATE_CELL",
        "UPDATE_CELL_POSITION",
        "CLEAR_CELL",
        "DELETE_CONTENT",
        /** GRID SHAPE */
        "ADD_COLUMNS_ROWS",
        "REMOVE_COLUMNS_ROWS",
        "RESIZE_COLUMNS_ROWS",
        "HIDE_COLUMNS_ROWS",
        "UNHIDE_COLUMNS_ROWS",
        "SET_GRID_LINES_VISIBILITY",
        "UNFREEZE_COLUMNS",
        "UNFREEZE_ROWS",
        "FREEZE_COLUMNS",
        "FREEZE_ROWS",
        "UNFREEZE_COLUMNS_ROWS",
        /** MERGE */
        "ADD_MERGE",
        "REMOVE_MERGE",
        /** SHEETS MANIPULATION */
        "CREATE_SHEET",
        "DELETE_SHEET",
        "DUPLICATE_SHEET",
        "MOVE_SHEET",
        "RENAME_SHEET",
        "HIDE_SHEET",
        "SHOW_SHEET",
        /** RANGES MANIPULATION */
        "MOVE_RANGES",
        /** CONDITIONAL FORMAT */
        "ADD_CONDITIONAL_FORMAT",
        "REMOVE_CONDITIONAL_FORMAT",
        "CHANGE_CONDITIONAL_FORMAT_PRIORITY",
        /** FIGURES */
        "CREATE_FIGURE",
        "DELETE_FIGURE",
        "UPDATE_FIGURE",
        /** FORMATTING */
        "SET_FORMATTING",
        "CLEAR_FORMATTING",
        "SET_BORDER",
        "SET_ZONE_BORDERS",
        /** CHART */
        "CREATE_CHART",
        "UPDATE_CHART",
        /** FILTERS */
        "CREATE_FILTER_TABLE",
        "REMOVE_FILTER_TABLE",
        /** IMAGE */
        "CREATE_IMAGE",
        /** HEADER GROUP */
        "GROUP_HEADERS",
        "UNGROUP_HEADERS",
        "UNFOLD_HEADER_GROUP",
        "FOLD_HEADER_GROUP",
        "FOLD_ALL_HEADER_GROUPS",
        "UNFOLD_ALL_HEADER_GROUPS",
        "UNFOLD_HEADER_GROUPS_IN_ZONE",
        "FOLD_HEADER_GROUPS_IN_ZONE",
        /** DATA VALIDATION */
        "ADD_DATA_VALIDATION_RULE",
        "REMOVE_DATA_VALIDATION_RULE",
        /** MISC */
        "UPDATE_LOCALE",
    ]);
    function isCoreCommand(cmd) {
        return coreTypes.has(cmd.type);
    }
    function canExecuteInReadonly(cmd) {
        return readonlyAllowedCommands.has(cmd.type);
    }
    /**
     * Holds the result of a command dispatch.
     * The command may have been successfully dispatched or cancelled
     * for one or more reasons.
     */
    class DispatchResult {
        reasons;
        constructor(results = []) {
            if (!Array.isArray(results)) {
                results = [results];
            }
            results = [...new Set(results)];
            this.reasons = results.filter((result) => result !== "Success" /* CommandResult.Success */);
        }
        /**
         * Static helper which returns a successful DispatchResult
         */
        static get Success() {
            return SUCCESS;
        }
        get isSuccessful() {
            return this.reasons.length === 0;
        }
        /**
         * Check if the dispatch has been cancelled because of
         * the given reason.
         */
        isCancelledBecause(reason) {
            return this.reasons.includes(reason);
        }
    }
    const SUCCESS = new DispatchResult();
    exports.CommandResult = void 0;
    (function (CommandResult) {
        CommandResult["Success"] = "Success";
        CommandResult["CancelledForUnknownReason"] = "CancelledForUnknownReason";
        CommandResult["WillRemoveExistingMerge"] = "WillRemoveExistingMerge";
        CommandResult["MergeIsDestructive"] = "MergeIsDestructive";
        CommandResult["CellIsMerged"] = "CellIsMerged";
        CommandResult["InvalidTarget"] = "InvalidTarget";
        CommandResult["EmptyUndoStack"] = "EmptyUndoStack";
        CommandResult["EmptyRedoStack"] = "EmptyRedoStack";
        CommandResult["NotEnoughElements"] = "NotEnoughElements";
        CommandResult["NotEnoughSheets"] = "NotEnoughSheets";
        CommandResult["MissingSheetName"] = "MissingSheetName";
        CommandResult["UnchangedSheetName"] = "UnchangedSheetName";
        CommandResult["DuplicatedSheetName"] = "DuplicatedSheetName";
        CommandResult["DuplicatedSheetId"] = "DuplicatedSheetId";
        CommandResult["ForbiddenCharactersInSheetName"] = "ForbiddenCharactersInSheetName";
        CommandResult["WrongSheetMove"] = "WrongSheetMove";
        CommandResult["WrongSheetPosition"] = "WrongSheetPosition";
        CommandResult["InvalidAnchorZone"] = "InvalidAnchorZone";
        CommandResult["SelectionOutOfBound"] = "SelectionOutOfBound";
        CommandResult["TargetOutOfSheet"] = "TargetOutOfSheet";
        CommandResult["WrongCutSelection"] = "WrongCutSelection";
        CommandResult["WrongPasteSelection"] = "WrongPasteSelection";
        CommandResult["WrongPasteOption"] = "WrongPasteOption";
        CommandResult["WrongFigurePasteOption"] = "WrongFigurePasteOption";
        CommandResult["EmptyClipboard"] = "EmptyClipboard";
        CommandResult["EmptyRange"] = "EmptyRange";
        CommandResult["InvalidRange"] = "InvalidRange";
        CommandResult["InvalidZones"] = "InvalidZones";
        CommandResult["InvalidSheetId"] = "InvalidSheetId";
        CommandResult["InvalidFigureId"] = "InvalidFigureId";
        CommandResult["InputAlreadyFocused"] = "InputAlreadyFocused";
        CommandResult["MaximumRangesReached"] = "MaximumRangesReached";
        CommandResult["MinimumRangesReached"] = "MinimumRangesReached";
        CommandResult["InvalidChartDefinition"] = "InvalidChartDefinition";
        CommandResult["InvalidDataSet"] = "InvalidDataSet";
        CommandResult["InvalidLabelRange"] = "InvalidLabelRange";
        CommandResult["InvalidScorecardKeyValue"] = "InvalidScorecardKeyValue";
        CommandResult["InvalidScorecardBaseline"] = "InvalidScorecardBaseline";
        CommandResult["InvalidGaugeDataRange"] = "InvalidGaugeDataRange";
        CommandResult["EmptyGaugeRangeMin"] = "EmptyGaugeRangeMin";
        CommandResult["GaugeRangeMinNaN"] = "GaugeRangeMinNaN";
        CommandResult["EmptyGaugeRangeMax"] = "EmptyGaugeRangeMax";
        CommandResult["GaugeRangeMaxNaN"] = "GaugeRangeMaxNaN";
        CommandResult["GaugeRangeMinBiggerThanRangeMax"] = "GaugeRangeMinBiggerThanRangeMax";
        CommandResult["GaugeLowerInflectionPointNaN"] = "GaugeLowerInflectionPointNaN";
        CommandResult["GaugeUpperInflectionPointNaN"] = "GaugeUpperInflectionPointNaN";
        CommandResult["GaugeLowerBiggerThanUpper"] = "GaugeLowerBiggerThanUpper";
        CommandResult["InvalidAutofillSelection"] = "InvalidAutofillSelection";
        CommandResult["WrongComposerSelection"] = "WrongComposerSelection";
        CommandResult["MinBiggerThanMax"] = "MinBiggerThanMax";
        CommandResult["LowerBiggerThanUpper"] = "LowerBiggerThanUpper";
        CommandResult["MidBiggerThanMax"] = "MidBiggerThanMax";
        CommandResult["MinBiggerThanMid"] = "MinBiggerThanMid";
        CommandResult["FirstArgMissing"] = "FirstArgMissing";
        CommandResult["SecondArgMissing"] = "SecondArgMissing";
        CommandResult["MinNaN"] = "MinNaN";
        CommandResult["MidNaN"] = "MidNaN";
        CommandResult["MaxNaN"] = "MaxNaN";
        CommandResult["ValueUpperInflectionNaN"] = "ValueUpperInflectionNaN";
        CommandResult["ValueLowerInflectionNaN"] = "ValueLowerInflectionNaN";
        CommandResult["MinInvalidFormula"] = "MinInvalidFormula";
        CommandResult["MidInvalidFormula"] = "MidInvalidFormula";
        CommandResult["MaxInvalidFormula"] = "MaxInvalidFormula";
        CommandResult["ValueUpperInvalidFormula"] = "ValueUpperInvalidFormula";
        CommandResult["ValueLowerInvalidFormula"] = "ValueLowerInvalidFormula";
        CommandResult["InvalidSortZone"] = "InvalidSortZone";
        CommandResult["WaitingSessionConfirmation"] = "WaitingSessionConfirmation";
        CommandResult["MergeOverlap"] = "MergeOverlap";
        CommandResult["TooManyHiddenElements"] = "TooManyHiddenElements";
        CommandResult["Readonly"] = "Readonly";
        CommandResult["InvalidViewportSize"] = "InvalidViewportSize";
        CommandResult["InvalidScrollingDirection"] = "InvalidScrollingDirection";
        CommandResult["FigureDoesNotExist"] = "FigureDoesNotExist";
        CommandResult["InvalidConditionalFormatId"] = "InvalidConditionalFormatId";
        CommandResult["InvalidCellPopover"] = "InvalidCellPopover";
        CommandResult["EmptyTarget"] = "EmptyTarget";
        CommandResult["InvalidFreezeQuantity"] = "InvalidFreezeQuantity";
        CommandResult["FrozenPaneOverlap"] = "FrozenPaneOverlap";
        CommandResult["ValuesNotChanged"] = "ValuesNotChanged";
        CommandResult["InvalidFilterZone"] = "InvalidFilterZone";
        CommandResult["FilterOverlap"] = "FilterOverlap";
        CommandResult["FilterNotFound"] = "FilterNotFound";
        CommandResult["MergeInFilter"] = "MergeInFilter";
        CommandResult["NonContinuousTargets"] = "NonContinuousTargets";
        CommandResult["DuplicatedFigureId"] = "DuplicatedFigureId";
        CommandResult["InvalidSelectionStep"] = "InvalidSelectionStep";
        CommandResult["DuplicatedChartId"] = "DuplicatedChartId";
        CommandResult["ChartDoesNotExist"] = "ChartDoesNotExist";
        CommandResult["InvalidHeaderIndex"] = "InvalidHeaderIndex";
        CommandResult["InvalidQuantity"] = "InvalidQuantity";
        CommandResult["MoreThanOneColumnSelected"] = "MoreThanOneColumnSelected";
        CommandResult["EmptySplitSeparator"] = "EmptySplitSeparator";
        CommandResult["SplitWillOverwriteContent"] = "SplitWillOverwriteContent";
        CommandResult["NoSplitSeparatorInSelection"] = "NoSplitSeparatorInSelection";
        CommandResult["NoActiveSheet"] = "NoActiveSheet";
        CommandResult["InvalidLocale"] = "InvalidLocale";
        CommandResult["AlreadyInPaintingFormatMode"] = "AlreadyInPaintingFormatMode";
        CommandResult["MoreThanOneRangeSelected"] = "MoreThanOneRangeSelected";
        CommandResult["NoColumnsProvided"] = "NoColumnsProvided";
        CommandResult["ColumnsNotIncludedInZone"] = "ColumnsNotIncludedInZone";
        CommandResult["DuplicatesColumnsSelected"] = "DuplicatesColumnsSelected";
        CommandResult["InvalidHeaderGroupStartEnd"] = "InvalidHeaderGroupStartEnd";
        CommandResult["HeaderGroupAlreadyExists"] = "HeaderGroupAlreadyExists";
        CommandResult["UnknownHeaderGroup"] = "UnknownHeaderGroup";
        CommandResult["UnknownDataValidationRule"] = "UnknownDataValidationRule";
        CommandResult["UnknownDataValidationCriterionType"] = "UnknownDataValidationCriterionType";
        CommandResult["InvalidDataValidationCriterionValue"] = "InvalidDataValidationCriterionValue";
        CommandResult["InvalidNumberOfCriterionValues"] = "InvalidNumberOfCriterionValues";
        CommandResult["BlockingValidationRule"] = "BlockingValidationRule";
        CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
        CommandResult["NoChanges"] = "NoChanges";
        CommandResult["InvalidInputId"] = "InvalidInputId";
    })(exports.CommandResult || (exports.CommandResult = {}));

    const DEFAULT_LOCALES = [
        {
            name: "English (US)",
            code: "en_US",
            thousandsSeparator: ",",
            decimalSeparator: ".",
            dateFormat: "m/d/yyyy",
            timeFormat: "hh:mm:ss a",
            formulaArgSeparator: ",",
        },
        {
            name: "French",
            code: "fr_FR",
            thousandsSeparator: " ",
            decimalSeparator: ",",
            dateFormat: "dd/mm/yyyy",
            timeFormat: "hh:mm:ss",
            formulaArgSeparator: ";",
        },
    ];
    const DEFAULT_LOCALE = DEFAULT_LOCALES[0];

    const borderStyles = ["thin", "medium", "thick", "dashed", "dotted"];
    function isMatrix(x) {
        return Array.isArray(x) && Array.isArray(x[0]);
    }
    var DIRECTION;
    (function (DIRECTION) {
        DIRECTION["UP"] = "up";
        DIRECTION["DOWN"] = "down";
        DIRECTION["LEFT"] = "left";
        DIRECTION["RIGHT"] = "right";
    })(DIRECTION || (DIRECTION = {}));

    var LAYERS;
    (function (LAYERS) {
        LAYERS[LAYERS["Background"] = 0] = "Background";
        LAYERS[LAYERS["Highlights"] = 1] = "Highlights";
        LAYERS[LAYERS["Clipboard"] = 2] = "Clipboard";
        LAYERS[LAYERS["Search"] = 3] = "Search";
        LAYERS[LAYERS["Chart"] = 4] = "Chart";
        LAYERS[LAYERS["Autofill"] = 5] = "Autofill";
        LAYERS[LAYERS["Selection"] = 6] = "Selection";
        LAYERS[LAYERS["Headers"] = 7] = "Headers";
    })(LAYERS || (LAYERS = {}));

    // HELPERS
    const SORT_TYPES_ORDER = ["number", "string", "boolean", "undefined"];
    function assert(condition, message) {
        if (!condition()) {
            throw new EvaluationError(CellErrorType.GenericError, message);
        }
    }
    // -----------------------------------------------------------------------------
    // FORMAT FUNCTIONS
    // -----------------------------------------------------------------------------
    const expectNumberValueError = (value) => _t("The function [[FUNCTION_NAME]] expects a number value, but '%s' is a string, and cannot be coerced to a number.", value);
    const expectNumberRangeError = (lowerBound, upperBound, value) => _t("The function [[FUNCTION_NAME]] expects a number value between %s and %s inclusive, but receives %s.", lowerBound.toString(), upperBound.toString(), value.toString());
    const expectStringSetError = (stringSet, value) => {
        const stringSetString = stringSet.map((str) => `'${str}'`).join(", ");
        return _t("The function [[FUNCTION_NAME]] has an argument with value '%s'. It should be one of: %s.", value, stringSetString);
    };
    function toNumber(value, locale) {
        switch (typeof value) {
            case "number":
                return value;
            case "boolean":
                return value ? 1 : 0;
            case "string":
                if (isNumber(value, locale) || value === "") {
                    return parseNumber(value, locale);
                }
                const internalDate = parseDateTime(value, locale);
                if (internalDate) {
                    return internalDate.value;
                }
                throw new Error(expectNumberValueError(value));
            default:
                return 0;
        }
    }
    function tryToNumber(value, locale) {
        try {
            return toNumber(value, locale);
        }
        catch (e) {
            return undefined;
        }
    }
    function tryCastAsNumberMatrix(data, argName) {
        data.forEach((row) => {
            row.forEach((cell) => {
                if (typeof cell !== "number") {
                    throw new Error(_t("Function [[FUNCTION_NAME]] expects number values for %s, but got a %s.", typeof cell, argName));
                }
            });
        });
        return data;
    }
    function strictToNumber(value, locale) {
        if (value === "") {
            throw new Error(expectNumberValueError(value));
        }
        return toNumber(value, locale);
    }
    function toInteger(value, locale) {
        return Math.trunc(toNumber(value, locale));
    }
    function strictToInteger(value, locale) {
        return Math.trunc(strictToNumber(value, locale));
    }
    function assertNumberGreaterThanOrEqualToOne(value) {
        assert(() => value >= 1, _t("The function [[FUNCTION_NAME]] expects a number value to be greater than or equal to 1, but receives %s.", value.toString()));
    }
    function toString(value) {
        switch (typeof value) {
            case "string":
                return value;
            case "number":
                return value.toString();
            case "boolean":
                return value ? "TRUE" : "FALSE";
            default:
                return "";
        }
    }
    /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
    const normalizeString = memoize(function normalizeString(str) {
        return str
            .toLowerCase()
            .normalize("NFD")
            .replace(/[\u0300-\u036f]/g, "");
    });
    const expectBooleanValueError = (value) => _t("The function [[FUNCTION_NAME]] expects a boolean value, but '%s' is a text, and cannot be coerced to a number.", value);
    function toBoolean(value) {
        switch (typeof value) {
            case "boolean":
                return value;
            case "string":
                if (value) {
                    let uppercaseVal = value.toUpperCase();
                    if (uppercaseVal === "TRUE") {
                        return true;
                    }
                    if (uppercaseVal === "FALSE") {
                        return false;
                    }
                    throw new Error(expectBooleanValueError(value));
                }
                else {
                    return false;
                }
            case "number":
                return value ? true : false;
            default:
                return false;
        }
    }
    function strictToBoolean(value) {
        if (value === "") {
            throw new Error(expectBooleanValueError(value));
        }
        return toBoolean(value);
    }
    function toJsDate(value, locale) {
        return numberToJsDate(toNumber(value, locale));
    }
    // -----------------------------------------------------------------------------
    // VISIT FUNCTIONS
    // -----------------------------------------------------------------------------
    function visitArgs(args, cellCb, dataCb) {
        for (let arg of args) {
            if (isMatrix(arg)) {
                // arg is ref to a Cell/Range
                const lenRow = arg.length;
                const lenCol = arg[0].length;
                for (let y = 0; y < lenCol; y++) {
                    for (let x = 0; x < lenRow; x++) {
                        cellCb(arg[x][y]);
                    }
                }
            }
            else {
                // arg is set directly in the formula function
                dataCb(arg);
            }
        }
    }
    function visitAny(args, cb) {
        visitArgs(args, cb, cb);
    }
    function visitNumbers(args, cb, locale) {
        visitArgs(args, (cellValue) => {
            if (typeof cellValue === "number") {
                cb(cellValue);
            }
        }, (argValue) => {
            cb(strictToNumber(argValue, locale));
        });
    }
    // -----------------------------------------------------------------------------
    // REDUCE FUNCTIONS
    // -----------------------------------------------------------------------------
    function reduceArgs(args, cellCb, dataCb, initialValue, dir = "rowFirst") {
        let val = initialValue;
        for (let arg of args) {
            if (isMatrix(arg)) {
                // arg is ref to a Cell/Range
                const numberOfCols = arg.length;
                const numberOfRows = arg[0].length;
                if (dir === "rowFirst") {
                    for (let row = 0; row < numberOfRows; row++) {
                        for (let col = 0; col < numberOfCols; col++) {
                            val = cellCb(val, arg[col][row]);
                        }
                    }
                }
                else {
                    for (let col = 0; col < numberOfCols; col++) {
                        for (let row = 0; row < numberOfRows; row++) {
                            val = cellCb(val, arg[col][row]);
                        }
                    }
                }
            }
            else {
                // arg is set directly in the formula function
                val = dataCb(val, arg);
            }
        }
        return val;
    }
    function reduceAny(args, cb, initialValue, dir = "rowFirst") {
        return reduceArgs(args, cb, cb, initialValue, dir);
    }
    function reduceNumbers(args, cb, initialValue, locale) {
        return reduceArgs(args, (acc, ArgValue) => {
            if (typeof ArgValue === "number") {
                return cb(acc, ArgValue);
            }
            return acc;
        }, (acc, argValue) => {
            return cb(acc, strictToNumber(argValue, locale));
        }, initialValue);
    }
    function reduceNumbersTextAs0(args, cb, initialValue, locale) {
        return reduceArgs(args, (acc, ArgValue) => {
            if (ArgValue !== undefined && ArgValue !== null) {
                if (typeof ArgValue === "number") {
                    return cb(acc, ArgValue);
                }
                else if (typeof ArgValue === "boolean") {
                    return cb(acc, toNumber(ArgValue, locale));
                }
                else {
                    return cb(acc, 0);
                }
            }
            return acc;
        }, (acc, argValue) => {
            return cb(acc, toNumber(argValue, locale));
        }, initialValue);
    }
    // -----------------------------------------------------------------------------
    // MATRIX FUNCTIONS
    // -----------------------------------------------------------------------------
    /**
     * Generate a matrix of size nColumns x nRows and apply a callback on each position
     */
    function generateMatrix(nColumns, nRows, callback) {
        const returned = Array(nColumns);
        for (let col = 0; col < nColumns; col++) {
            returned[col] = Array(nRows);
            for (let row = 0; row < nRows; row++) {
                returned[col][row] = callback(col, row);
            }
        }
        return returned;
    }
    function matrixMap(matrix, fn) {
        if (matrix.length === 0) {
            return [];
        }
        return generateMatrix(matrix.length, matrix[0].length, (col, row) => fn(matrix[col][row]));
    }
    function transposeMatrix(matrix) {
        if (!matrix.length) {
            return [];
        }
        return generateMatrix(matrix[0].length, matrix.length, (i, j) => matrix[j][i]);
    }
    // -----------------------------------------------------------------------------
    // CONDITIONAL EXPLORE FUNCTIONS
    // -----------------------------------------------------------------------------
    /**
     * This function allows to visit arguments and stop the visit if necessary.
     * It is mainly used to bypass argument evaluation for functions like OR or AND.
     */
    function conditionalVisitArgs(args, cellCb, dataCb) {
        for (let arg of args) {
            if (isMatrix(arg)) {
                // arg is ref to a Cell/Range
                const lenRow = arg.length;
                const lenCol = arg[0].length;
                for (let y = 0; y < lenCol; y++) {
                    for (let x = 0; x < lenRow; x++) {
                        if (!cellCb(arg[x][y] ?? undefined))
                            return;
                    }
                }
            }
            else {
                // arg is set directly in the formula function
                if (!dataCb(arg))
                    return;
            }
        }
    }
    function conditionalVisitBoolean(args, cb) {
        return conditionalVisitArgs(args, (ArgValue) => {
            if (typeof ArgValue === "boolean") {
                return cb(ArgValue);
            }
            if (typeof ArgValue === "number") {
                return cb(ArgValue ? true : false);
            }
            return true;
        }, (argValue) => {
            if (argValue !== undefined && argValue !== null) {
                return cb(strictToBoolean(argValue));
            }
            return true;
        });
    }
    function getPredicate(descr, isQuery, locale) {
        let operator;
        let operand;
        let subString = descr.substring(0, 2);
        if (subString === "<=" || subString === ">=" || subString === "<>") {
            operator = subString;
            operand = descr.substring(2);
        }
        else {
            subString = descr.substring(0, 1);
            if (subString === "<" || subString === ">" || subString === "=") {
                operator = subString;
                operand = descr.substring(1);
            }
            else {
                operator = "=";
                operand = descr;
            }
        }
        if (isNumber(operand, locale)) {
            operand = toNumber(operand, locale);
        }
        else if (operand === "TRUE" || operand === "FALSE") {
            operand = toBoolean(operand);
        }
        const result = { operator, operand };
        if (typeof operand === "string") {
            if (isQuery) {
                operand += "*";
            }
            result.regexp = operandToRegExp(operand);
        }
        return result;
    }
    function operandToRegExp(operand) {
        let exp = "";
        let predecessor = "";
        for (let char of operand) {
            if (char === "?" && predecessor !== "~") {
                exp += ".";
            }
            else if (char === "*" && predecessor !== "~") {
                exp += ".*";
            }
            else {
                if (char === "*" || char === "?") {
                    //remove "~"
                    exp = exp.slice(0, -1);
                }
                if (["^", ".", "[", "]", "$", "(", ")", "*", "+", "?", "|", "{", "}", "\\"].includes(char)) {
                    exp += "\\";
                }
                exp += char;
            }
            predecessor = char;
        }
        return new RegExp("^" + exp + "$", "i");
    }
    function evaluatePredicate(value, criterion) {
        const { operator, operand } = criterion;
        if (value === undefined || operand === undefined || value === null || operand === null) {
            return false;
        }
        if (typeof operand === "number" && operator === "=") {
            return toString(value) === toString(operand);
        }
        if (operator === "<>" || operator === "=") {
            let result;
            if (typeof value === typeof operand) {
                if (typeof value === "string" && criterion.regexp) {
                    result = criterion.regexp.test(value);
                }
                else {
                    result = value === operand;
                }
            }
            else {
                result = false;
            }
            return operator === "=" ? result : !result;
        }
        if (typeof value === typeof operand) {
            switch (operator) {
                case "<":
                    return value < operand;
                case ">":
                    return value > operand;
                case "<=":
                    return value <= operand;
                case ">=":
                    return value >= operand;
            }
        }
        return false;
    }
    /**
     * Functions used especially for predicate evaluation on ranges.
     *
     * Take ranges with same dimensions and take predicates, one for each range.
     * For (i, j) coordinates, if all elements with coordinates (i, j) of each
     * range correspond to the associated predicate, then the function uses a callback
     * function with the parameters "i" and "j".
     *
     * Syntax:
     * visitMatchingRanges([range1, predicate1, range2, predicate2, ...], cb(i,j), likeSelection)
     *
     * - range1 (range): The range to check against predicate1.
     * - predicate1 (string): The pattern or test to apply to range1.
     * - range2: (range, repeatable) ranges to check.
     * - predicate2 (string, repeatable): Additional pattern or test to apply to range2.
     *
     * - cb(i: number, j: number) => void: the callback function.
     *
     * - isQuery (boolean) indicates if the comparison with a string should be done as a SQL-like query.
     * (Ex1 isQuery = true, predicate = "abc", element = "abcde": predicate match the element),
     * (Ex2 isQuery = false, predicate = "abc", element = "abcde": predicate not match the element).
     * (Ex3 isQuery = true, predicate = "abc", element = "abc": predicate match the element),
     * (Ex4 isQuery = false, predicate = "abc", element = "abc": predicate match the element).
     */
    function visitMatchingRanges(args, cb, locale, isQuery = false) {
        const countArg = args.length;
        if (countArg % 2 === 1) {
            throw new Error(_t("Function [[FUNCTION_NAME]] expects criteria_range and criterion to be in pairs."));
        }
        const dimRow = args[0].length;
        const dimCol = args[0][0].length;
        let predicates = [];
        for (let i = 0; i < countArg - 1; i += 2) {
            const criteriaRange = args[i];
            if (!isMatrix(criteriaRange) ||
                criteriaRange.length !== dimRow ||
                criteriaRange[0].length !== dimCol) {
                throw new Error(_t("Function [[FUNCTION_NAME]] expects criteria_range to have the same dimension"));
            }
            const description = toString(args[i + 1]);
            predicates.push(getPredicate(description, isQuery, locale));
        }
        for (let i = 0; i < dimRow; i++) {
            for (let j = 0; j < dimCol; j++) {
                let validatedPredicates = true;
                for (let k = 0; k < countArg - 1; k += 2) {
                    const criteriaValue = args[k][i][j];
                    const criterion = predicates[k / 2];
                    validatedPredicates = evaluatePredicate(criteriaValue ?? undefined, criterion);
                    if (!validatedPredicates) {
                        break;
                    }
                }
                if (validatedPredicates) {
                    cb(i, j);
                }
            }
        }
    }
    // -----------------------------------------------------------------------------
    // COMMON FUNCTIONS
    // -----------------------------------------------------------------------------
    /**
     * Perform a dichotomic search on an array and return the index of the nearest match.
     *
     * The array should be sorted, if not an incorrect value might be returned. In the case where multiple
     * element of the array match the target, the method will return the first match if the array is sorted
     * in descending order, and the last match if the array is in ascending order.
     *
     *
     * @param data the array in which to search.
     * @param target the value to search.
     * @param mode "nextGreater/nextSmaller" : return next greater/smaller value if no exact match is found.
     * @param sortOrder whether the array is sorted in ascending or descending order.
     * @param rangeLength the number of elements to consider in the search array.
     * @param getValueInData function returning the element at index i in the search array.
     */
    function dichotomicSearch(data, target, mode, sortOrder, rangeLength, getValueInData) {
        if (target === null || target === undefined) {
            return -1;
        }
        const _target = normalizeValue(target);
        const targetType = typeof _target;
        let matchVal = undefined;
        let matchValIndex = undefined;
        let indexLeft = 0;
        let indexRight = rangeLength - 1;
        let indexMedian;
        let currentIndex;
        let currentVal;
        let currentType;
        while (indexRight - indexLeft >= 0) {
            indexMedian = Math.floor((indexLeft + indexRight) / 2);
            currentIndex = indexMedian;
            currentVal = normalizeValue(getValueInData(data, currentIndex));
            currentType = typeof currentVal;
            // 1 - linear search to find value with the same type
            while (indexLeft < currentIndex && targetType !== currentType) {
                currentIndex--;
                currentVal = normalizeValue(getValueInData(data, currentIndex));
                currentType = typeof currentVal;
            }
            if (currentType !== targetType || currentVal === undefined || currentVal === null) {
                indexLeft = indexMedian + 1;
                continue;
            }
            // 2 - check if value match
            if (mode === "strict" && currentVal === _target) {
                matchVal = currentVal;
                matchValIndex = currentIndex;
            }
            else if (mode === "nextSmaller" && currentVal <= _target) {
                if (matchVal === undefined ||
                    matchVal === null ||
                    matchVal < currentVal ||
                    (matchVal === currentVal && sortOrder === "asc" && matchValIndex < currentIndex) ||
                    (matchVal === currentVal && sortOrder === "desc" && matchValIndex > currentIndex)) {
                    matchVal = currentVal;
                    matchValIndex = currentIndex;
                }
            }
            else if (mode === "nextGreater" && currentVal >= _target) {
                if (matchVal === undefined ||
                    matchVal > currentVal ||
                    (matchVal === currentVal && sortOrder === "asc" && matchValIndex < currentIndex) ||
                    (matchVal === currentVal && sortOrder === "desc" && matchValIndex > currentIndex)) {
                    matchVal = currentVal;
                    matchValIndex = currentIndex;
                }
            }
            // 3 - give new indexes for the Binary search
            if ((sortOrder === "asc" && currentVal > _target) ||
                (sortOrder === "desc" && currentVal <= _target)) {
                indexRight = currentIndex - 1;
            }
            else {
                indexLeft = indexMedian + 1;
            }
        }
        // note that valMinIndex could be 0
        return matchValIndex !== undefined ? matchValIndex : -1;
    }
    /**
     * Perform a linear search and return the index of the match.
     * -1 is returned if no value is found.
     *
     * Example:
     * - [3, 6, 10], 3 => 0
     * - [3, 6, 10], 6 => 1
     * - [3, 6, 10], 9 => -1
     * - [3, 6, 10], 2 => -1
     *
     * @param data the array to search in.
     * @param target the value to search in the array.
     * @param mode if "strict" return exact match index. "nextGreater" returns the next greater
     * element from the target and "nextSmaller" the next smaller
     * @param numberOfValues the number of elements to consider in the search array.
     * @param getValueInData function returning the element at index i in the search array.
     * @param reverseSearch if true, search in the array starting from the end.

     */
    function linearSearch(data, target, mode, numberOfValues, getValueInData, reverseSearch = false) {
        if (target === null || target === undefined)
            return -1;
        const _target = normalizeValue(target);
        const getValue = reverseSearch
            ? (data, i) => getValueInData(data, numberOfValues - i - 1)
            : getValueInData;
        let closestMatch = undefined;
        let closestMatchIndex = -1;
        for (let i = 0; i < numberOfValues; i++) {
            const value = normalizeValue(getValue(data, i));
            if (value === _target) {
                return reverseSearch ? numberOfValues - i - 1 : i;
            }
            if (mode === "nextSmaller") {
                if ((!closestMatch && compareCellValues(_target, value) >= 0) ||
                    (compareCellValues(_target, value) >= 0 && compareCellValues(value, closestMatch) > 0)) {
                    closestMatch = value;
                    closestMatchIndex = i;
                }
            }
            else if (mode === "nextGreater") {
                if ((!closestMatch && compareCellValues(_target, value) <= 0) ||
                    (compareCellValues(_target, value) <= 0 && compareCellValues(value, closestMatch) < 0)) {
                    closestMatch = value;
                    closestMatchIndex = i;
                }
            }
        }
        return reverseSearch ? numberOfValues - closestMatchIndex - 1 : closestMatchIndex;
    }
    /**
     * Normalize a value.
     * If the cell value is a string, this will set it to lowercase and replacing accent letters with plain letters
     */
    function normalizeValue(value) {
        return typeof value === "string" ? normalizeString(value) : value;
    }
    function compareCellValues(left, right) {
        let typeOrder = SORT_TYPES_ORDER.indexOf(typeof left) - SORT_TYPES_ORDER.indexOf(typeof right);
        if (typeOrder === 0) {
            if (typeof left === "string" && typeof right === "string") {
                typeOrder = left.localeCompare(right);
            }
            else if (typeof left === "number" && typeof right === "number") {
                typeOrder = left - right;
            }
            else if (typeof left === "boolean" && typeof right === "boolean") {
                typeOrder = Number(left) - Number(right);
            }
        }
        return typeOrder;
    }
    function toMatrix(data) {
        if (data === undefined) {
            return [[]];
        }
        return isMatrix(data) ? data : [[data]];
    }
    /**
     * Flatten an array of items, where each item can be a single value or a 2D array, and apply the
     * callback to each element.
     *
     * The 2D array are flattened row first.
     */
    function flattenRowFirst(items, callback) {
        /**/
        return reduceAny(items, (array, val) => {
            array.push(callback(val));
            return array;
        }, [], "rowFirst");
    }

    function toCriterionDateNumber(dateValue) {
        const today = DateTime.now();
        switch (dateValue) {
            case "today":
                return jsDateToNumber(today);
            case "yesterday":
                return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 1)));
            case "tomorrow":
                return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() + 1)));
            case "lastWeek":
                return jsDateToNumber(DateTime.fromTimestamp(today.setDate(today.getDate() - 7)));
            case "lastMonth":
                return jsDateToNumber(DateTime.fromTimestamp(today.setMonth(today.getMonth() - 1)));
            case "lastYear":
                return jsDateToNumber(DateTime.fromTimestamp(today.setFullYear(today.getFullYear() - 1)));
        }
    }
    /** Get all the dates values of a criterion converted to numbers, converting date values such as "today" to actual dates  */
    function getDateNumberCriterionValues(criterion, locale) {
        if ("dateValue" in criterion && criterion.dateValue !== "exactDate") {
            return [toCriterionDateNumber(criterion.dateValue)];
        }
        return criterion.values.map((value) => valueToDateNumber(value, locale));
    }
    /** Convert the criterion values to numbers. Return undefined values if they cannot be converted to numbers. */
    function getCriterionValuesAsNumber(criterion, locale) {
        return criterion.values.map((value) => tryToNumber(value, locale));
    }

    const MAX_DELAY = 140;
    const MIN_DELAY = 20;
    const ACCELERATION = 0.035;
    /**
     * Decreasing exponential function used to determine the "speed" of edge-scrolling
     * as the timeout delay.
     *
     * Returns a timeout delay in milliseconds.
     */
    function scrollDelay(value) {
        // decreasing exponential from MAX_DELAY to MIN_DELAY
        return MIN_DELAY + (MAX_DELAY - MIN_DELAY) * Math.exp(-ACCELERATION * (value - 1));
    }

    /**
     *  Constant used to indicate the maximum of digits that is possible to display
     *  in a cell with standard size.
     */
    const MAX_DECIMAL_PLACES = 20;
    /**
     * Number of digits for the default number format. This number of digit make a number fit well in a cell
     * with default size and default font size.
     */
    const DEFAULT_FORMAT_NUMBER_OF_DIGITS = 11;
    //from https://stackoverflow.com/questions/721304/insert-commas-into-number-string @Thomas/Alan Moore
    const thousandsGroupsRegexp = /(\d+?)(?=(\d{3})+(?!\d)|$)/g;
    const zeroRegexp = /0/g;
    // TODO in the future : remove these constants MONTHS/DAYS, and use a library such as luxon to handle it
    // + possibly handle automatic translation of day/month
    const MONTHS = {
        0: _t("January"),
        1: _t("February"),
        2: _t("March"),
        3: _t("April"),
        4: _t("May"),
        5: _t("June"),
        6: _t("July"),
        7: _t("August"),
        8: _t("September"),
        9: _t("October"),
        10: _t("November"),
        11: _t("December"),
    };
    const DAYS$1 = {
        0: _t("Sunday"),
        1: _t("Monday"),
        2: _t("Tuesday"),
        3: _t("Wednesday"),
        4: _t("Thursday"),
        5: _t("Friday"),
        6: _t("Saturday"),
    };
    // -----------------------------------------------------------------------------
    // FORMAT REPRESENTATION CACHE
    // -----------------------------------------------------------------------------
    const internalFormatByFormatString = {};
    function parseFormat(formatString) {
        let internalFormat = internalFormatByFormatString[formatString];
        if (internalFormat === undefined) {
            internalFormat = convertFormatToInternalFormat(formatString);
            internalFormatByFormatString[formatString] = internalFormat;
        }
        return internalFormat;
    }
    // -----------------------------------------------------------------------------
    // APPLY FORMAT
    // -----------------------------------------------------------------------------
    /**
     * Formats a cell value with its format.
     */
    function formatValue(value, { format, locale }) {
        switch (typeof value) {
            case "string":
                if (value.includes('\\"')) {
                    return value.replace(/\\"/g, '"');
                }
                return value;
            case "boolean":
                return value ? "TRUE" : "FALSE";
            case "number":
                // transform to internalNumberFormat
                if (!format) {
                    format = createDefaultFormat(value);
                }
                const internalFormat = parseFormat(format);
                return applyInternalFormat(value, internalFormat, locale);
            case "object":
                return "0";
        }
    }
    function applyInternalFormat(value, internalFormat, locale) {
        if (internalFormat[0].type === "DATE") {
            return applyDateTimeFormat(value, internalFormat[0].format);
        }
        let formattedValue = value < 0 ? "-" : "";
        for (let part of internalFormat) {
            switch (part.type) {
                case "NUMBER":
                    formattedValue += applyInternalNumberFormat(Math.abs(value), part.format, locale);
                    break;
                case "STRING":
                    formattedValue += part.format;
                    break;
            }
        }
        return formattedValue;
    }
    function applyInternalNumberFormat(value, format, locale) {
        if (format.isPercent) {
            value = value * 100;
        }
        value = value / format.magnitude;
        let maxDecimals = 0;
        if (format.decimalPart !== undefined) {
            maxDecimals = format.decimalPart.length;
        }
        const { integerDigits, decimalDigits } = splitNumber(value, maxDecimals);
        let formattedValue = applyIntegerFormat(integerDigits, format.integerPart, format.thousandsSeparator ? locale.thousandsSeparator : undefined);
        if (format.decimalPart !== undefined) {
            formattedValue +=
                locale.decimalSeparator + applyDecimalFormat(decimalDigits || "", format.decimalPart);
        }
        if (format.isPercent) {
            formattedValue += "%";
        }
        return formattedValue;
    }
    function applyIntegerFormat(integerDigits, integerFormat, thousandsSeparator) {
        const _integerDigits = integerDigits === "0" ? "" : integerDigits;
        let formattedInteger = _integerDigits;
        const delta = integerFormat.length - _integerDigits.length;
        if (delta > 0) {
            // ex: format = "0#000000" and integerDigit: "123"
            const restIntegerFormat = integerFormat.substring(0, delta); // restIntegerFormat = "0#00"
            const countZero = (restIntegerFormat.match(zeroRegexp) || []).length; // countZero = 3
            formattedInteger = "0".repeat(countZero) + formattedInteger; // return "000123"
        }
        if (thousandsSeparator) {
            formattedInteger =
                formattedInteger.match(thousandsGroupsRegexp)?.join(thousandsSeparator) || formattedInteger;
        }
        return formattedInteger;
    }
    function applyDecimalFormat(decimalDigits, decimalFormat) {
        // assume the format is valid (no commas)
        let formattedDecimals = decimalDigits;
        if (decimalFormat.length - decimalDigits.length > 0) {
            const restDecimalFormat = decimalFormat.substring(decimalDigits.length, decimalFormat.length + 1);
            const countZero = (restDecimalFormat.match(zeroRegexp) || []).length;
            formattedDecimals = formattedDecimals + "0".repeat(countZero);
        }
        return formattedDecimals;
    }
    /**
     * this is a cache that can contains number representation formats
     * from 0 (minimum) to 20 (maximum) digits after the decimal point
     */
    const numberRepresentation = [];
    /** split a number into two strings that contain respectively:
     * - all digit stored in the integer part of the number
     * - all digit stored in the decimal part of the number
     *
     * The 'maxDecimal' parameter allows to indicate the number of digits to not
     * exceed in the decimal part, in which case digits are rounded.
     *
     **/
    function splitNumber(value, maxDecimals = MAX_DECIMAL_PLACES) {
        const asString = value.toString();
        if (asString.includes("e"))
            return splitNumberIntl(value, maxDecimals);
        if (Number.isInteger(value)) {
            return { integerDigits: asString, decimalDigits: undefined };
        }
        const indexOfDot = asString.indexOf(".");
        let integerDigits = asString.substring(0, indexOfDot);
        let decimalDigits = asString.substring(indexOfDot + 1);
        if (maxDecimals === 0) {
            if (Number(decimalDigits[0]) >= 5) {
                integerDigits = (Number(integerDigits) + 1).toString();
            }
            return { integerDigits, decimalDigits: undefined };
        }
        if (decimalDigits.length > maxDecimals) {
            const { integerDigits: roundedIntegerDigits, decimalDigits: roundedDecimalDigits } = limitDecimalDigits(decimalDigits, maxDecimals);
            decimalDigits = roundedDecimalDigits;
            if (roundedIntegerDigits !== "0") {
                integerDigits = (Number(integerDigits) + Number(roundedIntegerDigits)).toString();
            }
        }
        return { integerDigits, decimalDigits: removeTrailingZeroes(decimalDigits || "") };
    }
    /**
     *  Return the given string minus the trailing "0" characters.
     *
     * @param numberString : a string of integers
     * @returns the numberString, minus the eventual zeroes at the end
     */
    function removeTrailingZeroes(numberString) {
        let i = numberString.length - 1;
        while (i >= 0 && numberString[i] === "0") {
            i--;
        }
        return numberString.slice(0, i + 1) || undefined;
    }
    const leadingZeroesRegexp = /^0+/;
    /**
     * Limit the size of the decimal part of a number to the given number of digits.
     */
    function limitDecimalDigits(decimalDigits, maxDecimals) {
        let integerDigits = "0";
        let resultDecimalDigits = decimalDigits;
        // Note : we'd want to simply use number.toFixed() to handle the max digits & rounding,
        // but it has very strange behaviour. Ex: 12.345.toFixed(2) => "12.35", but 1.345.toFixed(2) => "1.34"
        let slicedDecimalDigits = decimalDigits.slice(0, maxDecimals);
        const i = maxDecimals;
        if (Number(decimalDigits[i]) < 5) {
            return { integerDigits, decimalDigits: slicedDecimalDigits };
        }
        // round up
        const leadingZeroes = slicedDecimalDigits.match(leadingZeroesRegexp)?.[0] || "";
        const slicedRoundedUp = (Number(slicedDecimalDigits) + 1).toString();
        const withoutLeadingZeroes = slicedDecimalDigits.slice(leadingZeroes.length);
        // e.g. carry over from 99 to 100
        const carryOver = slicedRoundedUp.length > withoutLeadingZeroes.length;
        if (carryOver && !leadingZeroes) {
            integerDigits = "1";
            resultDecimalDigits = undefined;
        }
        else if (carryOver) {
            resultDecimalDigits = leadingZeroes.slice(0, -1) + slicedRoundedUp;
        }
        else {
            resultDecimalDigits = leadingZeroes + slicedRoundedUp;
        }
        return { integerDigits, decimalDigits: resultDecimalDigits };
    }
    /**
     * Split numbers into decimal/integer digits using Intl.NumberFormat.
     * Supports numbers with a lot of digits that are transformed to scientific notation by
     * number.toString(), but is slow.
     */
    function splitNumberIntl(value, maxDecimals = MAX_DECIMAL_PLACES) {
        let formatter = numberRepresentation[maxDecimals];
        if (!formatter) {
            formatter = new Intl.NumberFormat("en-US", {
                maximumFractionDigits: maxDecimals,
                useGrouping: false,
            });
            numberRepresentation[maxDecimals] = formatter;
        }
        const [integerDigits, decimalDigits] = formatter.format(value).split(".");
        return { integerDigits, decimalDigits };
    }
    /** Convert a number into a string, without scientific notation */
    function numberToString(number, decimalSeparator) {
        const { integerDigits, decimalDigits } = splitNumber(number, 20);
        return decimalDigits ? integerDigits + decimalSeparator + decimalDigits : integerDigits;
    }
    /**
     * Check if the given format is a time, date or date time format.
     */
    function isDateTimeFormat(format) {
        if (!allowedDateTimeFormatFirstChar.has(format[0])) {
            // first check for performance reason
            return false;
        }
        try {
            applyDateTimeFormat(1, format);
            return true;
        }
        catch (error) {
            return false;
        }
    }
    const allowedDateTimeFormatFirstChar = new Set(["h", "m", "y", "d"]);
    function applyDateTimeFormat(value, format) {
        // TODO: unify the format functions for date and datetime
        // This requires some code to 'parse' or 'tokenize' the format, keep it in a
        // cache, and use it in a single mapping, that recognizes the special list
        // of tokens dd,d,m,y,h, ... and preserves the rest
        const jsDate = numberToJsDate(value);
        const indexH = format.indexOf("h");
        let strDate = "";
        let strTime = "";
        if (indexH > 0) {
            strDate = formatJSDate(jsDate, format.substring(0, indexH - 1));
            strTime = formatJSTime(jsDate, format.substring(indexH));
        }
        else if (indexH === 0) {
            strTime = formatJSTime(jsDate, format);
        }
        else if (indexH < 0) {
            strDate = formatJSDate(jsDate, format);
        }
        return strDate + (strDate && strTime ? " " : "") + strTime;
    }
    function formatJSDate(jsDate, format) {
        const sep = format.match(/\/|-|\s/)?.[0];
        const parts = sep ? format.split(sep) : [format];
        return parts
            .map((p) => {
            switch (p) {
                case "d":
                    return jsDate.getDate();
                case "dd":
                    return jsDate.getDate().toString().padStart(2, "0");
                case "ddd":
                    return DAYS$1[jsDate.getDay()].slice(0, 3);
                case "dddd":
                    return DAYS$1[jsDate.getDay()];
                case "m":
                    return jsDate.getMonth() + 1;
                case "mm":
                    return String(jsDate.getMonth() + 1).padStart(2, "0");
                case "mmm":
                    return MONTHS[jsDate.getMonth()].slice(0, 3);
                case "mmmm":
                    return MONTHS[jsDate.getMonth()];
                case "mmmmm":
                    return MONTHS[jsDate.getMonth()].slice(0, 1);
                case "yy":
                    const fullYear = String(jsDate.getFullYear()).replace("-", "").padStart(2, "0");
                    return fullYear.slice(fullYear.length - 2);
                case "yyyy":
                    return jsDate.getFullYear();
                default:
                    throw new Error(`invalid format: ${format}`);
            }
        })
            .join(sep);
    }
    function formatJSTime(jsDate, format) {
        let parts = format.split(/:|\s/);
        const dateHours = jsDate.getHours();
        const isMeridian = parts[parts.length - 1] === "a";
        let hours = dateHours;
        let meridian = "";
        if (isMeridian) {
            hours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
            meridian = dateHours >= 12 ? " PM" : " AM";
            parts.pop();
        }
        return (parts
            .map((p) => {
            switch (p) {
                case "hhhh":
                    const helapsedHours = Math.floor((jsDate.getTime() - INITIAL_1900_DAY.getTime()) / (60 * 60 * 1000));
                    return helapsedHours.toString();
                case "hh":
                    return hours.toString().padStart(2, "0");
                case "mm":
                    return jsDate.getMinutes().toString().padStart(2, "0");
                case "ss":
                    return jsDate.getSeconds().toString().padStart(2, "0");
                default:
                    throw new Error(`invalid format: ${format}`);
            }
        })
            .join(":") + meridian);
    }
    /**
     * Get a regex matching decimal number based on the locale's thousand separator
     *
     * eg. if the locale's thousand separator is a comma, this will return a regex /[0-9]+,[0-9]/
     */
    const getDecimalNumberRegex = memoize(function getDecimalNumberRegex(locale) {
        return new RegExp(`[0-9]+${escapeRegExp(locale.decimalSeparator)}[0-9]`);
    });
    // -----------------------------------------------------------------------------
    // CREATE / MODIFY FORMAT
    // -----------------------------------------------------------------------------
    /**
     * Create a default format for a number.
     *
     * If possible this will try round the number to have less than DEFAULT_FORMAT_NUMBER_OF_DIGITS characters
     * in the number. This is obviously only possible for number with a big decimal part. For number with a lot
     * of digits in the integer part, keep the number as it is.
     */
    function createDefaultFormat(value) {
        let { integerDigits, decimalDigits } = splitNumber(value);
        if (!decimalDigits)
            return "0";
        const digitsInIntegerPart = integerDigits.replace("-", "").length;
        // If there's no space for at least the decimal separator + a decimal digit, don't display decimals
        if (digitsInIntegerPart + 2 > DEFAULT_FORMAT_NUMBER_OF_DIGITS) {
            return "0";
        }
        // -1 for the decimal separator character
        const spaceForDecimalsDigits = DEFAULT_FORMAT_NUMBER_OF_DIGITS - digitsInIntegerPart - 1;
        ({ decimalDigits } = splitNumber(value, Math.min(spaceForDecimalsDigits, decimalDigits.length)));
        return decimalDigits ? "0." + "0".repeat(decimalDigits.length) : "0";
    }
    function detectDateFormat(content, locale) {
        if (!isDateTime(content, locale)) {
            return undefined;
        }
        const internalDate = parseDateTime(content, locale);
        return internalDate.format;
    }
    function detectNumberFormat(content) {
        if (!isNumber(content, DEFAULT_LOCALE)) {
            return undefined;
        }
        const digitBase = content.includes(".") ? "0.00" : "0";
        const matchedCurrencies = content.match(/[\$€]/);
        if (matchedCurrencies) {
            const matchedFirstDigit = content.match(/[\d]/);
            const currency = "[$" + matchedCurrencies.values().next().value + "]";
            if (matchedFirstDigit.index < matchedCurrencies.index) {
                return "#,##" + digitBase + currency;
            }
            return currency + "#,##" + digitBase;
        }
        if (content.includes("%")) {
            return digitBase + "%";
        }
        return undefined;
    }
    function createCurrencyFormat(currency) {
        const decimalPlaces = currency.decimalPlaces ?? 2;
        const position = currency.position ?? "before";
        const code = currency.code ?? "";
        const symbol = currency.symbol ?? "";
        const decimalRepresentation = decimalPlaces ? "." + "0".repeat(decimalPlaces) : "";
        const numberFormat = "#,##0" + decimalRepresentation;
        let textExpression = `${code} ${symbol}`.trim();
        if (position === "after" && code) {
            textExpression = " " + textExpression;
        }
        return insertTextInFormat(textExpression, position, numberFormat);
    }
    function insertTextInFormat(text, position, format) {
        const textExpression = `[$${text}]`;
        return position === "before" ? textExpression + format : format + textExpression;
    }
    function roundFormat(format) {
        const internalFormat = parseFormat(format);
        const roundedFormat = internalFormat.map((formatPart) => {
            if (formatPart.type === "NUMBER") {
                return {
                    type: formatPart.type,
                    format: {
                        ...formatPart.format,
                        decimalPart: undefined,
                    },
                };
            }
            return formatPart;
        });
        return convertInternalFormatToFormat(roundedFormat);
    }
    function createLargeNumberFormat(format, magnitude, postFix, locale) {
        const internalFormat = parseFormat(format || "#,##0");
        const largeNumberFormat = [];
        for (let i = 0; i < internalFormat.length; i++) {
            const formatPart = internalFormat[i];
            if (formatPart.type !== "NUMBER") {
                largeNumberFormat.push(formatPart);
                continue;
            }
            largeNumberFormat.push({
                ...formatPart,
                format: {
                    ...formatPart.format,
                    magnitude,
                    decimalPart: undefined,
                },
            });
            largeNumberFormat.push({
                type: "STRING",
                format: postFix,
            });
            const nextFormatPart = internalFormat[i + 1];
            if (nextFormatPart?.type === "STRING" && ["k", "m", "b"].includes(nextFormatPart.format)) {
                i++;
            }
        }
        return convertInternalFormatToFormat(largeNumberFormat);
    }
    function changeDecimalPlaces(format, step, locale) {
        const internalFormat = parseFormat(format);
        const newInternalFormat = internalFormat.map((intFmt) => {
            if (intFmt.type === "NUMBER") {
                return { ...intFmt, format: changeInternalNumberFormatDecimalPlaces(intFmt.format, step) };
            }
            else {
                return intFmt;
            }
        });
        const newFormat = convertInternalFormatToFormat(newInternalFormat);
        internalFormatByFormatString[newFormat] = newInternalFormat;
        return newFormat;
    }
    function changeInternalNumberFormatDecimalPlaces(format, step) {
        const _format = { ...format };
        const sign = Math.sign(step);
        const decimalLength = _format.decimalPart?.length || 0;
        const countZero = Math.min(Math.max(0, decimalLength + sign), MAX_DECIMAL_PLACES);
        _format.decimalPart = "0".repeat(countZero);
        if (_format.decimalPart === "") {
            delete _format.decimalPart;
        }
        return _format;
    }
    // -----------------------------------------------------------------------------
    // MANAGING FORMAT
    // -----------------------------------------------------------------------------
    /**
     * Validates the provided format string and returns an InternalFormat Object.
     */
    function convertFormatToInternalFormat(format) {
        if (format === "") {
            throw new Error("A format cannot be empty");
        }
        let currentIndex = 0;
        let result = [];
        while (currentIndex < format.length) {
            let closingIndex;
            if (format.charAt(currentIndex) === "[") {
                if (format.charAt(currentIndex + 1) !== "$") {
                    throw new Error(`Currency formats have to be prefixed by a $: ${format}`);
                }
                // manage brackets/customStrings
                closingIndex = format.substring(currentIndex + 1).indexOf("]") + currentIndex + 2;
                if (closingIndex === 0) {
                    throw new Error(`Invalid currency brackets format: ${format}`);
                }
                const str = format.substring(currentIndex + 2, closingIndex - 1);
                if (str.includes("[")) {
                    throw new Error(`Invalid currency format: ${format}`);
                }
                result.push({
                    type: "STRING",
                    format: str,
                }); // remove leading "[$"" and ending "]".
            }
            else {
                // rest of the time
                const nextPartIndex = format.substring(currentIndex).indexOf("[");
                closingIndex = nextPartIndex > -1 ? nextPartIndex + currentIndex : format.length;
                const subFormat = format.substring(currentIndex, closingIndex);
                if (isDateTimeFormat(subFormat)) {
                    result.push({ type: "DATE", format: subFormat });
                }
                else {
                    result.push({
                        type: "NUMBER",
                        format: convertToInternalNumberFormat(subFormat),
                    });
                }
            }
            currentIndex = closingIndex;
        }
        return result;
    }
    const magnitudeRegex = /,*?$/;
    /**
     * @param format a formatString that is only applicable to numbers. I.e. composed of characters 0 # , . %
     */
    function convertToInternalNumberFormat(format) {
        format = format.trim();
        if (containsInvalidNumberChars(format)) {
            throw new Error(`Invalid number format: ${format}`);
        }
        const isPercent = format.includes("%");
        const magnitudeCommas = format.match(magnitudeRegex)?.[0] || "";
        const magnitude = !magnitudeCommas ? 1 : 1000 ** magnitudeCommas.length;
        let _format = format.slice(0, format.length - (magnitudeCommas.length || 0));
        const thousandsSeparator = _format.includes(",");
        if (/\..*,/.test(_format)) {
            throw new Error("A format can't contain ',' symbol in the decimal part");
        }
        _format = _format.replace("%", "").replace(",", "");
        const extraSigns = _format.match(/[\%|,]/);
        if (extraSigns) {
            throw new Error(`A format can only contain a single '${extraSigns[0]}' symbol`);
        }
        const [integerPart, decimalPart] = _format.split(".");
        if (decimalPart && decimalPart.length > 20) {
            throw new Error("A format can't contain more than 20 decimal places");
        }
        if (decimalPart !== undefined) {
            return {
                integerPart,
                isPercent,
                thousandsSeparator,
                decimalPart,
                magnitude,
            };
        }
        else {
            return {
                integerPart,
                isPercent,
                thousandsSeparator,
                magnitude,
            };
        }
    }
    const validNumberChars = /[,#0.%]/g;
    function containsInvalidNumberChars(format) {
        return Boolean(format.replace(validNumberChars, ""));
    }
    function convertInternalFormatToFormat(internalFormat) {
        let format = "";
        for (let part of internalFormat) {
            let currentFormat;
            switch (part.type) {
                case "NUMBER":
                    const fmt = part.format;
                    currentFormat = fmt.integerPart;
                    if (fmt.thousandsSeparator) {
                        currentFormat = currentFormat.slice(0, -3) + "," + currentFormat.slice(-3);
                    }
                    if (fmt.decimalPart !== undefined) {
                        currentFormat += "." + fmt.decimalPart;
                    }
                    if (fmt.isPercent) {
                        currentFormat += "%";
                    }
                    if (fmt.magnitude) {
                        currentFormat += ",".repeat(Math.log10(fmt.magnitude) / 3);
                    }
                    break;
                case "STRING":
                    currentFormat = `[$${part.format}]`;
                    break;
                case "DATE":
                    currentFormat = part.format;
                    break;
            }
            format += currentFormat;
        }
        return format;
    }

    /** Reference of a cell (eg. A1, $B$5) */
    const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
    // Same as above, but matches the exact string (nothing before or after)
    const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
    /** Reference of a column header (eg. A, AB, $A) */
    const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
    /** Reference of a row header (eg. 1, $1) */
    const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
    /** Reference of a column (eg. A, $CA, Sheet1!B) */
    const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
    /** Reference of a row (eg. 1, 59, Sheet1!9) */
    const rowReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([0-9]{1,7})$/, "i");
    /** Reference of a normal range or a full row range (eg. A1:B1, 1:$5, $A2:5) */
    const fullRowXc = /(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*:\s*(\$?[A-Z]{1,3})?\$?[0-9]{1,7}\s*/i;
    /** Reference of a normal range or a column row range (eg. A1:B1, A:$B, $A1:C) */
    const fullColXc = /\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*:\s*\$?[A-Z]{1,3}(\$?[0-9]{1,7})?\s*/i;
    /** Reference of a cell or a range, it can be a bounded range, a full row or a full column */
    const rangeReference = new RegExp(/^\s*('.+'!|[^']+!)?/.source +
        "(" +
        [cellReference.source, fullRowXc.source, fullColXc.source].join("|") +
        ")" +
        /$/.source, "i");
    /**
     * Return true if the given xc is the reference of a column (e.g. A or AC or Sheet1!A)
     */
    function isColReference(xc) {
        return colReference.test(xc);
    }
    /**
     * Return true if the given xc is the reference of a column (e.g. 1 or Sheet1!1)
     */
    function isRowReference(xc) {
        return rowReference.test(xc);
    }
    function isColHeader(str) {
        return colHeader.test(str);
    }
    function isRowHeader(str) {
        return rowHeader.test(str);
    }
    /**
     * Return true if the given xc is the reference of a single cell,
     * without any specified sheet (e.g. A1)
     */
    function isSingleCellReference(xc) {
        return singleCellReference.test(xc);
    }
    function splitReference(ref) {
        const parts = ref.split("!");
        const xc = parts.pop();
        const sheetName = getUnquotedSheetName(parts.join("!")) || undefined;
        return { sheetName, xc };
    }

    /**
     * Convert from a cartesian reference to a Zone
     * The range boundaries will be kept in the same order as the
     * ones in the text.
     * Examples:
     *    "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
     *    "B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
     *    "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
     *    "Sheet1!B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
     *    "C3:A1" ==> Top 2, Bottom 0, Left 2, Right 0
     *    "A:A" ==> Top 0, Bottom undefined, Left 0, Right 0
     *    "A:B3" or "B3:A" ==> Top 2, Bottom undefined, Left 0, Right 1
     *
     * @param xc the string reference to convert
     *
     */
    function toZoneWithoutBoundaryChanges(xc) {
        if (xc.includes("!")) {
            xc = xc.split("!").at(-1);
        }
        if (xc.includes("$")) {
            xc = xc.replace(/\$/g, "");
        }
        let ranges;
        if (xc.includes(":")) {
            ranges = xc.split(":").map((x) => x.trim());
        }
        else {
            ranges = [xc.trim()];
        }
        let top, bottom, left, right;
        let fullCol = false;
        let fullRow = false;
        let hasHeader = false;
        const firstRangePart = ranges[0];
        const secondRangePart = ranges[1] && ranges[1];
        if (isColReference(firstRangePart)) {
            left = right = lettersToNumber(firstRangePart);
            top = bottom = 0;
            fullCol = true;
        }
        else if (isRowReference(firstRangePart)) {
            top = bottom = parseInt(firstRangePart, 10) - 1;
            left = right = 0;
            fullRow = true;
        }
        else {
            const c = toCartesian(firstRangePart);
            left = right = c.col;
            top = bottom = c.row;
            hasHeader = true;
        }
        if (ranges.length === 2) {
            if (isColReference(secondRangePart)) {
                right = lettersToNumber(secondRangePart);
                fullCol = true;
            }
            else if (isRowReference(secondRangePart)) {
                bottom = parseInt(secondRangePart, 10) - 1;
                fullRow = true;
            }
            else {
                const c = toCartesian(secondRangePart);
                right = c.col;
                bottom = c.row;
                top = fullCol ? bottom : top;
                left = fullRow ? right : left;
                hasHeader = true;
            }
        }
        if (fullCol && fullRow) {
            throw new Error("Wrong zone xc. The zone cannot be at the same time a full column and a full row");
        }
        const zone = {
            top,
            left,
            bottom: fullCol ? undefined : bottom,
            right: fullRow ? undefined : right,
        };
        hasHeader = hasHeader && (fullRow || fullCol);
        if (hasHeader) {
            zone.hasHeader = hasHeader;
        }
        return zone;
    }
    /**
     * Convert from a cartesian reference to a (possibly unbounded) Zone
     *
     * Examples:
     *    "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
     *    "B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
     *    "B:B" ==> Top 0, Bottom undefined, Left: 1, Right: 1
     *    "B2:B" ==> Top 1, Bottom undefined, Left: 1, Right: 1, hasHeader: 1
     *    "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
     *    "Sheet1!B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
     *
     * @param xc the string reference to convert
     *
     */
    function toUnboundedZone(xc) {
        const zone = toZoneWithoutBoundaryChanges(xc);
        if (zone.right !== undefined && zone.right < zone.left) {
            const tmp = zone.left;
            zone.left = zone.right;
            zone.right = tmp;
        }
        if (zone.bottom !== undefined && zone.bottom < zone.top) {
            const tmp = zone.top;
            zone.top = zone.bottom;
            zone.bottom = tmp;
        }
        return zone;
    }
    /**
     * Convert from a cartesian reference to a Zone.
     * Will return throw an error if given a unbounded zone (eg : A:A).
     *
     * Examples:
     *    "A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
     *    "B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
     *    "Sheet1!A1" ==> Top 0, Bottom 0, Left: 0, Right: 0
     *    "Sheet1!B1:B3" ==> Top 0, Bottom 3, Left: 1, Right: 1
     *
     * @param xc the string reference to convert
     *
     */
    function toZone(xc) {
        const zone = toUnboundedZone(xc);
        if (zone.bottom === undefined || zone.right === undefined) {
            throw new Error("This does not support unbounded ranges");
        }
        return zone;
    }
    /**
     * Check that the zone has valid coordinates and in
     * the correct order.
     */
    function isZoneValid(zone) {
        // Typescript *should* prevent this kind of errors but
        // it's better to be on the safe side at runtime as well.
        const { bottom, top, left, right } = zone;
        if ((bottom !== undefined && isNaN(bottom)) ||
            isNaN(top) ||
            isNaN(left) ||
            (right !== undefined && isNaN(right))) {
            return false;
        }
        return isZoneOrdered(zone) && zone.top >= 0 && zone.left >= 0;
    }
    /**
     * Check that the zone properties are in the correct order.
     */
    function isZoneOrdered(zone) {
        return ((zone.bottom === undefined || (zone.bottom >= zone.top && zone.bottom >= 0)) &&
            (zone.right === undefined || (zone.right >= zone.left && zone.right >= 0)));
    }
    /**
     * Convert from zone to a cartesian reference
     *
     */
    function zoneToXc(zone) {
        const { top, bottom, left, right } = zone;
        const hasHeader = "hasHeader" in zone ? zone.hasHeader : false;
        const isOneCell = top === bottom && left === right;
        if (bottom === undefined && right !== undefined) {
            return top === 0 && !hasHeader
                ? `${numberToLetters(left)}:${numberToLetters(right)}`
                : `${toXC(left, top)}:${numberToLetters(right)}`;
        }
        else if (right === undefined && bottom !== undefined) {
            return left === 0 && !hasHeader
                ? `${top + 1}:${bottom + 1}`
                : `${toXC(left, top)}:${bottom + 1}`;
        }
        else if (bottom !== undefined && right !== undefined) {
            return isOneCell ? toXC(left, top) : `${toXC(left, top)}:${toXC(right, bottom)}`;
        }
        throw new Error(_t("Bad zone format"));
    }
    /**
     * Expand a zone after inserting columns or rows.
     *
     * Don't resize the zone if a col/row was added right before/after the row but only move the zone.
     */
    function expandZoneOnInsertion(zone, start, base, position, quantity) {
        const dimension = start === "left" ? "columns" : "rows";
        const baseElement = position === "before" ? base - 1 : base;
        const end = start === "left" ? "right" : "bottom";
        const zoneEnd = zone[end];
        if (zone[start] <= baseElement && zoneEnd && zoneEnd > baseElement) {
            return createAdaptedZone(zone, dimension, "RESIZE", quantity);
        }
        if (baseElement < zone[start]) {
            return createAdaptedZone(zone, dimension, "MOVE", quantity);
        }
        return { ...zone };
    }
    /**
     * Update the selection after column/row addition
     */
    function updateSelectionOnInsertion(selection, start, base, position, quantity) {
        const dimension = start === "left" ? "columns" : "rows";
        const baseElement = position === "before" ? base - 1 : base;
        const end = start === "left" ? "right" : "bottom";
        if (selection[start] <= baseElement && selection[end] > baseElement) {
            return createAdaptedZone(selection, dimension, "RESIZE", quantity);
        }
        if (baseElement < selection[start]) {
            return createAdaptedZone(selection, dimension, "MOVE", quantity);
        }
        return { ...selection };
    }
    /**
     * Update the selection after column/row deletion
     */
    function updateSelectionOnDeletion(zone, start, elements) {
        const end = start === "left" ? "right" : "bottom";
        let newStart = zone[start];
        let newEnd = zone[end];
        for (let removedElement of elements.sort((a, b) => b - a)) {
            if (zone[start] > removedElement) {
                newStart--;
                newEnd--;
            }
            if (zone[start] < removedElement && zone[end] >= removedElement) {
                newEnd--;
            }
        }
        return { ...zone, [start]: newStart, [end]: newEnd };
    }
    /**
     * Reduce a zone after deletion of elements
     */
    function reduceZoneOnDeletion(zone, start, elements) {
        const end = start === "left" ? "right" : "bottom";
        let newStart = zone[start];
        let newEnd = zone[end];
        const zoneEnd = zone[end];
        for (let removedElement of elements.sort((a, b) => b - a)) {
            if (zone[start] > removedElement) {
                newStart--;
                if (newEnd !== undefined)
                    newEnd--;
            }
            if (zoneEnd !== undefined &&
                newEnd !== undefined &&
                zone[start] <= removedElement &&
                zoneEnd >= removedElement) {
                newEnd--;
            }
        }
        if (newEnd !== undefined && newStart > newEnd) {
            return undefined;
        }
        return { ...zone, [start]: newStart, [end]: newEnd };
    }
    /**
     * Compute the union of multiple zones.
     */
    function union(...zones) {
        return {
            top: Math.min(...zones.map((zone) => zone.top)),
            left: Math.min(...zones.map((zone) => zone.left)),
            bottom: Math.max(...zones.map((zone) => zone.bottom)),
            right: Math.max(...zones.map((zone) => zone.right)),
        };
    }
    /**
     * Compute the intersection of two zones. Returns nothing if the two zones don't overlap
     */
    function intersection(z1, z2) {
        if (!overlap(z1, z2)) {
            return undefined;
        }
        return {
            top: Math.max(z1.top, z2.top),
            left: Math.max(z1.left, z2.left),
            bottom: Math.min(z1.bottom, z2.bottom),
            right: Math.min(z1.right, z2.right),
        };
    }
    /**
     * Two zones are equal if they represent the same area, so we clearly cannot use
     * reference equality.
     */
    function isEqual(z1, z2) {
        return (z1.left === z2.left && z1.right === z2.right && z1.top === z2.top && z1.bottom === z2.bottom);
    }
    /**
     * Return true if two zones overlap, false otherwise.
     */
    function overlap(z1, z2) {
        if (z1.bottom < z2.top || z2.bottom < z1.top) {
            return false;
        }
        if (z1.right < z2.left || z2.right < z1.left) {
            return false;
        }
        return true;
    }
    function isInside(col, row, zone) {
        const { left, right, top, bottom } = zone;
        return col >= left && col <= right && row >= top && row <= bottom;
    }
    /**
     * Check if a zone is inside another
     */
    function isZoneInside(smallZone, biggerZone) {
        return isEqual(union(biggerZone, smallZone), biggerZone);
    }
    /**
     * Recompute the ranges of the zone to contain all the cells in zones, without the cells in toRemoveZones
     * Also regroup zones together to shorten the string
     */
    function recomputeZones(zonesXc, toRemoveZonesXc) {
        const zones = zonesXc.map(toUnboundedZone);
        const zonesToRemove = toRemoveZonesXc.map(toUnboundedZone);
        // Compute the max to replace the bottom of full columns and right of full rows by something
        // bigger than any other col/row to be able to apply the algorithm while keeping tracks of what
        // zones are full cols/rows
        const maxBottom = Math.max(...zones.concat(zonesToRemove).map((zone) => zone.bottom || 0));
        const maxRight = Math.max(...zones.concat(zonesToRemove).map((zone) => zone.right || 0));
        const expandedZones = zones.map((zone) => ({
            ...zone,
            bottom: zone.bottom === undefined ? maxBottom + 1 : zone.bottom,
            right: zone.right === undefined ? maxRight + 1 : zone.right,
        }));
        const expandedZonesToRemove = zonesToRemove.map((zone) => ({
            ...zone,
            bottom: zone.bottom === undefined ? maxBottom + 1 : zone.bottom,
            right: zone.right === undefined ? maxRight + 1 : zone.right,
        }));
        const zonePositions = expandedZones.map(positions).flat();
        const positionsToRemove = expandedZonesToRemove.map(positions).flat();
        const positionToKeep = positionsDifference(zonePositions, positionsToRemove);
        const columns = mergePositionsIntoColumns(positionToKeep);
        return mergeAlignedColumns(columns)
            .map((zone) => ({
            ...zone,
            bottom: zone.bottom === maxBottom + 1 ? undefined : zone.bottom,
            right: zone.right === maxRight + 1 ? undefined : zone.right,
        }))
            .map(zoneToXc);
    }
    /**
     * Merge aligned adjacent columns into single zones
     * e.g. A1:A5 and B1:B5 are merged into A1:B5
     */
    function mergeAlignedColumns(columns) {
        if (columns.length === 0) {
            return [];
        }
        if (columns.some((zone) => zone.left !== zone.right)) {
            throw new Error("only columns can be merged");
        }
        const done = [];
        const cols = removeRedundantZones(columns);
        const isAdjacentAndAligned = (zone, nextZone) => zone.top === nextZone.top &&
            zone.bottom === nextZone.bottom &&
            zone.right + 1 === nextZone.left;
        while (cols.length) {
            const merged = cols.reduce((zone, nextZone) => (isAdjacentAndAligned(zone, nextZone) ? union(zone, nextZone) : zone), cols.shift());
            done.push(merged);
        }
        return removeRedundantZones(done);
    }
    /**
     * Remove redundant zones in the list.
     * i.e. zones included in another zone.
     */
    function removeRedundantZones(zones) {
        const sortedZones = [...zones]
            .sort((a, b) => b.right - a.right)
            .sort((a, b) => b.bottom - a.bottom)
            .sort((a, b) => a.top - b.top)
            .sort((a, b) => a.left - b.left)
            .reverse();
        const checked = [];
        while (sortedZones.length !== 0) {
            const zone = sortedZones.shift();
            const isIncludedInOther = sortedZones.some((otherZone) => isZoneInside(zone, otherZone));
            if (!isIncludedInOther) {
                checked.push(zone);
            }
        }
        return checked.reverse();
    }
    /**
     * Merge adjacent positions into vertical zones (columns)
     */
    function mergePositionsIntoColumns(positions) {
        if (positions.length === 0) {
            return [];
        }
        const [startingPosition, ...sortedPositions] = [...positions]
            .sort((a, b) => a.row - b.row)
            .sort((a, b) => a.col - b.col);
        const done = [];
        let active = positionToZone(startingPosition);
        for (const { col, row } of sortedPositions) {
            if (isInside(col, row, active)) {
                continue;
            }
            else if (col === active.left && row === active.bottom + 1) {
                const bottom = active.bottom + 1;
                active = { ...active, bottom };
            }
            else {
                done.push(active);
                active = positionToZone({ col, row });
            }
        }
        return [...done, active];
    }
    /**
     * Returns positions in the first array which are not in the second array.
     */
    function positionsDifference(positions, toRemove) {
        const forbidden = new Set(toRemove.map(({ col, row }) => `${col}-${row}`));
        return positions.filter(({ col, row }) => !forbidden.has(`${col}-${row}`));
    }
    function zoneToDimension(zone) {
        return {
            numberOfRows: zone.bottom - zone.top + 1,
            numberOfCols: zone.right - zone.left + 1,
        };
    }
    function isOneDimensional(zone) {
        const { numberOfCols, numberOfRows } = zoneToDimension(zone);
        return numberOfCols === 1 || numberOfRows === 1;
    }
    /**
     * Array of all positions in the zone.
     */
    function positions(zone) {
        const positions = [];
        const [left, right] = [zone.right, zone.left].sort((a, b) => a - b);
        const [top, bottom] = [zone.top, zone.bottom].sort((a, b) => a - b);
        for (const col of range(left, right + 1)) {
            for (const row of range(top, bottom + 1)) {
                positions.push({ col, row });
            }
        }
        return positions;
    }
    function forEachPositionsInZone(zone, callback) {
        const { left, right, top, bottom } = zone;
        for (let col = left; col <= right; col++) {
            for (let row = top; row <= bottom; row++) {
                callback(col, row);
            }
        }
    }
    /**
     * This function returns a zone with coordinates modified according to the change
     * applied to the zone. It may be possible to change the zone by resizing or moving
     * it according to different dimensions.
     *
     * @param zone the zone to modify
     * @param dimension the direction to change the zone among "columns", "rows" and
     * "both"
     * @param operation how to change the zone, modify its size "RESIZE" or modify
     * its location "MOVE"
     * @param by a number of how many units the change should be made. This parameter
     * takes the form of a two-number array when the dimension is "both"
     */
    function createAdaptedZone(zone, dimension, operation, by) {
        const offsetX = dimension === "both" ? by[0] : dimension === "columns" ? by : 0;
        const offsetY = dimension === "both" ? by[1] : dimension === "rows" ? by : 0;
        // For full columns/rows, we have to make the distinction between the one that have a header and
        // whose start should be moved (eg. A2:A), and those who don't (eg. A:A)
        // The only time we don't want to move the start of the zone is if the zone is a full column (or a full row)
        // without header and that we are adding/removing a row (or a column)
        const hasHeader = "hasHeader" in zone ? zone.hasHeader : false;
        let shouldStartBeMoved;
        if (isFullCol(zone) && !hasHeader) {
            shouldStartBeMoved = dimension !== "rows";
        }
        else if (isFullRow(zone) && !hasHeader) {
            shouldStartBeMoved = dimension !== "columns";
        }
        else {
            shouldStartBeMoved = true;
        }
        const newZone = { ...zone };
        if (shouldStartBeMoved && operation === "MOVE") {
            newZone["left"] += offsetX;
            newZone["top"] += offsetY;
        }
        if (newZone["right"] !== undefined) {
            newZone["right"] += offsetX;
        }
        if (newZone["bottom"] !== undefined) {
            newZone["bottom"] += offsetY;
        }
        return newZone;
    }
    /**
     * Returns a Zone array with unique occurrence of each zone.
     * For each multiple occurrence, the occurrence with the largest index is kept.
     * This allows to always have the last selection made in the last position.
     * */
    function uniqueZones(zones) {
        return zones
            .reverse()
            .filter((zone, index, self) => index ===
            self.findIndex((z) => z.top === zone.top &&
                z.bottom === zone.bottom &&
                z.left === zone.left &&
                z.right === zone.right))
            .reverse();
    }
    /**
     * This function will find all overlapping zones in an array and transform them
     * into an union of each one.
     * */
    function mergeOverlappingZones(zones) {
        return zones.reduce((dissociatedZones, zone) => {
            const nextIndex = dissociatedZones.length;
            for (let i = 0; i < nextIndex; i++) {
                if (overlap(dissociatedZones[i], zone)) {
                    dissociatedZones[i] = union(dissociatedZones[i], zone);
                    return dissociatedZones;
                }
            }
            dissociatedZones[nextIndex] = zone;
            return dissociatedZones;
        }, []);
    }
    /**
     * This function will compare the modifications of selection to determine
     * a cell that is part of the new zone and not the previous one.
     */
    function findCellInNewZone(oldZone, currentZone) {
        let col, row;
        const { left: oldLeft, right: oldRight, top: oldTop, bottom: oldBottom } = oldZone;
        const { left, right, top, bottom } = currentZone;
        if (left != oldLeft) {
            col = left;
        }
        else if (right != oldRight) {
            col = right;
        }
        else {
            // left and right don't change
            col = left;
        }
        if (top != oldTop) {
            row = top;
        }
        else if (bottom != oldBottom) {
            row = bottom;
        }
        else {
            // top and bottom don't change
            row = top;
        }
        return { col, row };
    }
    function organizeZone(zone) {
        return {
            top: Math.min(zone.top, zone.bottom),
            bottom: Math.max(zone.top, zone.bottom),
            left: Math.min(zone.left, zone.right),
            right: Math.max(zone.left, zone.right),
        };
    }
    function positionToZone(position) {
        return { left: position.col, right: position.col, top: position.row, bottom: position.row };
    }
    function isFullRow(zone) {
        return zone.right === undefined;
    }
    function isFullCol(zone) {
        return zone.bottom === undefined;
    }
    /** Returns the area of a zone */
    function getZoneArea(zone) {
        return (zone.bottom - zone.top + 1) * (zone.right - zone.left + 1);
    }
    /**
     * Check if the zones are continuous, ie. if they can be merged into a single zone without
     * including cells outside the zones
     * */
    function areZonesContinuous(...zones) {
        if (zones.length < 2)
            return true;
        return recomputeZones(zones.map(zoneToXc), []).length === 1;
    }
    /** Return all the columns in the given list of zones */
    function getZonesCols(zones) {
        const set = new Set();
        for (let zone of zones) {
            for (let col of range(zone.left, zone.right + 1)) {
                set.add(col);
            }
        }
        return set;
    }
    /** Return all the rows in the given list of zones */
    function getZonesRows(zones) {
        const set = new Set();
        for (let zone of zones) {
            for (let row of range(zone.top, zone.bottom + 1)) {
                set.add(row);
            }
        }
        return set;
    }

    class RangeImpl {
        getSheetSize;
        _zone;
        parts;
        invalidXc;
        prefixSheet = false;
        sheetId; // the sheet on which the range is defined
        invalidSheetName; // the name of any sheet that is invalid
        constructor(args, getSheetSize) {
            this.getSheetSize = getSheetSize;
            this._zone = args.zone;
            this.prefixSheet = args.prefixSheet;
            this.invalidXc = args.invalidXc;
            this.sheetId = args.sheetId;
            this.invalidSheetName = args.invalidSheetName;
            let _fixedParts = [...args.parts];
            if (args.parts.length === 1 && getZoneArea(this.zone) > 1) {
                _fixedParts.push({ ...args.parts[0] });
            }
            else if (args.parts.length === 2 && getZoneArea(this.zone) === 1) {
                _fixedParts.pop();
            }
            this.parts = _fixedParts;
        }
        static fromRange(range, getters) {
            if (range instanceof RangeImpl) {
                return range;
            }
            return new RangeImpl(range, getters.getSheetSize);
        }
        get unboundedZone() {
            return this._zone;
        }
        get zone() {
            const { left, top, bottom, right } = this._zone;
            if (right !== undefined && bottom !== undefined) {
                return this._zone;
            }
            else if (bottom === undefined && right !== undefined) {
                return { right, top, left, bottom: this.getSheetSize(this.sheetId).numberOfRows - 1 };
            }
            else if (right === undefined && bottom !== undefined) {
                return { bottom, left, top, right: this.getSheetSize(this.sheetId).numberOfCols - 1 };
            }
            throw new Error(_t("Bad zone format"));
        }
        static getRangeParts(xc, zone) {
            const parts = xc.split(":").map((p) => {
                const isFullRow = isRowReference(p);
                return {
                    colFixed: isFullRow ? false : p.startsWith("$"),
                    rowFixed: isFullRow ? p.startsWith("$") : p.includes("$", 1),
                };
            });
            const isFullCol = zone.bottom === undefined;
            const isFullRow = zone.right === undefined;
            if (isFullCol) {
                parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
                parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
            }
            if (isFullRow) {
                parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
                parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
            }
            return parts;
        }
        get isFullCol() {
            return this._zone.bottom === undefined;
        }
        get isFullRow() {
            return this._zone.right === undefined;
        }
        get rangeData() {
            return {
                _zone: this._zone,
                _sheetId: this.sheetId,
            };
        }
        /**
         * Check that a zone is valid regarding the order of top-bottom and left-right.
         * Left should be smaller than right, top should be smaller than bottom.
         * If it's not the case, simply invert them, and invert the linked parts
         */
        orderZone() {
            if (isZoneOrdered(this._zone)) {
                return this;
            }
            const zone = { ...this._zone };
            let parts = this.parts;
            if (zone.right !== undefined && zone.right < zone.left) {
                let right = zone.right;
                zone.right = zone.left;
                zone.left = right;
                parts = [
                    {
                        colFixed: parts[1]?.colFixed || false,
                        rowFixed: parts[0]?.rowFixed || false,
                    },
                    {
                        colFixed: parts[0]?.colFixed || false,
                        rowFixed: parts[1]?.rowFixed || false,
                    },
                ];
            }
            if (zone.bottom !== undefined && zone.bottom < zone.top) {
                let bottom = zone.bottom;
                zone.bottom = zone.top;
                zone.top = bottom;
                parts = [
                    {
                        colFixed: parts[0]?.colFixed || false,
                        rowFixed: parts[1]?.rowFixed || false,
                    },
                    {
                        colFixed: parts[1]?.colFixed || false,
                        rowFixed: parts[0]?.rowFixed || false,
                    },
                ];
            }
            return this.clone({ zone, parts });
        }
        /**
         *
         * @param rangeParams optional, values to put in the cloned range instead of the current values of the range
         */
        clone(rangeParams) {
            return new RangeImpl({
                zone: rangeParams?.zone ? rangeParams.zone : { ...this._zone },
                sheetId: rangeParams?.sheetId ? rangeParams.sheetId : this.sheetId,
                invalidSheetName: rangeParams && "invalidSheetName" in rangeParams // 'attr in obj' instead of just 'obj.attr' because we accept undefined values
                    ? rangeParams.invalidSheetName
                    : this.invalidSheetName,
                invalidXc: rangeParams && "invalidXc" in rangeParams ? rangeParams.invalidXc : this.invalidXc,
                parts: rangeParams?.parts
                    ? rangeParams.parts
                    : this.parts.map((part) => {
                        return { rowFixed: part.rowFixed, colFixed: part.colFixed };
                    }),
                prefixSheet: rangeParams?.prefixSheet ? rangeParams.prefixSheet : this.prefixSheet,
            }, this.getSheetSize);
        }
    }
    /**
     * Copy a range. If the range is on the sheetIdFrom, the range will target
     * sheetIdTo.
     */
    function copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {
        const sheetId = range.sheetId === sheetIdFrom ? sheetIdTo : range.sheetId;
        return range.clone({ sheetId });
    }
    /**
     * Create a range from a xc. If the xc is empty, this function returns undefined.
     */
    function createRange(getters, sheetId, range) {
        return range ? getters.getRangeFromSheetXC(sheetId, range) : undefined;
    }
    /**
     * Spread multiple colrows zone to one row/col zone and add a many new input range as needed.
     * For example, A1:B4 will become [A1:A4, B1:B4]
     */
    function spreadRange(getters, ranges) {
        const postProcessedRanges = [];
        for (const range of ranges) {
            if (!getters.isRangeValid(range)) {
                postProcessedRanges.push(range); // ignore invalid range
                continue;
            }
            const { sheetName } = splitReference(range);
            const sheetPrefix = sheetName ? `${sheetName}!` : "";
            const zone = toUnboundedZone(range);
            if (zone.bottom !== zone.top && zone.left != zone.right) {
                if (zone.right) {
                    for (let j = zone.left; j <= zone.right; ++j) {
                        postProcessedRanges.push(`${sheetPrefix}${zoneToXc({
                        left: j,
                        right: j,
                        top: zone.top,
                        bottom: zone.bottom,
                    })}`);
                    }
                }
                else {
                    for (let j = zone.top; j <= zone.bottom; ++j) {
                        postProcessedRanges.push(`${sheetPrefix}${zoneToXc({
                        left: zone.left,
                        right: zone.right,
                        top: j,
                        bottom: j,
                    })}`);
                    }
                }
            }
            else {
                postProcessedRanges.push(range);
            }
        }
        return postProcessedRanges;
    }
    /**
     * Get all the cell positions in the given ranges. If a cell is in multiple ranges, it will be returned multiple times.
     */
    function getCellPositionsInRanges(ranges) {
        const cellPositions = [];
        for (const range of ranges) {
            for (const position of positions(range.zone)) {
                cellPositions.push({ ...position, sheetId: range.sheetId });
            }
        }
        return cellPositions;
    }

    /** Methods from Odoo Web Utils  */
    /**
     * This function computes a score that represent the fact that the
     * string contains the pattern, or not
     *
     * - If the score is 0, the string does not contain the letters of the pattern in
     *   the correct order.
     * - if the score is > 0, it actually contains the letters.
     *
     * Better matches will get a higher score: consecutive letters are better,
     * and a match closer to the beginning of the string is also scored higher.
     */
    function fuzzyMatch(pattern, str) {
        pattern = pattern.toLocaleLowerCase();
        str = str.toLocaleLowerCase();
        let totalScore = 0;
        let currentScore = 0;
        let len = str.length;
        let patternIndex = 0;
        for (let i = 0; i < len; i++) {
            if (str[i] === pattern[patternIndex]) {
                patternIndex++;
                currentScore += 100 + currentScore - i / 200;
            }
            else {
                currentScore = 0;
            }
            totalScore = totalScore + currentScore;
        }
        return patternIndex === pattern.length ? totalScore : 0;
    }
    /**
     * Return a list of things that matches a pattern, ordered by their 'score' (
     * higher score first). An higher score means that the match is better. For
     * example, consecutive letters are considered a better match.
     */
    function fuzzyLookup(pattern, list, fn) {
        const results = [];
        list.forEach((data) => {
            const score = fuzzyMatch(pattern, fn(data));
            if (score > 0) {
                results.push({ score, elem: data });
            }
        });
        // we want better matches first
        results.sort((a, b) => b.score - a.score);
        return results.map((r) => r.elem);
    }

    function createDefaultRows(rowNumber) {
        const rows = [];
        for (let i = 0; i < rowNumber; i++) {
            const row = {
                cells: {},
            };
            rows.push(row);
        }
        return rows;
    }
    function moveHeaderIndexesOnHeaderAddition(indexHeaderAdded, numberAdded, headers) {
        return headers.map((header) => {
            if (header >= indexHeaderAdded) {
                return header + numberAdded;
            }
            return header;
        });
    }
    function moveHeaderIndexesOnHeaderDeletion(deletedHeaders, headers) {
        deletedHeaders = [...deletedHeaders].sort((a, b) => b - a);
        return headers
            .map((header) => {
            for (const deletedHeader of deletedHeaders) {
                if (header > deletedHeader) {
                    header--;
                }
                else if (header === deletedHeader) {
                    return undefined;
                }
            }
            return header;
        })
            .filter(isDefined$1);
    }

    function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
        return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN;
    }
    /**
     * Get the default height of the cell given its style.
     */
    function getDefaultCellHeight(ctx, cell, colSize) {
        if (!cell || !cell.content)
            return DEFAULT_CELL_HEIGHT;
        const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
        const numberOfLines = cell.isFormula
            ? 1
            : splitTextToWidth(ctx, cell.content, cell.style, maxWidth).length;
        const fontSize = computeTextFontSizeInPixels(cell.style);
        return computeTextLinesHeight(fontSize, numberOfLines) + 2 * PADDING_AUTORESIZE_VERTICAL;
    }
    const textWidthCache = {};
    function computeTextWidth(context, text, style) {
        const font = computeTextFont(style);
        if (!textWidthCache[font]) {
            textWidthCache[font] = {};
        }
        if (textWidthCache[font][text] === undefined) {
            context.save();
            context.font = font;
            const textWidth = context.measureText(text).width;
            context.restore();
            textWidthCache[font][text] = textWidth;
        }
        return textWidthCache[font][text];
    }
    function fontSizeInPixels(fontSize) {
        return Math.round((fontSize * 96) / 72);
    }
    function computeTextFont(style) {
        const italic = style.italic ? "italic " : "";
        const weight = style.bold ? "bold" : DEFAULT_FONT_WEIGHT;
        const size = computeTextFontSizeInPixels(style);
        return `${italic}${weight} ${size}px ${DEFAULT_FONT}`;
    }
    function computeTextFontSizeInPixels(style) {
        const sizeInPt = style?.fontSize || DEFAULT_FONT_SIZE;
        return fontSizeInPixels(sizeInPt);
    }
    function splitWordToSpecificWidth(ctx, word, width, style) {
        const wordWidth = computeTextWidth(ctx, word, style);
        if (wordWidth <= width) {
            return [word];
        }
        const splitWord = [];
        let wordPart = "";
        for (let l of word) {
            const wordPartWidth = computeTextWidth(ctx, wordPart + l, style);
            if (wordPartWidth > width) {
                splitWord.push(wordPart);
                wordPart = l;
            }
            else {
                wordPart += l;
            }
        }
        splitWord.push(wordPart);
        return splitWord;
    }
    /**
     * Return the given text, split in multiple lines if needed. The text will be split in multiple
     * line if it contains NEWLINE characters, or if it's longer than the given width.
     */
    function splitTextToWidth(ctx, text, style, width) {
        if (!style)
            style = {};
        const brokenText = [];
        // Checking if text contains NEWLINE before split makes it very slightly slower if text contains it,
        // but 5-10x faster if it doesn't
        const lines = text.includes(NEWLINE) ? text.split(NEWLINE) : [text];
        for (const line of lines) {
            const words = line.includes(" ") ? line.split(" ") : [line];
            if (!width) {
                brokenText.push(line);
                continue;
            }
            let textLine = "";
            let availableWidth = width;
            for (let word of words) {
                const splitWord = splitWordToSpecificWidth(ctx, word, width, style);
                const lastPart = splitWord.pop();
                const lastPartWidth = computeTextWidth(ctx, lastPart, style);
                // At this step: "splitWord" is an array composed of parts of word whose
                // length is at most equal to "width".
                // Last part contains the end of the word.
                // Note that: When word length is less than width, then lastPart is equal
                // to word and splitWord is empty
                if (splitWord.length) {
                    if (textLine !== "") {
                        brokenText.push(textLine);
                        textLine = "";
                        availableWidth = width;
                    }
                    splitWord.forEach((wordPart) => {
                        brokenText.push(wordPart);
                    });
                    textLine = lastPart;
                    availableWidth = width - lastPartWidth;
                }
                else {
                    // here "lastPart" is equal to "word" and the "word" size is smaller than "width"
                    const _word = textLine === "" ? lastPart : " " + lastPart;
                    const wordWidth = computeTextWidth(ctx, _word, style);
                    if (wordWidth <= availableWidth) {
                        textLine += _word;
                        availableWidth -= wordWidth;
                    }
                    else {
                        brokenText.push(textLine);
                        textLine = lastPart;
                        availableWidth = width - lastPartWidth;
                    }
                }
            }
            if (textLine !== "") {
                brokenText.push(textLine);
            }
        }
        return brokenText;
    }
    /**
     * Return the font size that makes the width of a text match the given line width.
     * Minimum font size is 1.
     *
     * @param getTextWidth function that takes a fontSize as argument, and return the width of the text with this font size.
     */
    function getFontSizeMatchingWidth(lineWidth, maxFontSize, getTextWidth, precision = 0.25) {
        let minFontSize = 1;
        if (getTextWidth(minFontSize) > lineWidth)
            return minFontSize;
        if (getTextWidth(maxFontSize) < lineWidth)
            return maxFontSize;
        // Dichotomic search
        let fontSize = (minFontSize + maxFontSize) / 2;
        let currentTextWidth = getTextWidth(fontSize);
        // Use a maximum number of iterations to be safe, because measuring text isn't 100% precise
        let iterations = 0;
        while (Math.abs(currentTextWidth - lineWidth) > precision && iterations < 20) {
            if (currentTextWidth >= lineWidth) {
                maxFontSize = (minFontSize + maxFontSize) / 2;
            }
            else {
                minFontSize = (minFontSize + maxFontSize) / 2;
            }
            fontSize = (minFontSize + maxFontSize) / 2;
            currentTextWidth = getTextWidth(fontSize);
            iterations++;
        }
        return fontSize;
    }
    function computeIconWidth(style) {
        return computeTextFontSizeInPixels(style) + 2 * MIN_CF_ICON_MARGIN;
    }
    /** Transform a string to lower case. If the string is undefined, return an empty string */
    function toLowerCase(str) {
        return str ? str.toLowerCase() : "";
    }
    /**
     * Extract the fontSize from a context font string
     * @param font The (context) font string to parse
     * @returns The fontSize in pixels
     */
    const pxRegex = /([0-9\.]*)px/;
    function getContextFontSize(font) {
        return Number(font.match(pxRegex)?.[1]);
    }
    function drawDecoratedText(context, text, position, underline = false, strikethrough = false, strokeWidth = getContextFontSize(context.font) / 10 //This value is defined to get a good looking stroke
    ) {
        context.fillText(text, position.x, position.y);
        if (!underline && !strikethrough) {
            return;
        }
        const measure = context.measureText(text);
        const textWidth = measure.width;
        const textHeight = measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent;
        const boxHeight = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
        let { x, y } = position;
        let strikeY = y, underlineY = y;
        switch (context.textAlign) {
            case "center":
                x -= textWidth / 2;
                break;
            case "right":
                x -= textWidth;
                break;
        }
        switch (context.textBaseline) {
            case "top":
                underlineY += boxHeight - 2 * strokeWidth;
                strikeY += boxHeight / 2 - strokeWidth;
                break;
            case "middle":
                underlineY += boxHeight / 2 - strokeWidth;
                break;
            case "alphabetic":
                underlineY += 2 * strokeWidth;
                strikeY -= 3 * strokeWidth;
                break;
            case "bottom":
                underlineY = y;
                strikeY -= textHeight / 2 - strokeWidth / 2;
                break;
        }
        if (underline) {
            context.lineWidth = strokeWidth;
            context.strokeStyle = context.fillStyle;
            context.beginPath();
            context.moveTo(x, underlineY);
            context.lineTo(x + textWidth, underlineY);
            context.stroke();
        }
        if (strikethrough) {
            context.lineWidth = strokeWidth;
            context.strokeStyle = context.fillStyle;
            context.beginPath();
            context.moveTo(x, strikeY);
            context.lineTo(x + textWidth, strikeY);
            context.stroke();
        }
    }

    /*
     * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
     * */
    class UuidGenerator {
        isFastIdStrategy = false;
        fastIdStart = 0;
        setIsFastStrategy(isFast) {
            this.isFastIdStrategy = isFast;
        }
        uuidv4() {
            if (this.isFastIdStrategy) {
                this.fastIdStart++;
                return String(this.fastIdStart);
                //@ts-ignore
            }
            else if (window.crypto && window.crypto.getRandomValues) {
                //@ts-ignore
                return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
            }
            else {
                // mainly for jest and other browsers that do not have the crypto functionality
                return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
                    var r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
                    return v.toString(16);
                });
            }
        }
    }

    function createActions(menuItems) {
        return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
    }
    const uuidGenerator$2 = new UuidGenerator();
    function createAction(item) {
        const name = item.name;
        const children = item.children;
        const description = item.description;
        const icon = item.icon;
        return {
            id: item.id || uuidGenerator$2.uuidv4(),
            name: typeof name === "function" ? name : () => name,
            isVisible: item.isVisible ? item.isVisible : () => true,
            isEnabled: item.isEnabled ? item.isEnabled : () => true,
            isActive: item.isActive,
            execute: item.execute,
            children: children
                ? (env) => {
                    return children
                        .map((child) => (typeof child === "function" ? child(env) : child))
                        .flat()
                        .map(createAction);
                }
                : () => [],
            isReadonlyAllowed: item.isReadonlyAllowed || false,
            separator: item.separator || false,
            icon: typeof icon === "function" ? icon : () => icon || "",
            description: typeof description === "function" ? description : () => description || "",
            textColor: item.textColor,
            sequence: item.sequence || 0,
        };
    }

    function transformZone(zone, executed) {
        if (executed.type === "REMOVE_COLUMNS_ROWS") {
            return reduceZoneOnDeletion(zone, executed.dimension === "COL" ? "left" : "top", executed.elements);
        }
        if (executed.type === "ADD_COLUMNS_ROWS") {
            return expandZoneOnInsertion(zone, executed.dimension === "COL" ? "left" : "top", executed.base, executed.position, executed.quantity);
        }
        return { ...zone };
    }
    function transformRangeData(range, executed) {
        const deletedSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
        if ("sheetId" in executed && range._sheetId !== executed.sheetId) {
            return range;
        }
        else {
            const newZone = transformZone(range._zone, executed);
            if (newZone && deletedSheet !== range._sheetId) {
                return { ...range, _zone: newZone };
            }
        }
        return undefined;
    }

    class ChartJsComponent extends owl.Component {
        static template = "o-spreadsheet-ChartJsComponent";
        canvas = owl.useRef("graphContainer");
        chart;
        get background() {
            return this.chartRuntime.background;
        }
        get canvasStyle() {
            return `background-color: ${this.background}`;
        }
        get chartRuntime() {
            const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
            if (!("chartJsConfig" in runtime)) {
                throw new Error("Unsupported chart runtime");
            }
            return runtime;
        }
        setup() {
            owl.onMounted(() => {
                const runtime = this.chartRuntime;
                this.createChart(runtime.chartJsConfig);
            });
            owl.useEffect(() => this.updateChartJs(this.chartRuntime), () => [this.chartRuntime]);
        }
        createChart(chartData) {
            const canvas = this.canvas.el;
            const ctx = canvas.getContext("2d");
            // @ts-ignore
            this.chart = new window.Chart(ctx, chartData);
        }
        updateChartJs(chartRuntime) {
            const chartData = chartRuntime.chartJsConfig;
            if (chartData.data && chartData.data.datasets) {
                this.chart.data = chartData.data;
                if (chartData.options?.plugins?.title) {
                    this.chart.config.options.plugins.title = chartData.options.plugins.title;
                }
                if (chartData.options && "valueLabel" in chartData.options) {
                    if (chartData.options?.valueLabel) {
                        this.chart.config.options.valueLabel =
                            chartData.options.valueLabel;
                    }
                }
            }
            else {
                this.chart.data.datasets = [];
            }
            this.chart.config.options.plugins.tooltip = chartData.options.plugins.tooltip;
            this.chart.config.options.plugins.legend = chartData.options.plugins.legend;
            this.chart.config.options.scales = chartData.options?.scales;
            // ?
            this.chart.update("active");
        }
    }
    ChartJsComponent.props = {
        figure: Object,
    };

    /**
     * AbstractChart is the class from which every Chart should inherit.
     * The role of this class is to maintain the state of each chart.
     */
    class AbstractChart {
        sheetId;
        title;
        getters;
        constructor(definition, sheetId, getters) {
            this.title = definition.title;
            this.sheetId = sheetId;
            this.getters = getters;
        }
        /**
         * Validate the chart definition given as arguments. This function will be
         * called from allowDispatch function
         */
        static validateChartDefinition(validator, definition) {
            throw new Error("This method should be implemented by sub class");
        }
        /**
         * Get a new chart definition transformed with the executed command. This
         * functions will be called during operational transform process
         */
        static transformDefinition(definition, executed) {
            throw new Error("This method should be implemented by sub class");
        }
        /**
         * Get an empty definition based on the given context
         */
        static getDefinitionFromContextCreation(context) {
            throw new Error("This method should be implemented by sub class");
        }
    }

    /**
     * This file contains helpers that are common to different charts (mainly
     * line, bar and pie charts)
     */
    /**
     * Adapt ranges of a chart which support DataSet (dataSets and LabelRange).
     */
    function updateChartRangesWithDataSets(getters, applyChange, chartDataSets, chartLabelRange) {
        let isStale = false;
        const dataSetsWithUndefined = [];
        for (let index in chartDataSets) {
            let ds = chartDataSets[index];
            if (ds.labelCell) {
                const labelCell = adaptChartRange(ds.labelCell, applyChange);
                if (ds.labelCell !== labelCell) {
                    isStale = true;
                    ds = {
                        ...ds,
                        labelCell: labelCell,
                    };
                }
            }
            const dataRange = adaptChartRange(ds.dataRange, applyChange);
            if (dataRange === undefined ||
                getters.getRangeString(dataRange, dataRange.sheetId) === INCORRECT_RANGE_STRING) {
                isStale = true;
                ds = undefined;
            }
            else if (dataRange !== ds.dataRange) {
                isStale = true;
                ds = {
                    ...ds,
                    dataRange,
                };
            }
            dataSetsWithUndefined[index] = ds;
        }
        let labelRange = chartLabelRange;
        const range = adaptChartRange(labelRange, applyChange);
        if (range !== labelRange) {
            isStale = true;
            labelRange = range;
        }
        const dataSets = dataSetsWithUndefined.filter(isDefined$1);
        return {
            isStale,
            dataSets,
            labelRange,
        };
    }
    /**
     * Copy the dataSets given. All the ranges which are on sheetIdFrom will target
     * sheetIdTo.
     */
    function copyDataSetsWithNewSheetId(sheetIdFrom, sheetIdTo, dataSets) {
        return dataSets.map((ds) => {
            return {
                dataRange: copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.dataRange),
                labelCell: ds.labelCell
                    ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, ds.labelCell)
                    : undefined,
            };
        });
    }
    /**
     * Copy a range. If the range is on the sheetIdFrom, the range will target
     * sheetIdTo.
     */
    function copyLabelRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) {
        return range ? copyRangeWithNewSheetId(sheetIdFrom, sheetIdTo, range) : undefined;
    }
    /**
     * Adapt a single range of a chart
     */
    function adaptChartRange(range, applyChange) {
        if (!range) {
            return undefined;
        }
        const change = applyChange(range);
        switch (change.changeType) {
            case "NONE":
                return range;
            case "REMOVE":
                return undefined;
            default:
                return change.range;
        }
    }
    /**
     * Create the dataSet objects from xcs
     */
    function createDataSets(getters, dataSetsString, sheetId, dataSetsHaveTitle) {
        const dataSets = [];
        for (const sheetXC of dataSetsString) {
            const dataRange = getters.getRangeFromSheetXC(sheetId, sheetXC);
            const { unboundedZone: zone, sheetId: dataSetSheetId, invalidSheetName, invalidXc } = dataRange;
            if (invalidSheetName || invalidXc) {
                continue;
            }
            // It's a rectangle. We treat all columns (arbitrary) as different data series.
            if (zone.left !== zone.right && zone.top !== zone.bottom) {
                if (zone.right === undefined) {
                    // Should never happens because of the allowDispatch of charts, but just making sure
                    continue;
                }
                for (let column = zone.left; column <= zone.right; column++) {
                    const columnZone = {
                        ...zone,
                        left: column,
                        right: column,
                    };
                    dataSets.push(createDataSet(getters, dataSetSheetId, columnZone, dataSetsHaveTitle
                        ? {
                            top: columnZone.top,
                            bottom: columnZone.top,
                            left: columnZone.left,
                            right: columnZone.left,
                        }
                        : undefined));
                }
            }
            else {
                /* 1 cell, 1 row or 1 column */
                dataSets.push(createDataSet(getters, dataSetSheetId, zone, dataSetsHaveTitle
                    ? {
                        top: zone.top,
                        bottom: zone.top,
                        left: zone.left,
                        right: zone.left,
                    }
                    : undefined));
            }
        }
        return dataSets;
    }
    function createDataSet(getters, sheetId, fullZone, titleZone) {
        if (fullZone.left !== fullZone.right && fullZone.top !== fullZone.bottom) {
            throw new Error(`Zone should be a single column or row: ${zoneToXc(fullZone)}`);
        }
        if (titleZone) {
            const dataXC = zoneToXc(fullZone);
            const labelCellXC = zoneToXc(titleZone);
            return {
                labelCell: getters.getRangeFromSheetXC(sheetId, labelCellXC),
                dataRange: getters.getRangeFromSheetXC(sheetId, dataXC),
            };
        }
        else {
            return {
                labelCell: undefined,
                dataRange: getters.getRangeFromSheetXC(sheetId, zoneToXc(fullZone)),
            };
        }
    }
    /**
     * Transform a dataSet to a ExcelDataSet
     */
    function toExcelDataset(getters, ds) {
        const labelZone = ds.labelCell?.zone;
        let dataZone = ds.dataRange.zone;
        if (labelZone) {
            const { numberOfRows, numberOfCols } = zoneToDimension(dataZone);
            if (numberOfRows === 1) {
                dataZone = { ...dataZone, left: dataZone.left + 1 };
            }
            else if (numberOfCols === 1) {
                dataZone = { ...dataZone, top: dataZone.top + 1 };
            }
        }
        const dataRange = ds.dataRange.clone({ zone: dataZone });
        return {
            label: ds.labelCell
                ? getters.getRangeString(ds.labelCell, "forceSheetReference", { useFixedReference: true })
                : undefined,
            range: getters.getRangeString(dataRange, "forceSheetReference", { useFixedReference: true }),
        };
    }
    function toExcelLabelRange(getters, labelRange, shouldRemoveFirstLabel) {
        if (!labelRange)
            return undefined;
        let zone = {
            ...labelRange.zone,
        };
        if (shouldRemoveFirstLabel && labelRange.zone.bottom > labelRange.zone.top) {
            zone.top = zone.top + 1;
        }
        const range = labelRange.clone({ zone });
        return getters.getRangeString(range, "forceSheetReference", { useFixedReference: true });
    }
    /**
     * Transform a chart definition which supports dataSets (dataSets and LabelRange)
     * with an executed command
     */
    function transformChartDefinitionWithDataSetsWithZone(definition, executed) {
        let labelRange;
        if (definition.labelRange) {
            const labelZone = transformZone(toUnboundedZone(definition.labelRange), executed);
            labelRange = labelZone ? zoneToXc(labelZone) : undefined;
        }
        const dataSets = definition.dataSets
            .map(toUnboundedZone)
            .map((zone) => transformZone(zone, executed))
            .filter(isDefined$1)
            .map(zoneToXc);
        return {
            ...definition,
            labelRange,
            dataSets,
        };
    }
    const GraphColors = [
        // the same colors as those used in odoo reporting
        "rgb(31,119,180)",
        "rgb(255,127,14)",
        "rgb(174,199,232)",
        "rgb(255,187,120)",
        "rgb(44,160,44)",
        "rgb(152,223,138)",
        "rgb(214,39,40)",
        "rgb(255,152,150)",
        "rgb(148,103,189)",
        "rgb(197,176,213)",
        "rgb(140,86,75)",
        "rgb(196,156,148)",
        "rgb(227,119,194)",
        "rgb(247,182,210)",
        "rgb(127,127,127)",
        "rgb(199,199,199)",
        "rgb(188,189,34)",
        "rgb(219,219,141)",
        "rgb(23,190,207)",
        "rgb(158,218,229)",
    ];
    class ChartColors {
        graphColorIndex = 0;
        next() {
            return GraphColors[this.graphColorIndex++ % GraphColors.length];
        }
    }
    /**
     * Choose a font color based on a background color.
     * The font is white with a dark background.
     */
    function chartFontColor(backgroundColor) {
        if (!backgroundColor) {
            return "#000000";
        }
        return relativeLuminance(backgroundColor) < 0.3 ? "#FFFFFF" : "#000000";
    }
    function checkDataset(definition) {
        if (definition.dataSets) {
            const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range)) !== undefined;
            if (invalidRanges) {
                return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
            }
            const zones = definition.dataSets.map(toUnboundedZone);
            if (zones.some((zone) => zone.top !== zone.bottom && isFullRow(zone))) {
                return "InvalidDataSet" /* CommandResult.InvalidDataSet */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    function checkLabelRange(definition) {
        if (definition.labelRange) {
            const invalidLabels = !rangeReference.test(definition.labelRange || "");
            if (invalidLabels) {
                return "InvalidLabelRange" /* CommandResult.InvalidLabelRange */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
        if (!dataSetsHaveTitle)
            return false;
        if (!labelRange)
            return false;
        if (!dataset)
            return true;
        const datasetLength = getZoneArea(dataset.dataRange.zone);
        const labelLength = getZoneArea(labelRange.zone);
        if (labelLength < datasetLength) {
            return false;
        }
        return true;
    }
    // ---------------------------------------------------------------------------
    // Scorecard
    // ---------------------------------------------------------------------------
    function getBaselineText(baseline, keyValue, baselineMode, locale) {
        if (!baseline) {
            return "";
        }
        else if (baselineMode === "text" ||
            keyValue?.type !== CellValueType.number ||
            baseline.type !== CellValueType.number) {
            return baseline.formattedValue;
        }
        else {
            let diff = keyValue.value - baseline.value;
            if (baselineMode === "percentage" && diff !== 0) {
                diff = (diff / baseline.value) * 100;
            }
            if (baselineMode !== "percentage" && baseline.format) {
                return formatValue(diff, { format: baseline.format, locale });
            }
            const baselineStr = Math.abs(parseFloat(diff.toFixed(2))).toLocaleString();
            return baselineMode === "percentage" ? baselineStr + "%" : baselineStr;
        }
    }
    function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
        if (baselineMode === "text" ||
            baseline?.type !== CellValueType.number ||
            keyValue?.type !== CellValueType.number) {
            return undefined;
        }
        const diff = keyValue.value - baseline.value;
        if (diff > 0) {
            return colorUp;
        }
        else if (diff < 0) {
            return colorDown;
        }
        return undefined;
    }
    function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
        if (baselineMode === "text" ||
            baseline?.type !== CellValueType.number ||
            keyValue?.type !== CellValueType.number) {
            return "neutral";
        }
        const diff = keyValue.value - baseline.value;
        if (diff > 0) {
            return "up";
        }
        else if (diff < 0) {
            return "down";
        }
        return "neutral";
    }
    function getChartPositionAtCenterOfViewport(getters, chartSize) {
        const { x, y } = getters.getMainViewportCoordinates();
        const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
        const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
        const position = {
            x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
            y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
        }; // Position at the center of the scrollable viewport
        return position;
    }

    function checkKeyValue(definition) {
        return definition.keyValue && !rangeReference.test(definition.keyValue)
            ? "InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */
            : "Success" /* CommandResult.Success */;
    }
    function checkBaseline(definition) {
        return definition.baseline && !rangeReference.test(definition.baseline)
            ? "InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */
            : "Success" /* CommandResult.Success */;
    }
    const arrowDownPath = new window.Path2D("M8.6 4.8a.5.5 0 0 1 0 .75l-3.9 3.9a.5 .5 0 0 1 -.75 0l-3.8 -3.9a.5 .5 0 0 1 0 -.75l.4-.4a.5.5 0 0 1 .75 0l2.3 2.4v-5.7c0-.25.25-.5.5-.5h.6c.25 0 .5.25.5.5v5.8l2.3 -2.4a.5.5 0 0 1 .75 0z");
    const arrowUpPath = new window.Path2D("M8.7 5.5a.5.5 0 0 0 0-.75l-3.8-4a.5.5 0 0 0-.75 0l-3.8 4a.5.5 0 0 0 0 .75l.4.4a.5.5 0 0 0 .75 0l2.3-2.4v5.8c0 .25.25.5.5.5h.6c.25 0 .5-.25.5-.5v-5.8l2.2 2.4a.5.5 0 0 0 .75 0z");
    let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
        keyValue;
        baseline;
        baselineMode;
        baselineDescr;
        background;
        baselineColorUp;
        baselineColorDown;
        fontColor;
        type = "scorecard";
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.keyValue = createRange(getters, sheetId, definition.keyValue);
            this.baseline = createRange(getters, sheetId, definition.baseline);
            this.baselineMode = definition.baselineMode;
            this.baselineDescr = definition.baselineDescr;
            this.background = definition.background;
            this.baselineColorUp = definition.baselineColorUp;
            this.baselineColorDown = definition.baselineColorDown;
        }
        static validateChartDefinition(validator, definition) {
            return validator.checkValidations(definition, checkKeyValue, checkBaseline);
        }
        static getDefinitionFromContextCreation(context) {
            return {
                background: context.background,
                type: "scorecard",
                keyValue: context.range ? context.range[0] : undefined,
                title: context.title || "",
                baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
                baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
                baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
                baseline: context.auxiliaryRange || "",
            };
        }
        static transformDefinition(definition, executed) {
            let baselineZone;
            let keyValueZone;
            if (definition.baseline) {
                baselineZone = transformZone(toUnboundedZone(definition.baseline), executed);
            }
            if (definition.keyValue) {
                keyValueZone = transformZone(toUnboundedZone(definition.keyValue), executed);
            }
            return {
                ...definition,
                baseline: baselineZone ? zoneToXc(baselineZone) : undefined,
                keyValue: keyValueZone ? zoneToXc(keyValueZone) : undefined,
            };
        }
        copyForSheetId(sheetId) {
            const baseline = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.baseline);
            const keyValue = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.keyValue);
            const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue, sheetId);
            return new ScorecardChart(definition, sheetId, this.getters);
        }
        copyInSheetId(sheetId) {
            const definition = this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue, sheetId);
            return new ScorecardChart(definition, sheetId, this.getters);
        }
        getDefinition() {
            return this.getDefinitionWithSpecificRanges(this.baseline, this.keyValue);
        }
        getContextCreation() {
            return {
                background: this.background,
                title: this.title,
                range: this.keyValue ? [this.getters.getRangeString(this.keyValue, this.sheetId)] : undefined,
                auxiliaryRange: this.baseline
                    ? this.getters.getRangeString(this.baseline, this.sheetId)
                    : undefined,
            };
        }
        getDefinitionWithSpecificRanges(baseline, keyValue, targetSheetId) {
            return {
                baselineColorDown: this.baselineColorDown,
                baselineColorUp: this.baselineColorUp,
                baselineMode: this.baselineMode,
                title: this.title,
                type: "scorecard",
                background: this.background,
                baseline: baseline
                    ? this.getters.getRangeString(baseline, targetSheetId || this.sheetId)
                    : undefined,
                baselineDescr: this.baselineDescr,
                keyValue: keyValue
                    ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
                    : undefined,
            };
        }
        getDefinitionForExcel() {
            // This kind of graph is not exportable in Excel
            return undefined;
        }
        updateRanges(applyChange) {
            const baseline = adaptChartRange(this.baseline, applyChange);
            const keyValue = adaptChartRange(this.keyValue, applyChange);
            if (this.baseline === baseline && this.keyValue === keyValue) {
                return this;
            }
            const definition = this.getDefinitionWithSpecificRanges(baseline, keyValue);
            return new ScorecardChart(definition, this.sheetId, this.getters);
        }
    };
    function drawScoreChart(structure, canvas) {
        const ctx = canvas.getContext("2d");
        canvas.width = structure.canvas.width;
        canvas.height = structure.canvas.height;
        ctx.fillStyle = structure.canvas.backgroundColor;
        ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);
        if (structure.title) {
            ctx.font = structure.title.style.font;
            ctx.fillStyle = structure.title.style.color;
            ctx.fillText(structure.title.text, structure.title.position.x, structure.title.position.y);
        }
        if (structure.baseline) {
            ctx.font = structure.baseline.style.font;
            ctx.fillStyle = structure.baseline.style.color;
            drawDecoratedText(ctx, structure.baseline.text, structure.baseline.position, structure.baseline.style.underline, structure.baseline.style.strikethrough);
        }
        if (structure.baselineArrow && structure.baselineArrow.style.size > 0) {
            ctx.save();
            ctx.fillStyle = structure.baselineArrow.style.color;
            ctx.translate(structure.baselineArrow.position.x, structure.baselineArrow.position.y);
            // This ratio is computed according to the original svg size and the final size we want
            const ratio = structure.baselineArrow.style.size / 10;
            ctx.scale(ratio, ratio);
            switch (structure.baselineArrow.direction) {
                case "down": {
                    ctx.fill(arrowDownPath);
                    break;
                }
                case "up": {
                    ctx.fill(arrowUpPath);
                    break;
                }
            }
            ctx.restore();
        }
        if (structure.baselineDescr) {
            ctx.font = structure.baselineDescr.style.font;
            ctx.fillStyle = structure.baselineDescr.style.color;
            ctx.fillText(structure.baselineDescr.text, structure.baselineDescr.position.x, structure.baselineDescr.position.y);
        }
        if (structure.key) {
            ctx.font = structure.key.style.font;
            ctx.fillStyle = structure.key.style.color;
            drawDecoratedText(ctx, structure.key.text, structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
        }
    }
    function createScorecardChartRuntime(chart, getters) {
        let keyValue = "";
        let formattedKeyValue = "";
        let keyValueCell;
        if (chart.keyValue) {
            const keyValuePosition = {
                sheetId: chart.keyValue.sheetId,
                col: chart.keyValue.zone.left,
                row: chart.keyValue.zone.top,
            };
            keyValueCell = getters.getEvaluatedCell(keyValuePosition);
            keyValue = String(keyValueCell.value);
            formattedKeyValue = keyValueCell.formattedValue;
        }
        let baselineCell;
        const baseline = chart.baseline;
        if (baseline) {
            const baselinePosition = {
                sheetId: chart.baseline.sheetId,
                col: chart.baseline.zone.left,
                row: chart.baseline.zone.top,
            };
            baselineCell = getters.getEvaluatedCell(baselinePosition);
        }
        const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
        const locale = getters.getLocale();
        return {
            title: _t(chart.title),
            keyValue: formattedKeyValue || keyValue,
            baselineDisplay: getBaselineText(baselineCell, keyValueCell, chart.baselineMode, locale),
            baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
            baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
            baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
            fontColor,
            background,
            baselineStyle: chart.baselineMode !== "percentage" && baseline
                ? getters.getCellStyle({
                    sheetId: baseline.sheetId,
                    col: baseline.zone.left,
                    row: baseline.zone.top,
                })
                : undefined,
            keyValueStyle: chart.keyValue
                ? getters.getCellStyle({
                    sheetId: chart.keyValue.sheetId,
                    col: chart.keyValue.zone.left,
                    row: chart.keyValue.zone.top,
                })
                : undefined,
        };
    }

    /* Sizes of boxes containing the texts, in percentage of the Chart size */
    const TITLE_FONT_SIZE = 18;
    const BASELINE_BOX_HEIGHT_RATIO = 0.35;
    const KEY_BOX_HEIGHT_RATIO = 0.65;
    /** Baseline description should have a smaller font than the baseline */
    const BASELINE_DESCR_FONT_RATIO = 0.9;
    /* Padding at the border of the chart, in percentage of the chart width */
    const CHART_PADDING_RATIO = 0.02;
    /**
     * Line height (in em)
     * Having a line heigh =1em (=font size) don't work, the font will overflow.
     */
    const LINE_HEIGHT = 1.2;
    function formatBaselineDescr(baselineDescr, baseline) {
        const _baselineDescr = baselineDescr || "";
        return baseline && _baselineDescr ? " " + _baselineDescr : _baselineDescr;
    }
    function getDefaultContextFont(fontSize, bold = false, italic = false) {
        const italicStr = italic ? "italic" : "";
        const weight = bold ? "bold" : "";
        return `${italicStr} ${weight} ${fontSize}px ${DEFAULT_FONT}`;
    }
    function getScorecardConfiguration({ width, height }, runtime) {
        const designer = new ScorecardChartConfigBuilder({ width, height }, runtime);
        return designer.computeDesign();
    }
    class ScorecardChartConfigBuilder {
        runtime;
        context;
        width;
        height;
        constructor({ width, height }, runtime) {
            this.runtime = runtime;
            const canvas = document.createElement("canvas");
            this.width = canvas.width = width;
            this.height = canvas.height = height;
            this.context = canvas.getContext("2d");
        }
        computeDesign() {
            const structure = {
                canvas: {
                    width: this.width,
                    height: this.height,
                    backgroundColor: this.backgroundColor,
                },
            };
            const style = this.getTextStyles();
            const { height: titleHeight } = this.getTextDimensions(this.title, style.title.font);
            if (this.title) {
                structure.title = {
                    text: this.title,
                    style: style.title,
                    position: {
                        x: this.chartPadding,
                        y: this.chartPadding + titleHeight,
                    },
                };
            }
            const baselineArrowSize = style.baselineArrow?.size ?? 0;
            const { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
            const { width: baselineDescrWidth } = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font);
            structure.baseline = {
                text: this.baseline,
                style: style.baselineValue,
                position: {
                    x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2,
                    y: this.keyValue
                        ? this.height - 2 * this.chartPadding
                        : this.height - (this.height - titleHeight - baselineHeight) / 2 - this.chartPadding,
                },
            };
            if (style.baselineArrow) {
                structure.baselineArrow = {
                    direction: this.baselineArrow,
                    style: style.baselineArrow,
                    position: {
                        x: structure.baseline.position.x - baselineArrowSize,
                        y: structure.baseline.position.y - (baselineHeight + baselineArrowSize) / 2,
                    },
                };
            }
            if (this.baselineDescr) {
                structure.baselineDescr = {
                    text: this.baselineDescr,
                    style: style.baselineDescr,
                    position: {
                        x: structure.baseline.position.x + baselineWidth,
                        y: structure.baseline.position.y,
                    },
                };
            }
            const { height: keyHeight, width: keyWidth } = this.getTextDimensions(this.keyValue, style.keyValue.font);
            if (this.keyValue) {
                structure.key = {
                    text: this.keyValue,
                    style: style.keyValue,
                    position: {
                        x: (this.width - keyWidth) / 2,
                        y: (this.height - baselineHeight + titleHeight + keyHeight) / 2 - this.chartPadding,
                    },
                };
            }
            return structure;
        }
        get title() {
            return this.runtime.title;
        }
        get keyValue() {
            return this.runtime.keyValue;
        }
        get baseline() {
            return this.runtime.baselineDisplay;
        }
        get baselineDescr() {
            return formatBaselineDescr(this.runtime.baselineDescr, this.baseline);
        }
        get baselineArrow() {
            return this.runtime.baselineArrow;
        }
        get backgroundColor() {
            return this.runtime.background;
        }
        get secondaryFontColor() {
            return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
        }
        get chartPadding() {
            return this.width * CHART_PADDING_RATIO;
        }
        getTextDimensions(text, font) {
            this.context.font = font;
            const measure = this.context.measureText(text);
            return {
                width: measure.width,
                height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,
            };
        }
        getTextStyles() {
            // If the widest text overflows horizontally, scale it down, and apply the same scaling factors to all the other fonts.
            const maxLineWidth = this.width * (1 - 2 * CHART_PADDING_RATIO);
            const widestElement = this.getWidestElement();
            const baseFontSize = widestElement.getElementMaxFontSize(this.getDrawableHeight(), this);
            const fontSizeMatchingWidth = getFontSizeMatchingWidth(maxLineWidth, baseFontSize, (fontSize) => widestElement.getElementWidth(fontSize, this.context, this));
            let scalingFactor = fontSizeMatchingWidth / baseFontSize;
            // Fonts sizes in px
            const keyFontSize = new KeyValueElement(this.runtime.keyValueStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
            const baselineFontSize = new BaselineElement(this.runtime.baselineStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
            return {
                title: {
                    font: getDefaultContextFont(TITLE_FONT_SIZE),
                    color: this.secondaryFontColor,
                },
                keyValue: {
                    color: this.runtime.keyValueStyle?.textColor || this.runtime.fontColor,
                    font: getDefaultContextFont(keyFontSize, this.runtime.keyValueStyle?.bold, this.runtime.keyValueStyle?.italic),
                    strikethrough: this.runtime.keyValueStyle?.strikethrough,
                    underline: this.runtime.keyValueStyle?.underline,
                },
                baselineValue: {
                    font: getDefaultContextFont(baselineFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
                    strikethrough: this.runtime.baselineStyle?.strikethrough,
                    underline: this.runtime.baselineStyle?.underline,
                    color: this.runtime.baselineStyle?.textColor ||
                        this.runtime.baselineColor ||
                        this.secondaryFontColor,
                },
                baselineDescr: {
                    font: getDefaultContextFont(baselineFontSize * BASELINE_DESCR_FONT_RATIO),
                    color: this.secondaryFontColor,
                },
                baselineArrow: this.baselineArrow === "neutral"
                    ? undefined
                    : {
                        size: this.keyValue ? 0.8 * baselineFontSize : 0,
                        color: this.runtime.baselineColor || this.secondaryFontColor,
                    },
            };
        }
        /** Get the height of the chart minus all the vertical paddings */
        getDrawableHeight() {
            const verticalPadding = 2 * this.chartPadding;
            let availableHeight = this.height - verticalPadding;
            availableHeight -= this.title ? TITLE_FONT_SIZE * LINE_HEIGHT : 0;
            return availableHeight;
        }
        /** Return the element with he widest text in the chart */
        getWidestElement() {
            const baseline = new BaselineElement(this.runtime.baselineStyle);
            const keyValue = new KeyValueElement(this.runtime.keyValueStyle);
            return baseline.getElementWidth(BASELINE_BOX_HEIGHT_RATIO, this.context, this) >
                keyValue.getElementWidth(KEY_BOX_HEIGHT_RATIO, this.context, this)
                ? baseline
                : keyValue;
        }
    }
    class ScorecardScalableElement {
        style;
        constructor(style = {}) {
            this.style = style;
        }
        measureTextWidth(ctx, text, fontSize) {
            ctx.font = getDefaultContextFont(fontSize, this.style.bold, this.style.italic);
            return ctx.measureText(text).width;
        }
    }
    class BaselineElement extends ScorecardScalableElement {
        getElementWidth(fontSize, ctx, chart) {
            if (!chart.runtime) {
                return 0;
            }
            const baselineStr = chart.baseline;
            // Put mock text to simulate the width of the up/down arrow
            const largeText = chart.baselineArrow !== "neutral" ? "A " + baselineStr : baselineStr;
            let textWidth = this.measureTextWidth(ctx, largeText, fontSize);
            // Baseline descr font size should be smaller than baseline font size
            textWidth += this.measureTextWidth(ctx, chart.baselineDescr, fontSize * BASELINE_DESCR_FONT_RATIO);
            return textWidth;
        }
        getElementMaxFontSize(availableHeight, chart) {
            if (!chart.runtime) {
                return 0;
            }
            const haveBaseline = chart.baseline !== "" || chart.baselineDescr;
            const maxHeight = haveBaseline ? BASELINE_BOX_HEIGHT_RATIO * availableHeight : 0;
            return maxHeight / LINE_HEIGHT;
        }
    }
    class KeyValueElement extends ScorecardScalableElement {
        getElementWidth(fontSize, ctx, chart) {
            if (!chart.runtime) {
                return 0;
            }
            const str = chart.keyValue || "";
            return this.measureTextWidth(ctx, str, fontSize);
        }
        getElementMaxFontSize(availableHeight, chart) {
            if (!chart.runtime) {
                return 0;
            }
            const haveBaseline = chart.baseline !== "" || chart.baselineDescr;
            const maxHeight = haveBaseline ? KEY_BOX_HEIGHT_RATIO * availableHeight : availableHeight;
            return maxHeight / LINE_HEIGHT;
        }
    }

    class ScorecardChart extends owl.Component {
        static template = "o-spreadsheet-ScorecardChart";
        canvas = owl.useRef("chartContainer");
        get runtime() {
            return this.env.model.getters.getChartRuntime(this.props.figure.id);
        }
        setup() {
            owl.useEffect(this.createChart.bind(this), () => {
                const canvas = this.canvas.el;
                const rect = canvas.getBoundingClientRect();
                return [rect.width, rect.height, this.runtime, this.canvas.el];
            });
        }
        createChart() {
            const canvas = this.canvas.el;
            const config = getScorecardConfiguration(canvas.getBoundingClientRect(), this.runtime);
            drawScoreChart(config, canvas);
        }
    }
    ScorecardChart.props = {
        figure: Object,
    };

    /**
     * Registry
     *
     * The Registry class is basically just a mapping from a string key to an object.
     * It is really not much more than an object. It is however useful for the
     * following reasons:
     *
     * 1. it let us react and execute code when someone add something to the registry
     *   (for example, the FunctionRegistry subclass this for this purpose)
     * 2. it throws an error when the get operation fails
     * 3. it provides a chained API to add items to the registry.
     */
    class Registry {
        content = {};
        /**
         * Add an item to the registry
         *
         * Note that this also returns the registry, so another add method call can
         * be chained
         */
        add(key, value) {
            this.content[key] = value;
            return this;
        }
        /**
         * Get an item from the registry
         */
        get(key) {
            /**
             * Note: key in {} is ~12 times slower than {}[key].
             * So, we check the absence of key only when the direct access returns
             * a falsy value. It's done to ensure that the registry can contains falsy values
             */
            const content = this.content[key];
            if (!content) {
                if (!(key in this.content)) {
                    throw new Error(`Cannot find ${key} in this registry!`);
                }
            }
            return content;
        }
        /**
         * Check if the key is already in the registry
         */
        contains(key) {
            return key in this.content;
        }
        /**
         * Get a list of all elements in the registry
         */
        getAll() {
            return Object.values(this.content);
        }
        /**
         * Get a list of all keys in the registry
         */
        getKeys() {
            return Object.keys(this.content);
        }
        /**
         * Remove an item from the registry
         */
        remove(key) {
            delete this.content[key];
        }
    }

    /**
     * Add the `https` prefix to the url if it's missing
     */
    function withHttps(url) {
        return !/^https?:\/\//i.test(url) ? `https://${url}` : url;
    }
    const urlRegistry = new Registry();
    function createWebLink(url, label) {
        url = withHttps(url);
        return {
            url,
            label: label || url,
            isExternal: true,
            isUrlEditable: true,
        };
    }
    urlRegistry.add("sheet_URL", {
        match: (url) => isSheetUrl(url),
        createLink: (url, label) => {
            return {
                label,
                url,
                isExternal: false,
                isUrlEditable: false,
            };
        },
        urlRepresentation(url, getters) {
            const sheetId = parseSheetUrl(url);
            return getters.tryGetSheetName(sheetId) || _t("Invalid sheet");
        },
        open(url, env) {
            const sheetId = parseSheetUrl(url);
            env.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: env.model.getters.getActiveSheetId(),
                sheetIdTo: sheetId,
            });
        },
        sequence: 0,
    });
    const WebUrlSpec = {
        createLink: createWebLink,
        match: (url) => isWebLink(url),
        open: (url) => window.open(url, "_blank"),
        urlRepresentation: (url) => url,
        sequence: 0,
    };
    function findMatchingSpec(url) {
        return (urlRegistry
            .getAll()
            .sort((a, b) => a.sequence - b.sequence)
            .find((urlType) => urlType.match(url)) || WebUrlSpec);
    }
    function urlRepresentation(link, getters) {
        return findMatchingSpec(link.url).urlRepresentation(link.url, getters);
    }
    function openLink(link, env) {
        findMatchingSpec(link.url).open(link.url, env);
    }
    function detectLink(value) {
        if (typeof value !== "string") {
            return undefined;
        }
        if (isMarkdownLink(value)) {
            const { label, url } = parseMarkdownLink(value);
            return findMatchingSpec(url).createLink(url, label);
        }
        else if (isWebLink(value)) {
            return createWebLink(value);
        }
        return undefined;
    }

    function evaluateLiteral(content, localeFormat) {
        return createEvaluatedCell(parseLiteral(content || "", localeFormat.locale), localeFormat);
    }
    function parseLiteral(content, locale) {
        if (content.startsWith("=")) {
            throw new Error(`Cannot parse "${content}" because it's not a literal value. It's a formula`);
        }
        if (isNumber(content, DEFAULT_LOCALE)) {
            return toNumber(content, DEFAULT_LOCALE);
        }
        else if (isDateTime(content, locale)) {
            return toNumber(content, locale);
        }
        else if (isBoolean(content)) {
            return content.toUpperCase() === "TRUE" ? true : false;
        }
        return content;
    }
    function createEvaluatedCell(value, localeFormat) {
        const link = detectLink(value);
        if (link) {
            return {
                ..._createEvaluatedCell(parseLiteral(link.label, localeFormat.locale), {
                    format: localeFormat.format ||
                        detectDateFormat(link.label, localeFormat.locale) ||
                        detectNumberFormat(link.label),
                    locale: localeFormat.locale,
                }),
                link,
            };
        }
        return _createEvaluatedCell(value, localeFormat);
    }
    function _createEvaluatedCell(value, localeFormat) {
        try {
            for (const builder of builders) {
                const evaluateCell = builder(value, localeFormat);
                if (evaluateCell) {
                    return evaluateCell;
                }
            }
            return textCell((value || "").toString(), localeFormat);
        }
        catch (error) {
            return errorCell(new EvaluationError(CellErrorType.GenericError, error.message || DEFAULT_ERROR_MESSAGE));
        }
    }
    function textCell(value, localeFormat) {
        return {
            type: CellValueType.text,
            value,
            format: localeFormat.format,
            isAutoSummable: true,
            defaultAlign: "left",
            formattedValue: formatValue(value, localeFormat),
        };
    }
    function numberCell(value, localeFormat) {
        return {
            type: CellValueType.number,
            value: value || 0,
            format: localeFormat.format,
            isAutoSummable: true,
            defaultAlign: "right",
            formattedValue: formatValue(value, localeFormat),
        };
    }
    const EMPTY_EVALUATED_CELL = {
        type: CellValueType.empty,
        value: "",
        format: undefined,
        isAutoSummable: true,
        defaultAlign: "left",
        formattedValue: "",
    };
    function emptyCell(localeFormat) {
        if (localeFormat.format === undefined) {
            // share the same object to save memory
            return EMPTY_EVALUATED_CELL;
        }
        return {
            type: CellValueType.empty,
            value: "",
            format: localeFormat.format,
            isAutoSummable: true,
            defaultAlign: "left",
            formattedValue: "",
        };
    }
    function dateTimeCell(value, localeFormat) {
        const formattedValue = formatValue(value, localeFormat);
        return {
            type: CellValueType.number,
            value,
            format: localeFormat.format,
            isAutoSummable: false,
            defaultAlign: "right",
            formattedValue,
        };
    }
    function booleanCell(value, localeFormat) {
        const formattedValue = value ? "TRUE" : "FALSE";
        return {
            type: CellValueType.boolean,
            value,
            format: localeFormat.format,
            isAutoSummable: false,
            defaultAlign: "center",
            formattedValue,
        };
    }
    function errorCell(error) {
        return {
            type: CellValueType.error,
            value: error.errorType,
            error,
            isAutoSummable: false,
            defaultAlign: "center",
            formattedValue: error.errorType,
        };
    }
    const builders = [
        function createEmpty(value, localeFormat) {
            if (value === "") {
                return emptyCell(localeFormat);
            }
            return undefined;
        },
        function createDateTime(value, localeFormat) {
            if (!!localeFormat.format &&
                typeof value === "number" &&
                isDateTimeFormat(localeFormat.format)) {
                return dateTimeCell(value, localeFormat);
            }
            return undefined;
        },
        function createNumber(value, localeFormat) {
            if (typeof value === "number") {
                return numberCell(value, localeFormat);
            }
            else if (value === null) {
                return numberCell(0, localeFormat);
            }
            return undefined;
        },
        function createBoolean(value, localeFormat) {
            if (typeof value === "boolean") {
                return booleanCell(value, localeFormat);
            }
            return undefined;
        },
    ];

    /**
     * An AutofillModifierImplementation is used to describe how to handle a
     * AutofillModifier.
     */
    const autofillModifiersRegistry = new Registry();
    autofillModifiersRegistry
        .add("ALPHANUMERIC_INCREMENT_MODIFIER", {
        apply: (rule, data) => {
            rule.current += rule.increment;
            const content = `${rule.prefix}${rule.current
            .toString()
            .padStart(rule.numberPostfixLength || 0, "0")}`;
            return {
                cellData: {
                    border: data.border,
                    style: data.cell && data.cell.style,
                    format: data.cell && data.cell.format,
                    content,
                },
                tooltip: { props: { content } },
            };
        },
    })
        .add("INCREMENT_MODIFIER", {
        apply: (rule, data, getters) => {
            rule.current += rule.increment;
            const content = rule.current.toString();
            const locale = getters.getLocale();
            const tooltipValue = formatValue(rule.current, { format: data.cell?.format, locale });
            return {
                cellData: {
                    border: data.border,
                    style: data.cell && data.cell.style,
                    format: data.cell && data.cell.format,
                    content,
                },
                tooltip: content ? { props: { content: tooltipValue } } : undefined,
            };
        },
    })
        .add("COPY_MODIFIER", {
        apply: (rule, data, getters) => {
            const content = data.cell?.content || "";
            const localeFormat = { locale: getters.getLocale(), format: data.cell?.format };
            return {
                cellData: {
                    border: data.border,
                    style: data.cell && data.cell.style,
                    format: data.cell && data.cell.format,
                    content,
                },
                tooltip: content
                    ? {
                        props: {
                            content: evaluateLiteral(data.cell?.content, localeFormat).formattedValue,
                        },
                    }
                    : undefined,
            };
        },
    })
        .add("FORMULA_MODIFIER", {
        apply: (rule, data, getters, direction) => {
            rule.current += rule.increment;
            let x = 0;
            let y = 0;
            switch (direction) {
                case "up" /* DIRECTION.UP */:
                    x = 0;
                    y = -rule.current;
                    break;
                case "down" /* DIRECTION.DOWN */:
                    x = 0;
                    y = rule.current;
                    break;
                case "left" /* DIRECTION.LEFT */:
                    x = -rule.current;
                    y = 0;
                    break;
                case "right" /* DIRECTION.RIGHT */:
                    x = rule.current;
                    y = 0;
                    break;
            }
            const cell = data.cell;
            if (!cell || !cell.isFormula) {
                return { cellData: {} };
            }
            const sheetId = data.sheetId;
            const content = getters.getTranslatedCellFormula(sheetId, x, y, cell.compiledFormula);
            return {
                cellData: {
                    border: data.border,
                    style: cell.style,
                    format: cell.format,
                    content,
                },
                tooltip: content ? { props: { content } } : undefined,
            };
        },
    });

    const autofillRulesRegistry = new Registry();
    const numberPostfixRegExp = /(\d+)$/;
    const stringPrefixRegExp = /^(.*\D+)/;
    const alphaNumericValueRegExp = /^(.*\D+)(\d+)$/;
    /**
     * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
     * Return the one which contains the given cell
     */
    function getGroup(cell, cells, filter) {
        let group = [];
        let found = false;
        for (let x of cells) {
            if (x === cell) {
                found = true;
            }
            const cellValue = evaluateLiteral(x?.content, { locale: DEFAULT_LOCALE });
            if (filter(cellValue)) {
                group.push(cellValue);
            }
            else {
                if (found) {
                    return group;
                }
                group = [];
            }
        }
        return group;
    }
    /**
     * Get the average steps between numbers
     */
    function getAverageIncrement(group) {
        const averages = [];
        let last = group[0];
        for (let i = 1; i < group.length; i++) {
            const current = group[i];
            averages.push(current - last);
            last = current;
        }
        return averages.reduce((a, b) => a + b, 0) / averages.length;
    }
    /**
     * Get the step for a group
     */
    function calculateIncrementBasedOnGroup(group) {
        let increment = 1;
        if (group.length >= 2) {
            increment = getAverageIncrement(group) * group.length;
        }
        return increment;
    }
    autofillRulesRegistry
        .add("simple_value_copy", {
        condition: (cell, cells) => {
            return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
        },
        generateRule: () => {
            return { type: "COPY_MODIFIER" };
        },
        sequence: 10,
    })
        .add("increment_alphanumeric_value", {
        condition: (cell) => !cell.isFormula &&
            evaluateLiteral(cell.content, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
            alphaNumericValueRegExp.test(cell.content),
        generateRule: (cell, cells) => {
            const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
            const prefix = cell.content.match(stringPrefixRegExp)[0];
            const numberPostfixLength = cell.content.length - prefix.length;
            const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
                alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
                .filter((cell) => prefix === cell.value.toString().match(stringPrefixRegExp)[0])
                .map((cell) => parseInt(cell.value.toString().match(numberPostfixRegExp)[0]));
            const increment = calculateIncrementBasedOnGroup(group);
            return {
                type: "ALPHANUMERIC_INCREMENT_MODIFIER",
                prefix,
                current: numberPostfix,
                increment,
                numberPostfixLength,
            };
        },
        sequence: 15,
    })
        .add("copy_text", {
        condition: (cell) => !cell.isFormula &&
            evaluateLiteral(cell.content, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
        generateRule: () => {
            return { type: "COPY_MODIFIER" };
        },
        sequence: 20,
    })
        .add("update_formula", {
        condition: (cell) => cell.isFormula,
        generateRule: (_, cells) => {
            return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
        },
        sequence: 30,
    })
        .add("increment_number", {
        condition: (cell) => !cell.isFormula &&
            evaluateLiteral(cell.content, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
        generateRule: (cell, cells) => {
            const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
            const increment = calculateIncrementBasedOnGroup(group);
            const evaluation = evaluateLiteral(cell.content, { locale: DEFAULT_LOCALE });
            return {
                type: "INCREMENT_MODIFIER",
                increment,
                current: evaluation.type === CellValueType.number ? evaluation.value : 0,
            };
        },
        sequence: 40,
    });

    /**
     * This file is largely inspired by owl 1.
     * `css` tag has been removed from owl 2 without workaround to manage css.
     * So, the solution was to import the behavior of owl 1 directly in our
     * codebase, with one difference: the css is added to the sheet as soon as the
     * css tag is executed. In owl 1, the css was added as soon as a Component was
     * created for the first time.
     */
    const STYLESHEETS = {};
    let nextId = 0;
    /**
     * CSS tag helper for defining inline stylesheets.  With this, one can simply define
     * an inline stylesheet with just the following code:
     * ```js
     *     css`.component-a { color: red; }`;
     * ```
     */
    function css(strings, ...args) {
        const name = `__sheet__${nextId++}`;
        const value = String.raw(strings, ...args);
        registerSheet(name, value);
        activateSheet(name);
        return name;
    }
    function processSheet(str) {
        const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
        const selectorStack = [];
        const parts = [];
        let rules = [];
        function generateSelector(stackIndex, parentSelector) {
            const parts = [];
            for (const selector of selectorStack[stackIndex]) {
                let part = (parentSelector && parentSelector + " " + selector) || selector;
                if (part.includes("&")) {
                    part = selector.replace(/&/g, parentSelector || "");
                }
                if (stackIndex < selectorStack.length - 1) {
                    part = generateSelector(stackIndex + 1, part);
                }
                parts.push(part);
            }
            return parts.join(", ");
        }
        function generateRules() {
            if (rules.length) {
                parts.push(generateSelector(0) + " {");
                parts.push(...rules);
                parts.push("}");
                rules = [];
            }
        }
        while (tokens.length) {
            let token = tokens.shift();
            if (token === "}") {
                generateRules();
                selectorStack.pop();
            }
            else {
                if (tokens[0] === "{") {
                    generateRules();
                    selectorStack.push(token.split(/\s*,\s*/));
                    tokens.shift();
                }
                if (tokens[0] === ";") {
                    rules.push("  " + token + ";");
                }
            }
        }
        return parts.join("\n");
    }
    function registerSheet(id, css) {
        const sheet = document.createElement("style");
        sheet.textContent = processSheet(css);
        STYLESHEETS[id] = sheet;
    }
    function activateSheet(id) {
        const sheet = STYLESHEETS[id];
        sheet.setAttribute("component", id);
        document.head.appendChild(sheet);
    }
    function getTextDecoration({ strikethrough, underline, }) {
        if (!strikethrough && !underline) {
            return "none";
        }
        return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
    }
    /**
     * Convert the cell style to CSS properties.
     */
    function cellStyleToCss(style) {
        const attributes = cellTextStyleToCss(style);
        if (!style)
            return attributes;
        if (style.fillColor) {
            attributes["background"] = style.fillColor;
        }
        return attributes;
    }
    /**
     * Convert the cell text style to CSS properties.
     */
    function cellTextStyleToCss(style) {
        const attributes = {};
        if (!style)
            return attributes;
        if (style.bold) {
            attributes["font-weight"] = "bold";
        }
        if (style.italic) {
            attributes["font-style"] = "italic";
        }
        if (style.strikethrough || style.underline) {
            let decoration = style.strikethrough ? "line-through" : "";
            decoration = style.underline ? decoration + " underline" : decoration;
            attributes["text-decoration"] = decoration;
        }
        if (style.textColor) {
            attributes["color"] = style.textColor;
        }
        return attributes;
    }
    /**
     * Transform CSS properties into a CSS string.
     */
    function cssPropertiesToCss(attributes) {
        const str = Object.entries(attributes)
            .filter(([attName, attValue]) => attValue !== undefined)
            .map(([attName, attValue]) => `${attName}:${attValue};`)
            .join(" ");
        return str;
    }
    function getElementMargins(el) {
        const style = window.getComputedStyle(el);
        const margins = {
            top: parseInt(style.marginTop, 10) || 0,
            bottom: parseInt(style.marginBottom, 10) || 0,
            left: parseInt(style.marginLeft, 10) || 0,
            right: parseInt(style.marginRight, 10) || 0,
        };
        return margins;
    }

    const ERROR_TOOLTIP_MAX_HEIGHT = 80;
    const ERROR_TOOLTIP_WIDTH = 180;
    css /* scss */ `
  .o-error-tooltip {
    font-size: 13px;
    background-color: white;
    border-left: 3px solid red;
    padding: 10px;
    width: ${ERROR_TOOLTIP_WIDTH}px;
    box-sizing: border-box !important;
    overflow-wrap: break-word;

    .o-error-tooltip-message {
      overflow: hidden;
    }
  }
`;
    class ErrorToolTip extends owl.Component {
        static maxSize = { maxHeight: ERROR_TOOLTIP_MAX_HEIGHT };
        static template = "o-spreadsheet-ErrorToolTip";
    }
    ErrorToolTip.props = {
        errors: Array,
        onClosed: { type: Function, optional: true },
    };
    const ErrorToolTipPopoverBuilder = {
        onHover: (position, getters) => {
            const cell = getters.getEvaluatedCell(position);
            const errors = [];
            if (cell.type === CellValueType.error && cell.error.isVerbose) {
                errors.push({
                    title: _t("Error"),
                    message: cell.error.message,
                });
            }
            const validationErrorMessage = getters.getInvalidDataValidationMessage(position);
            if (validationErrorMessage) {
                errors.push({
                    title: _t("Invalid"),
                    message: validationErrorMessage,
                });
            }
            if (!errors.length) {
                return { isOpen: false };
            }
            return {
                isOpen: true,
                props: { errors: errors },
                Component: ErrorToolTip,
                cellCorner: "TopRight",
            };
        },
    };

    css /*SCSS*/ `
  .o-filter-menu-value {
    padding: 4px;
    line-height: 20px;
    height: 28px;
    .o-filter-menu-value-checked {
      width: 20px;
    }
  }
`;
    class FilterMenuValueItem extends owl.Component {
        static template = "o-spreadsheet-FilterMenuValueItem";
        itemRef = owl.useRef("menuValueItem");
        setup() {
            owl.onWillPatch(() => {
                if (this.props.scrolledTo) {
                    this.scrollListToSelectedValue();
                }
            });
        }
        scrollListToSelectedValue() {
            if (!this.itemRef.el) {
                return;
            }
            this.itemRef.el.scrollIntoView?.({
                block: this.props.scrolledTo === "bottom" ? "end" : "start",
            });
        }
    }
    FilterMenuValueItem.props = {
        value: String,
        isChecked: Boolean,
        isSelected: Boolean,
        onMouseMove: Function,
        onClick: Function,
        scrolledTo: { type: String, optional: true },
    };

    const FILTER_MENU_HEIGHT = 295;
    const CSS$2 = css /* scss */ `
  .o-filter-menu {
    box-sizing: border-box;
    padding: 8px 16px;
    height: ${FILTER_MENU_HEIGHT}px;
    line-height: 1;

    .o-filter-menu-item {
      display: flex;
      box-sizing: border-box;
      height: ${MENU_ITEM_HEIGHT}px;
      padding: 4px 4px 4px 0px;
      cursor: pointer;
      user-select: none;

      &.selected {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }

    input {
      box-sizing: border-box;
      margin-bottom: 5px;
      border: 1px solid #949494;
      height: 24px;
      padding-right: 28px;
    }

    .o-search-icon {
      right: 5px;
      top: 4px;
      opacity: 0.4;

      svg {
        height: 16px;
        width: 16px;
        vertical-align: middle;
      }
    }

    .o-filter-menu-actions {
      display: flex;
      flex-direction: row;
      margin-bottom: 4px;

      .o-filter-menu-action-text {
        cursor: pointer;
        margin-right: 10px;
        color: blue;
        text-decoration: underline;
      }
    }

    .o-filter-menu-list {
      flex: auto;
      overflow-y: auto;
      border: 1px solid #949494;

      .o-filter-menu-no-values {
        color: #949494;
        font-style: italic;
      }
    }

    .o-filter-menu-buttons {
      margin-top: 9px;

      .o-filter-menu-button {
        border: 1px solid lightgrey;
        padding: 6px 10px;
        cursor: pointer;
        border-radius: 4px;
        font-weight: 500;
        line-height: 16px;
      }

      .o-filter-menu-button-cancel {
        background: white;
        &:hover {
          background-color: rgba(0, 0, 0, 0.08);
        }
      }

      .o-filter-menu-button-primary {
        background-color: #188038;
        &:hover {
          background-color: #1d9641;
        }
        color: white;
        font-weight: bold;
        margin-left: 10px;
      }
    }
  }
`;
    class FilterMenu extends owl.Component {
        static size = { width: MENU_WIDTH, height: FILTER_MENU_HEIGHT };
        static template = "o-spreadsheet-FilterMenu";
        static style = CSS$2;
        static components = { FilterMenuValueItem };
        state = owl.useState({
            values: [],
            textFilter: "",
            selectedValue: undefined,
        });
        searchBar = owl.useRef("filterMenuSearchBar");
        setup() {
            owl.onWillUpdateProps((nextProps) => {
                if (!deepEquals(nextProps.filterPosition, this.props.filterPosition)) {
                    this.state.values = this.getFilterValues(nextProps.filterPosition);
                }
            });
            this.state.values = this.getFilterValues(this.props.filterPosition);
        }
        get isReadonly() {
            return this.env.model.getters.isReadonly();
        }
        getFilterValues(position) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const filter = this.env.model.getters.getFilter({ sheetId, ...position });
            if (!filter) {
                return [];
            }
            const cellValues = (filter.filteredZone ? positions(filter.filteredZone) : [])
                .filter(({ row }) => !this.env.model.getters.isRowHidden(sheetId, row))
                .map(({ col, row }) => this.env.model.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue);
            const filterValues = this.env.model.getters.getFilterValues({ sheetId, ...position });
            const strValues = [...cellValues, ...filterValues];
            const normalizedFilteredValues = filterValues.map(toLowerCase);
            // Set with lowercase values to avoid duplicates
            const normalizedValues = [...new Set(strValues.map(toLowerCase))];
            const sortedValues = normalizedValues.sort((val1, val2) => val1.localeCompare(val2, undefined, { numeric: true, sensitivity: "base" }));
            return sortedValues.map((normalizedValue) => {
                const checked = normalizedFilteredValues.findIndex((filteredValue) => filteredValue === normalizedValue) ===
                    -1;
                return {
                    checked,
                    string: strValues.find((val) => toLowerCase(val) === normalizedValue) || "",
                };
            });
        }
        checkValue(value) {
            this.state.selectedValue = value.string;
            value.checked = !value.checked;
            this.searchBar.el?.focus();
        }
        onMouseMove(value) {
            this.state.selectedValue = value.string;
        }
        selectAll() {
            this.displayedValues.forEach((value) => (value.checked = true));
        }
        clearAll() {
            this.displayedValues.forEach((value) => (value.checked = false));
        }
        get filterTable() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const position = this.props.filterPosition;
            return this.env.model.getters.getFilterTable({ sheetId, ...position });
        }
        get displayedValues() {
            if (!this.state.textFilter) {
                return this.state.values;
            }
            return fuzzyLookup(this.state.textFilter, this.state.values, (val) => val.string);
        }
        confirm() {
            const position = this.props.filterPosition;
            this.env.model.dispatch("UPDATE_FILTER", {
                ...position,
                sheetId: this.env.model.getters.getActiveSheetId(),
                hiddenValues: this.state.values.filter((val) => !val.checked).map((val) => val.string),
            });
            this.props.onClosed?.();
        }
        cancel() {
            this.props.onClosed?.();
        }
        onKeyDown(ev) {
            const displayedValues = this.displayedValues;
            if (displayedValues.length === 0)
                return;
            let selectedIndex = undefined;
            if (this.state.selectedValue !== undefined) {
                const index = displayedValues.findIndex((val) => val.string === this.state.selectedValue);
                selectedIndex = index === -1 ? undefined : index;
            }
            switch (ev.key) {
                case "ArrowDown":
                    if (selectedIndex === undefined) {
                        selectedIndex = 0;
                    }
                    else {
                        selectedIndex = Math.min(selectedIndex + 1, displayedValues.length - 1);
                    }
                    ev.preventDefault();
                    ev.stopPropagation();
                    break;
                case "ArrowUp":
                    if (selectedIndex === undefined) {
                        selectedIndex = displayedValues.length - 1;
                    }
                    else {
                        selectedIndex = Math.max(selectedIndex - 1, 0);
                    }
                    ev.preventDefault();
                    ev.stopPropagation();
                    break;
                case "Enter":
                    if (selectedIndex !== undefined) {
                        this.checkValue(displayedValues[selectedIndex]);
                    }
                    ev.stopPropagation();
                    ev.preventDefault();
                    break;
            }
            this.state.selectedValue =
                selectedIndex !== undefined ? displayedValues[selectedIndex].string : undefined;
            if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
                this.scrollListToSelectedValue(ev.key);
            }
        }
        clearScrolledToValue() {
            this.state.values.forEach((val) => (val.scrolledTo = undefined));
        }
        scrollListToSelectedValue(arrow) {
            this.clearScrolledToValue();
            const selectedValue = this.state.values.find((val) => val.string === this.state.selectedValue);
            if (selectedValue) {
                selectedValue.scrolledTo = arrow === "ArrowUp" ? "top" : "bottom";
            }
        }
        sortFilterZone(sortDirection) {
            const filterPosition = this.props.filterPosition;
            const filterTable = this.filterTable;
            if (!filterPosition || !filterTable || !filterTable.contentZone) {
                return;
            }
            const sheetId = this.env.model.getters.getActiveSheetId();
            this.env.model.dispatch("SORT_CELLS", {
                sheetId,
                col: filterPosition.col,
                row: filterTable.contentZone.top,
                zone: filterTable.contentZone,
                sortDirection,
                sortOptions: { emptyCellAsZero: true, sortHeaders: true },
            });
            this.props.onClosed?.();
        }
    }
    FilterMenu.props = {
        filterPosition: Object,
        onClosed: { type: Function, optional: true },
    };
    const FilterMenuPopoverBuilder = {
        onOpen: (position, getters) => {
            return {
                isOpen: true,
                props: { filterPosition: position },
                Component: FilterMenu,
                cellCorner: "BottomLeft",
            };
        },
    };

    const macRegex = /Mac/i;
    /**
     * Return true if the event was triggered from
     * a child element.
     */
    function isChildEvent(parent, ev) {
        return !!ev.target && parent.contains(ev.target);
    }
    function gridOverlayPosition() {
        const spreadsheetElement = document.querySelector(".o-grid-overlay");
        if (spreadsheetElement) {
            const { top, left } = spreadsheetElement?.getBoundingClientRect();
            return { top, left };
        }
        throw new Error("Can't find spreadsheet position");
    }
    function getBoundingRectAsPOJO(el) {
        const rect = el.getBoundingClientRect();
        return {
            x: rect.x,
            y: rect.y,
            width: rect.width,
            height: rect.height,
        };
    }
    /**
     * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
     */
    function* iterateChildren(el) {
        yield el;
        if (el.hasChildNodes()) {
            for (let child of el.childNodes) {
                yield* iterateChildren(child);
            }
        }
    }
    function getOpenedMenus() {
        return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
    }
    function isMacOS() {
        return Boolean(macRegex.test(navigator.userAgent));
    }
    /**
     * @param {KeyboardEvent | MouseEvent} ev
     * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
     * On Mac, this is the "meta" or "command" key.
     */
    function isCtrlKey(ev) {
        return isMacOS() ? ev.metaKey : ev.ctrlKey;
    }

    /**
     * Return the o-spreadsheet element position relative
     * to the browser viewport.
     */
    function useSpreadsheetRect() {
        const position = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
        let spreadsheetElement = document.querySelector(".o-spreadsheet");
        updatePosition();
        function updatePosition() {
            if (!spreadsheetElement) {
                spreadsheetElement = document.querySelector(".o-spreadsheet");
            }
            if (spreadsheetElement) {
                const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();
                position.x = left;
                position.y = top;
                position.width = width;
                position.height = height;
            }
        }
        owl.onMounted(updatePosition);
        owl.onPatched(updatePosition);
        return position;
    }
    /**
     * Return the component (or ref's component) BoundingRect, relative
     * to the upper left corner of the screen (<body> element).
     *
     * Note: when used with a <Portal/> component, it will
     * return the portal position, not the teleported position.
     */
    function useAbsoluteBoundingRect(ref) {
        const rect = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
        function updateElRect() {
            const el = ref.el;
            if (el === null) {
                return;
            }
            const { top, left, width, height } = el.getBoundingClientRect();
            rect.x = left;
            rect.y = top;
            rect.width = width;
            rect.height = height;
        }
        owl.onMounted(updateElRect);
        owl.onPatched(updateElRect);
        return rect;
    }
    /**
     * Get the rectangle inside which a popover should stay when being displayed.
     * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
     * element by default.
     *
     * Coordinates are expressed expressed as absolute DOM position.
     */
    function usePopoverContainer() {
        const container = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
        const component = owl.useComponent();
        const spreadsheetRect = useSpreadsheetRect();
        function updateRect() {
            const env = component.env;
            const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
            container.x = newRect.x;
            container.y = newRect.y;
            container.width = newRect.width;
            container.height = newRect.height;
        }
        updateRect();
        owl.onMounted(updateRect);
        owl.onPatched(updateRect);
        return container;
    }

    /**
     * Compute the intersection of two rectangles. Returns nothing if the two rectangles don't overlap
     */
    function rectIntersection(rect1, rect2) {
        return zoneToRect(intersection(rectToZone(rect1), rectToZone(rect2)));
    }
    /** Compute the union of the rectangles, ie. the smallest rectangle that contain them all */
    function rectUnion(...rects) {
        return zoneToRect(union(...rects.map(rectToZone)));
    }
    function rectToZone(rect) {
        return {
            left: rect.x,
            top: rect.y,
            right: rect.x + rect.width,
            bottom: rect.y + rect.height,
        };
    }
    function zoneToRect(zone) {
        if (!zone)
            return undefined;
        return {
            x: zone.left,
            y: zone.top,
            width: zone.right - zone.left,
            height: zone.bottom - zone.top,
        };
    }

    css /* scss */ `
  .o-popover {
    position: absolute;
    z-index: ${ComponentsImportance.Popover};
    overflow: auto;
    box-shadow: 1px 2px 5px 2px rgb(51 51 51 / 15%);
    width: fit-content;
    height: fit-content;
  }
`;
    class Popover extends owl.Component {
        static template = "o-spreadsheet-Popover";
        static defaultProps = {
            positioning: "BottomLeft",
            verticalOffset: 0,
            onMouseWheel: () => { },
            onPopoverMoved: () => { },
            onPopoverHidden: () => { },
            zIndex: ComponentsImportance.Popover,
        };
        popoverRef = owl.useRef("popover");
        currentPosition = undefined;
        currentDisplayValue = undefined;
        spreadsheetRect = useSpreadsheetRect();
        containerRect;
        setup() {
            this.containerRect = usePopoverContainer();
            // useEffect occurs after the DOM is created and the element width/height are computed, but before
            // the element in rendered, so we can still set its position
            owl.useEffect(() => {
                if (!this.containerRect)
                    throw new Error("Popover container is not defined");
                const el = this.popoverRef.el;
                const anchor = rectIntersection(this.props.anchorRect, this.containerRect);
                const newDisplay = anchor ? "block" : "none";
                if (this.currentDisplayValue !== "none" && newDisplay === "none") {
                    this.props.onPopoverHidden?.();
                }
                el.style.display = newDisplay;
                this.currentDisplayValue = newDisplay;
                if (!anchor)
                    return;
                const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
                const elDims = {
                    width: el.getBoundingClientRect().width,
                    height: el.getBoundingClientRect().height,
                };
                const spreadsheetRect = this.spreadsheetRect;
                const popoverPositionHelper = this.props.positioning === "BottomLeft"
                    ? new BottomLeftPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect)
                    : new TopRightPopoverContext(anchor, this.containerRect, propsMaxSize, spreadsheetRect);
                const style = popoverPositionHelper.getCss(elDims, this.props.verticalOffset);
                for (const property of Object.keys(style)) {
                    el.style[property] = style[property];
                }
                const newPosition = popoverPositionHelper.getCurrentPosition(elDims);
                if (this.currentPosition && newPosition !== this.currentPosition) {
                    this.props.onPopoverMoved?.();
                }
                this.currentPosition = newPosition;
            });
        }
        get popoverStyle() {
            return cssPropertiesToCss({
                "z-index": `${this.props.zIndex}`,
            });
        }
    }
    Popover.props = {
        anchorRect: Object,
        containerRect: { type: Object, optional: true },
        positioning: { type: String, optional: true },
        maxWidth: { type: Number, optional: true },
        maxHeight: { type: Number, optional: true },
        verticalOffset: { type: Number, optional: true },
        onMouseWheel: { type: Function, optional: true },
        onPopoverHidden: { type: Function, optional: true },
        onPopoverMoved: { type: Function, optional: true },
        zIndex: { type: Number, optional: true },
        slots: Object,
    };
    class PopoverPositionContext {
        anchorRect;
        containerRect;
        propsMaxSize;
        spreadsheetOffset;
        constructor(anchorRect, containerRect, propsMaxSize, spreadsheetOffset) {
            this.anchorRect = anchorRect;
            this.containerRect = containerRect;
            this.propsMaxSize = propsMaxSize;
            this.spreadsheetOffset = spreadsheetOffset;
        }
        /** Check if there is enough space for the popover to be rendered at the bottom of the anchorRect */
        shouldRenderAtBottom(elementHeight) {
            return (elementHeight <= this.availableHeightDown ||
                this.availableHeightDown >= this.availableHeightUp);
        }
        /** Check if there is enough space for the popover to be rendered at the right of the anchorRect */
        shouldRenderAtRight(elementWidth) {
            return (elementWidth <= this.availableWidthRight ||
                this.availableWidthRight >= this.availableWidthLeft);
        }
        getMaxHeight(elementHeight) {
            const shouldRenderAtBottom = this.shouldRenderAtBottom(elementHeight);
            const availableHeight = shouldRenderAtBottom
                ? this.availableHeightDown
                : this.availableHeightUp;
            return this.propsMaxSize.height
                ? Math.min(availableHeight, this.propsMaxSize.height)
                : availableHeight;
        }
        getMaxWidth(elementWidth) {
            const shouldRenderAtRight = this.shouldRenderAtRight(elementWidth);
            const availableWidth = shouldRenderAtRight ? this.availableWidthRight : this.availableWidthLeft;
            return this.propsMaxSize.width
                ? Math.min(availableWidth, this.propsMaxSize.width)
                : availableWidth;
        }
        getCss(elDims, verticalOffset) {
            const maxHeight = this.getMaxHeight(elDims.height);
            const maxWidth = this.getMaxWidth(elDims.width);
            const actualHeight = Math.min(maxHeight, elDims.height);
            const actualWidth = Math.min(maxWidth, elDims.width);
            const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
            const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
            verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset;
            const cssProperties = {
                "max-height": maxHeight + "px",
                "max-width": maxWidth + "px",
                top: this.getTopCoordinate(actualHeight, shouldRenderAtBottom) -
                    this.spreadsheetOffset.y -
                    verticalOffset +
                    "px",
                left: this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + "px",
            };
            return cssProperties;
        }
        getCurrentPosition(elDims) {
            const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
            const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
            if (shouldRenderAtBottom && shouldRenderAtRight)
                return "BottomRight";
            if (shouldRenderAtBottom && !shouldRenderAtRight)
                return "BottomLeft";
            if (!shouldRenderAtBottom && shouldRenderAtRight)
                return "TopRight";
            return "TopLeft";
        }
    }
    class BottomLeftPopoverContext extends PopoverPositionContext {
        get availableHeightUp() {
            return this.anchorRect.y - this.containerRect.y;
        }
        get availableHeightDown() {
            return this.containerRect.height - this.availableHeightUp - this.anchorRect.height;
        }
        get availableWidthRight() {
            return this.containerRect.x + this.containerRect.width - this.anchorRect.x;
        }
        get availableWidthLeft() {
            return this.anchorRect.x + this.anchorRect.width - this.containerRect.x;
        }
        getTopCoordinate(elementHeight, shouldRenderAtBottom) {
            if (shouldRenderAtBottom) {
                return this.anchorRect.y + this.anchorRect.height;
            }
            else {
                return this.anchorRect.y - elementHeight;
            }
        }
        getLeftCoordinate(elementWidth, shouldRenderAtRight) {
            if (shouldRenderAtRight) {
                return this.anchorRect.x;
            }
            else {
                return this.anchorRect.x + this.anchorRect.width - elementWidth;
            }
        }
    }
    class TopRightPopoverContext extends PopoverPositionContext {
        get availableHeightUp() {
            return this.anchorRect.y + this.anchorRect.height - this.containerRect.y;
        }
        get availableHeightDown() {
            return this.containerRect.y + this.containerRect.height - this.anchorRect.y;
        }
        get availableWidthRight() {
            return this.containerRect.width - this.anchorRect.width - this.availableWidthLeft;
        }
        get availableWidthLeft() {
            return this.anchorRect.x - this.containerRect.x;
        }
        getTopCoordinate(elementHeight, shouldRenderAtBottom) {
            if (shouldRenderAtBottom) {
                return this.anchorRect.y;
            }
            else {
                return this.anchorRect.y + this.anchorRect.height - elementHeight;
            }
        }
        getLeftCoordinate(elementWidth, shouldRenderAtRight) {
            if (shouldRenderAtRight) {
                return this.anchorRect.x + this.anchorRect.width;
            }
            else {
                return this.anchorRect.x - elementWidth;
            }
        }
    }

    //------------------------------------------------------------------------------
    // Context Menu Component
    //------------------------------------------------------------------------------
    css /* scss */ `
  .o-menu {
    background-color: white;
    padding: ${MENU_VERTICAL_PADDING}px 0px;
    width: ${MENU_WIDTH}px;
    box-sizing: border-box !important;

    .o-menu-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      box-sizing: border-box;
      height: ${MENU_ITEM_HEIGHT}px;
      padding: ${MENU_ITEM_PADDING_VERTICAL}px ${MENU_ITEM_PADDING_HORIZONTAL}px;
      cursor: pointer;
      user-select: none;

      .o-menu-item-name {
        min-width: 40%;
      }

      &.o-menu-root {
        display: flex;
        justify-content: space-between;
      }

      .o-menu-item-icon {
        display: inline-block;
        margin: 0px 8px 0px 0px;
        width: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;
        line-height: ${MENU_ITEM_HEIGHT - 2 * MENU_ITEM_PADDING_VERTICAL}px;
      }
      .o-menu-item-root {
        width: 10px;
      }

      &:not(.disabled) {
        &:hover,
        &.o-menu-item-active {
          background-color: ${BG_HOVER_COLOR};
        }
        .o-menu-item-description {
          color: grey;
        }
        .o-menu-item-icon {
          .o-icon {
            color: ${ICONS_COLOR};
          }
        }
      }
      &.disabled {
        color: ${DISABLED_TEXT_COLOR};
        cursor: not-allowed;
      }
    }
  }
`;
    class Menu extends owl.Component {
        static template = "o-spreadsheet-Menu";
        static components = { Menu, Popover };
        static defaultProps = {
            depth: 1,
        };
        subMenu = owl.useState({
            isOpen: false,
            position: null,
            scrollOffset: 0,
            menuItems: [],
        });
        menuRef = owl.useRef("menu");
        position = useAbsoluteBoundingRect(this.menuRef);
        setup() {
            owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
            owl.useExternalListener(window, "contextmenu", this.onExternalClick, { capture: true });
            owl.onWillUpdateProps((nextProps) => {
                if (nextProps.menuItems !== this.props.menuItems) {
                    this.closeSubMenu();
                }
            });
        }
        get menuItemsAndSeparators() {
            const menuItemsAndSeparators = [];
            for (let i = 0; i < this.props.menuItems.length; i++) {
                const menuItem = this.props.menuItems[i];
                if (menuItem.isVisible(this.env)) {
                    menuItemsAndSeparators.push(menuItem);
                }
                if (menuItem.separator &&
                    i !== this.props.menuItems.length - 1 && // no separator at the end
                    menuItemsAndSeparators[menuItemsAndSeparators.length - 1] !== "separator" // no double separator
                ) {
                    menuItemsAndSeparators.push("separator");
                }
            }
            if (menuItemsAndSeparators[menuItemsAndSeparators.length - 1] === "separator") {
                menuItemsAndSeparators.pop();
            }
            if (menuItemsAndSeparators.length === 1 && menuItemsAndSeparators[0] === "separator") {
                return [];
            }
            return menuItemsAndSeparators;
        }
        get subMenuPosition() {
            const position = Object.assign({}, this.subMenu.position);
            position.y -= this.subMenu.scrollOffset || 0;
            return position;
        }
        get popoverProps() {
            const isRoot = this.props.depth === 1;
            return {
                anchorRect: {
                    x: this.props.position.x - MENU_WIDTH * (this.props.depth - 1),
                    y: this.props.position.y,
                    width: isRoot ? 0 : MENU_WIDTH,
                    height: isRoot ? 0 : MENU_ITEM_HEIGHT,
                },
                positioning: "TopRight",
                verticalOffset: isRoot ? 0 : MENU_VERTICAL_PADDING,
                onPopoverHidden: () => this.closeSubMenu(),
                onPopoverMoved: () => this.closeSubMenu(),
            };
        }
        get childrenHaveIcon() {
            return this.props.menuItems.some((menuItem) => !!this.getIconName(menuItem));
        }
        getIconName(menu) {
            if (menu.icon(this.env)) {
                return menu.icon(this.env);
            }
            if (menu.isActive?.(this.env)) {
                return "o-spreadsheet-Icon.CHECK";
            }
            return "";
        }
        getColor(menu) {
            return menu.textColor ? `color: ${menu.textColor}` : undefined;
        }
        async activateMenu(menu) {
            const result = await menu.execute?.(this.env);
            this.close();
            this.props.onMenuClicked?.({ detail: result });
        }
        close() {
            this.closeSubMenu();
            this.props.onClose();
        }
        onExternalClick(ev) {
            // Don't close a root menu when clicked to open the submenus.
            const el = this.menuRef.el;
            if (el && getOpenedMenus().some((el) => isChildEvent(el, ev))) {
                return;
            }
            ev.closedMenuId = this.props.menuId;
            this.close();
        }
        getName(menu) {
            return menu.name(this.env);
        }
        isRoot(menu) {
            return !menu.execute;
        }
        isEnabled(menu) {
            if (menu.isEnabled(this.env)) {
                return this.env.model.getters.isReadonly() ? menu.isReadonlyAllowed : true;
            }
            return false;
        }
        onScroll(ev) {
            this.subMenu.scrollOffset = ev.target.scrollTop;
        }
        /**
         * If the given menu is not disabled, open it's submenu at the
         * correct position according to available surrounding space.
         */
        openSubMenu(menu, menuIndex, ev) {
            const parentMenuEl = ev.currentTarget;
            if (!parentMenuEl)
                return;
            const y = parentMenuEl.getBoundingClientRect().top;
            this.subMenu.position = {
                x: this.position.x + this.props.depth * MENU_WIDTH,
                y: y - (this.subMenu.scrollOffset || 0),
            };
            this.subMenu.menuItems = menu.children(this.env);
            this.subMenu.isOpen = true;
            this.subMenu.parentMenu = menu;
        }
        isParentMenu(subMenu, menuItem) {
            return subMenu.parentMenu?.id === menuItem.id;
        }
        closeSubMenu() {
            this.subMenu.isOpen = false;
            this.subMenu.parentMenu = undefined;
        }
        onClickMenu(menu, menuIndex, ev) {
            if (this.isEnabled(menu)) {
                if (this.isRoot(menu)) {
                    this.openSubMenu(menu, menuIndex, ev);
                }
                else {
                    this.activateMenu(menu);
                }
            }
        }
        onMouseOver(menu, position, ev) {
            if (this.isEnabled(menu)) {
                if (this.isRoot(menu)) {
                    this.openSubMenu(menu, position, ev);
                }
                else {
                    this.closeSubMenu();
                }
            }
        }
    }
    Menu.props = {
        position: Object,
        menuItems: Array,
        depth: { type: Number, optional: true },
        maxHeight: { type: Number, optional: true },
        onClose: Function,
        onMenuClicked: { type: Function, optional: true },
        menuId: { type: String, optional: true },
    };

    const LINK_TOOLTIP_HEIGHT = 32;
    const LINK_TOOLTIP_WIDTH = 220;
    css /* scss */ `
  .o-link-tool {
    font-size: 13px;
    background-color: white;
    box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);
    padding: 6px 12px;
    border-radius: 4px;
    display: flex;
    justify-content: space-between;
    height: ${LINK_TOOLTIP_HEIGHT}px;
    width: ${LINK_TOOLTIP_WIDTH}px;
    box-sizing: border-box !important;

    img {
      margin-right: 3px;
      width: 16px;
      height: 16px;
    }

    a.o-link {
      color: #01666b;
      text-decoration: none;
      flex-grow: 2;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    a.o-link:hover {
      text-decoration: none;
      color: #001d1f;
      cursor: pointer;
    }
  }
  .o-link-icon {
    float: right;
    padding-left: 5px;
    .o-icon {
      height: 16px;
    }
  }
  .o-link-icon .o-icon {
    height: 13px;
  }
  .o-link-icon:hover {
    cursor: pointer;
    color: #000;
  }
`;
    class LinkDisplay extends owl.Component {
        static components = { Menu };
        static template = "o-spreadsheet-LinkDisplay";
        get cell() {
            const { col, row } = this.props.cellPosition;
            const sheetId = this.env.model.getters.getActiveSheetId();
            return this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
        }
        get link() {
            if (this.cell.link) {
                return this.cell.link;
            }
            const { col, row } = this.props.cellPosition;
            throw new Error(`LinkDisplay Component can only be used with link cells. ${toXC(col, row)} is not a link.`);
        }
        getUrlRepresentation(link) {
            return urlRepresentation(link, this.env.model.getters);
        }
        openLink() {
            openLink(this.link, this.env);
        }
        edit() {
            const { col, row } = this.props.cellPosition;
            this.env.model.dispatch("OPEN_CELL_POPOVER", {
                col,
                row,
                popoverType: "LinkEditor",
            });
        }
        unlink() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const { col, row } = this.props.cellPosition;
            const style = this.env.model.getters.getCellComputedStyle({ sheetId, col, row });
            const textColor = style?.textColor === LINK_COLOR ? undefined : style?.textColor;
            this.env.model.dispatch("UPDATE_CELL", {
                col,
                row,
                sheetId,
                content: this.link.label,
                style: { ...style, textColor, underline: undefined },
            });
        }
    }
    const LinkCellPopoverBuilder = {
        onHover: (position, getters) => {
            const cell = getters.getEvaluatedCell(position);
            const shouldDisplayLink = !getters.isDashboard() && cell.link && getters.isVisibleInViewport(position);
            if (!shouldDisplayLink)
                return { isOpen: false };
            return {
                isOpen: true,
                Component: LinkDisplay,
                props: { cellPosition: position },
                cellCorner: "BottomLeft",
            };
        },
    };
    LinkDisplay.props = {
        cellPosition: Object,
        onClosed: { type: Function, optional: true },
    };

    /**
     * Tokenizer
     *
     * A tokenizer is a piece of code whose job is to transform a string into a list
     * of "tokens". For example, "(12+" is converted into:
     *   [{type: "LEFT_PAREN", value: "("},
     *    {type: "NUMBER", value: "12"},
     *    {type: "OPERATOR", value: "+"}]
     *
     * As the example shows, a tokenizer does not care about the meaning behind those
     * tokens. It only cares about the structure.
     *
     * The tokenizer is usually the first step in a compilation pipeline.  Also, it
     * is useful for the composer, which needs to be able to work with incomplete
     * formulas.
     */
    const POSTFIX_UNARY_OPERATORS = ["%"];
    const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
    function tokenize(str, locale = DEFAULT_LOCALE) {
        str = replaceSpecialSpaces(str);
        const chars = new TokenizingChars(str);
        const result = [];
        while (!chars.isOver()) {
            let token = tokenizeSpace(chars) ||
                tokenizeArgsSeparator(chars, locale) ||
                tokenizeParenthesis(chars) ||
                tokenizeOperator(chars) ||
                tokenizeString(chars) ||
                tokenizeDebugger(chars) ||
                tokenizeInvalidRange(chars) ||
                tokenizeNumber(chars, locale) ||
                tokenizeSymbol(chars);
            if (!token) {
                token = { type: "UNKNOWN", value: chars.shift() };
            }
            result.push(token);
        }
        return result;
    }
    function tokenizeDebugger(chars) {
        if (chars.current === "?") {
            chars.shift();
            return { type: "DEBUGGER", value: "?" };
        }
        return null;
    }
    const parenthesis = {
        "(": { type: "LEFT_PAREN", value: "(" },
        ")": { type: "RIGHT_PAREN", value: ")" },
    };
    function tokenizeParenthesis(chars) {
        if (chars.current === "(" || chars.current === ")") {
            const value = chars.shift();
            return parenthesis[value];
        }
        return null;
    }
    function tokenizeArgsSeparator(chars, locale) {
        if (chars.current === locale.formulaArgSeparator) {
            const value = chars.shift();
            const type = "ARG_SEPARATOR";
            return { type, value };
        }
        return null;
    }
    function tokenizeOperator(chars) {
        for (let op of OPERATORS) {
            if (chars.currentStartsWith(op)) {
                chars.advanceBy(op.length);
                return { type: "OPERATOR", value: op };
            }
        }
        return null;
    }
    const FIRST_POSSIBLE_NUMBER_CHARS = new Set("0123456789");
    function tokenizeNumber(chars, locale) {
        if (!FIRST_POSSIBLE_NUMBER_CHARS.has(chars.current) &&
            chars.current !== locale.decimalSeparator) {
            return null;
        }
        const match = chars.remaining().match(getFormulaNumberRegex(locale.decimalSeparator));
        if (match) {
            chars.advanceBy(match[0].length);
            return { type: "NUMBER", value: match[0] };
        }
        return null;
    }
    function tokenizeString(chars) {
        if (chars.current === '"') {
            const startChar = chars.shift();
            let letters = startChar;
            while (chars.current && (chars.current !== startChar || letters[letters.length - 1] === "\\")) {
                letters += chars.shift();
            }
            if (chars.current === '"') {
                letters += chars.shift();
            }
            return {
                type: "STRING",
                value: letters,
            };
        }
        return null;
    }
    const separatorRegexp = /^[\w\.!\$]+/;
    /**
     * A "Symbol" is just basically any word-like element that can appear in a
     * formula, which is not a string. So:
     *   A1
     *   SUM
     *   CEILING.MATH
     *   A$1
     *   Sheet2!A2
     *   'Sheet 2'!A2
     *
     * are examples of symbols
     */
    function tokenizeSymbol(chars) {
        let result = "";
        // there are two main cases to manage: either something which starts with
        // a ', like 'Sheet 2'A2, or a word-like element.
        if (chars.current === "'") {
            let lastChar = chars.shift();
            result += lastChar;
            while (chars.current) {
                lastChar = chars.shift();
                result += lastChar;
                if (lastChar === "'") {
                    if (chars.current && chars.current === "'") {
                        lastChar = chars.shift();
                        result += lastChar;
                    }
                    else {
                        break;
                    }
                }
            }
            if (lastChar !== "'") {
                return {
                    type: "UNKNOWN",
                    value: result,
                };
            }
        }
        const match = chars.remaining().match(separatorRegexp);
        if (match) {
            const value = match[0];
            result += value;
            chars.advanceBy(value.length);
        }
        if (result.length) {
            const value = result;
            const isReference = rangeReference.test(value);
            if (isReference) {
                return { type: "REFERENCE", value };
            }
            return { type: "SYMBOL", value };
        }
        return null;
    }
    function tokenizeSpace(chars) {
        let length = 0;
        while (chars.current === NEWLINE) {
            length++;
            chars.shift();
        }
        if (length) {
            return { type: "SPACE", value: NEWLINE.repeat(length) };
        }
        while (chars.current === " ") {
            length++;
            chars.shift();
        }
        if (length) {
            return { type: "SPACE", value: " ".repeat(length) };
        }
        return null;
    }
    function tokenizeInvalidRange(chars) {
        if (chars.currentStartsWith(INCORRECT_RANGE_STRING)) {
            chars.advanceBy(INCORRECT_RANGE_STRING.length);
            return { type: "INVALID_REFERENCE", value: INCORRECT_RANGE_STRING };
        }
        return null;
    }
    class TokenizingChars {
        text;
        currentIndex = 0;
        current;
        constructor(text) {
            this.text = text;
            this.current = text[0];
        }
        shift() {
            const current = this.current;
            const next = this.text[++this.currentIndex];
            this.current = next;
            return current;
        }
        advanceBy(length) {
            this.currentIndex += length;
            this.current = this.text[this.currentIndex];
        }
        isOver() {
            return this.currentIndex >= this.text.length;
        }
        remaining() {
            return this.text.substring(this.currentIndex);
        }
        currentStartsWith(str) {
            if (this.current !== str[0]) {
                return false;
            }
            for (let j = 1; j < str.length; j++) {
                if (this.text[this.currentIndex + j] !== str[j]) {
                    return false;
                }
            }
            return true;
        }
    }

    function isValidLocale(locale) {
        if (!(locale &&
            typeof locale === "object" &&
            typeof locale.name === "string" &&
            typeof locale.code === "string" &&
            typeof locale.thousandsSeparator === "string" &&
            typeof locale.decimalSeparator === "string" &&
            typeof locale.dateFormat === "string" &&
            typeof locale.timeFormat === "string" &&
            typeof locale.formulaArgSeparator === "string")) {
            return false;
        }
        if (!Object.values(locale).every((v) => v)) {
            return false;
        }
        if (locale.formulaArgSeparator === locale.decimalSeparator) {
            return false;
        }
        if (locale.thousandsSeparator === locale.decimalSeparator) {
            return false;
        }
        try {
            formatValue(1, { locale, format: "#,##0.00" });
            formatValue(1, { locale, format: locale.dateFormat });
            formatValue(1, { locale, format: locale.timeFormat });
        }
        catch {
            return false;
        }
        return true;
    }
    /**
     * Change a content string from the given locale to its canonical form (en_US locale). Don't convert date string.
     *
     * @example
     * canonicalizeNumberContent("=SUM(1,5; 02/12/2012)", FR_LOCALE) // "=SUM(1.5, 02/12/2012)"
     * canonicalizeNumberContent("125,9", FR_LOCALE) // "125.9"
     * canonicalizeNumberContent("02/12/2012", FR_LOCALE) // "02/12/2012"
     */
    function canonicalizeNumberContent(content, locale) {
        return content.startsWith("=")
            ? canonicalizeFormula$1(content, locale)
            : canonicalizeNumberLiteral(content, locale);
    }
    /**
     * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
     * This is destructive and won't preserve the original format.
     *
     * @example
     * canonicalizeContent("=SUM(1,5; 5)", FR_LOCALE) // "=SUM(1.5, 5)"
     * canonicalizeContent("125,9", FR_LOCALE) // "125.9"
     * canonicalizeContent("02/12/2012", FR_LOCALE) // "12/02/2012"
     * canonicalizeContent("02-12-2012", FR_LOCALE) // "12/02/2012"
     */
    function canonicalizeContent(content, locale) {
        return content.startsWith("=")
            ? canonicalizeFormula$1(content, locale)
            : canonicalizeLiteral(content, locale);
    }
    /**
     * Change a content string from its canonical form (en_US locale) to the given locale. Also convert date string.
     *
     * @example
     * localizeContent("=SUM(1.5, 5)", FR_LOCALE) // "=SUM(1,5; 5)"
     * localizeContent("125.9", FR_LOCALE) // "125,9"
     * localizeContent("12/02/2012", FR_LOCALE) // "02/12/2012"
     */
    function localizeContent(content, locale) {
        return content.startsWith("=")
            ? localizeFormula(content, locale)
            : localizeLiteral(content, locale);
    }
    /** Change a formula to its canonical form (en_US locale) */
    function canonicalizeFormula$1(formula, locale) {
        return _localizeFormula$1(formula, locale, DEFAULT_LOCALE);
    }
    /** Change a formula from the canonical form to the given locale */
    function localizeFormula(formula, locale) {
        return _localizeFormula$1(formula, DEFAULT_LOCALE, locale);
    }
    function _localizeFormula$1(formula, fromLocale, toLocale) {
        if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
            fromLocale.decimalSeparator === toLocale.decimalSeparator) {
            return formula;
        }
        const tokens = tokenize(formula, fromLocale);
        let localizedFormula = "";
        for (const token of tokens) {
            if (token.type === "NUMBER") {
                localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
            }
            else if (token.type === "ARG_SEPARATOR") {
                localizedFormula += toLocale.formulaArgSeparator;
            }
            else {
                localizedFormula += token.value;
            }
        }
        return localizedFormula;
    }
    /**
     * Change a literal string from the given locale to its canonical form (en_US locale). Don't convert date string.
     *
     * @example
     * canonicalizeNumberLiteral("125,9", FR_LOCALE) // "125.9"
     * canonicalizeNumberLiteral("02/12/2012", FR_LOCALE) // "02/12/2012"
     */
    function canonicalizeNumberLiteral(content, locale) {
        if (locale.decimalSeparator === "." || !isNumber(content, locale)) {
            return content;
        }
        return content.replace(locale.thousandsSeparator, "").replace(locale.decimalSeparator, ".");
    }
    /**
     * Change a content string from the given locale to its canonical form (en_US locale). Also convert date string.
     * This is destructive and won't preserve the original format.
     *
     * @example
     * canonicalizeLiteral("125,9", FR_LOCALE) // "125.9"
     * canonicalizeLiteral("02/12/2012", FR_LOCALE) // "12/02/2012"
     * canonicalizeLiteral("02-12-2012", FR_LOCALE) // "12/02/2012"
     */
    function canonicalizeLiteral(content, locale) {
        if (isDateTime(content, locale)) {
            const dateNumber = toNumber(content, locale);
            let format = DEFAULT_LOCALE.dateFormat;
            if (!Number.isInteger(dateNumber)) {
                format += " " + DEFAULT_LOCALE.timeFormat;
            }
            return formatValue(dateNumber, { locale: DEFAULT_LOCALE, format });
        }
        return canonicalizeNumberLiteral(content, locale);
    }
    /**
     * Change a literal string from its canonical form (en_US locale) to the given locale. Don't convert date string.
     * This is destructive and won't preserve the original format.
     *
     * @example
     * localizeNumberLiteral("125.9", FR_LOCALE) // "125,9"
     * localizeNumberLiteral("12/02/2012", FR_LOCALE) // "12/02/2012"
     * localizeNumberLiteral("12-02-2012", FR_LOCALE) // "12/02/2012"
     */
    function localizeNumberLiteral(literal, locale) {
        if (locale.decimalSeparator === "." || !isNumber(literal, DEFAULT_LOCALE)) {
            return literal;
        }
        const decimalNumberRegex = getDecimalNumberRegex(DEFAULT_LOCALE);
        const localized = literal.replace(decimalNumberRegex, (match) => {
            return match.replace(".", locale.decimalSeparator);
        });
        return localized;
    }
    /**
     * Change a literal string from its canonical form (en_US locale) to the given locale. Also convert date string.
     *
     * @example
     * localizeLiteral("125.9", FR_LOCALE) // "125,9"
     * localizeLiteral("12/02/2012", FR_LOCALE) // "02/12/2012"
     */
    function localizeLiteral(literal, locale) {
        if (isDateTime(literal, DEFAULT_LOCALE)) {
            const dateNumber = toNumber(literal, DEFAULT_LOCALE);
            let format = locale.dateFormat;
            if (!Number.isInteger(dateNumber)) {
                format += " " + locale.timeFormat;
            }
            return formatValue(dateNumber, { locale, format });
        }
        return localizeNumberLiteral(literal, locale);
    }
    function canonicalizeCFRule(cf, locale) {
        return changeCFRuleLocale(cf, (content) => canonicalizeContent(content, locale));
    }
    function localizeCFRule(cf, locale) {
        return changeCFRuleLocale(cf, (content) => localizeContent(content, locale));
    }
    function localizeDataValidationRule(rule, locale) {
        const localizedDVRule = deepCopy(rule);
        localizedDVRule.criterion.values = localizedDVRule.criterion.values.map((content) => localizeContent(content, locale));
        return localizedDVRule;
    }
    function changeCFRuleLocale(rule, changeContentLocale) {
        rule = deepCopy(rule);
        switch (rule.type) {
            case "CellIsRule":
                // Only change value for number operators
                switch (rule.operator) {
                    case "Between":
                    case "NotBetween":
                    case "Equal":
                    case "NotEqual":
                    case "GreaterThan":
                    case "GreaterThanOrEqual":
                    case "LessThan":
                    case "LessThanOrEqual":
                        rule.values = rule.values.map((v) => changeContentLocale(v));
                        return rule;
                    case "BeginsWith":
                    case "ContainsText":
                    case "EndsWith":
                    case "NotContains":
                    case "IsEmpty":
                    case "IsNotEmpty":
                        return rule;
                }
                break;
            case "ColorScaleRule":
                rule.minimum = changeCFRuleThresholdLocale(rule.minimum, changeContentLocale);
                rule.maximum = changeCFRuleThresholdLocale(rule.maximum, changeContentLocale);
                if (rule.midpoint) {
                    rule.midpoint = changeCFRuleThresholdLocale(rule.midpoint, changeContentLocale);
                }
                return rule;
            case "IconSetRule":
                rule.lowerInflectionPoint.value = changeContentLocale(rule.lowerInflectionPoint.value);
                rule.upperInflectionPoint.value = changeContentLocale(rule.upperInflectionPoint.value);
                return rule;
        }
    }
    function changeCFRuleThresholdLocale(threshold, changeContentLocale) {
        if (!threshold?.value) {
            return threshold;
        }
        const value = threshold.type === "formula" ? "=" + threshold.value : threshold.value;
        const modified = changeContentLocale(value);
        const newValue = threshold.type === "formula" ? modified.slice(1) : modified;
        return { ...threshold, value: newValue };
    }
    function getDateTimeFormat(locale) {
        return locale.dateFormat + " " + locale.timeFormat;
    }

    const linkSheet = {
        name: _t("Link sheet"),
        children: [
            (env) => {
                const sheets = env.model.getters
                    .getSheetIds()
                    .map((sheetId) => env.model.getters.getSheet(sheetId));
                return sheets.map((sheet) => ({
                    id: sheet.id,
                    name: sheet.name,
                    execute: () => markdownLink(sheet.name, buildSheetLink(sheet.id)),
                }));
            },
        ],
    };
    const deleteSheet = {
        name: _t("Delete"),
        isVisible: (env) => {
            return env.model.getters.getSheetIds().length > 1;
        },
        execute: (env) => env.askConfirmation(_t("Are you sure you want to delete this sheet?"), () => {
            env.model.dispatch("DELETE_SHEET", { sheetId: env.model.getters.getActiveSheetId() });
        }),
    };
    const duplicateSheet = {
        name: _t("Duplicate"),
        execute: (env) => {
            const sheetIdFrom = env.model.getters.getActiveSheetId();
            const sheetIdTo = env.model.uuidGenerator.uuidv4();
            env.model.dispatch("DUPLICATE_SHEET", {
                sheetId: sheetIdFrom,
                sheetIdTo,
            });
            env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom, sheetIdTo });
        },
    };
    const renameSheet = (args) => {
        return {
            name: _t("Rename"),
            execute: args.renameSheetCallback,
        };
    };
    const sheetMoveRight = {
        name: _t("Move right"),
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            const sheetIds = env.model.getters.getVisibleSheetIds();
            return sheetIds.indexOf(sheetId) !== sheetIds.length - 1;
        },
        execute: (env) => env.model.dispatch("MOVE_SHEET", {
            sheetId: env.model.getters.getActiveSheetId(),
            delta: 1,
        }),
    };
    const sheetMoveLeft = {
        name: _t("Move left"),
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            return env.model.getters.getVisibleSheetIds()[0] !== sheetId;
        },
        execute: (env) => env.model.dispatch("MOVE_SHEET", {
            sheetId: env.model.getters.getActiveSheetId(),
            delta: -1,
        }),
    };
    const hideSheet = {
        name: _t("Hide sheet"),
        isVisible: (env) => env.model.getters.getVisibleSheetIds().length !== 1,
        execute: (env) => env.model.dispatch("HIDE_SHEET", { sheetId: env.model.getters.getActiveSheetId() }),
    };

    /**
     * The class Registry is extended in order to add the function addChild
     *
     */
    class MenuItemRegistry extends Registry {
        /**
         * @override
         */
        add(key, value) {
            if (value.id === undefined) {
                value.id = key;
            }
            this.content[key] = value;
            return this;
        }
        /**
         * Add a subitem to an existing item
         * @param path Path of items to add this subitem
         * @param value Subitem to add
         */
        addChild(key, path, value) {
            if (typeof value !== "function" && value.id === undefined) {
                value.id = key;
            }
            const root = path.splice(0, 1)[0];
            let node = this.content[root];
            if (!node) {
                throw new Error(`Path ${root + ":" + path.join(":")} not found`);
            }
            for (let p of path) {
                const children = node.children;
                if (!children || typeof children === "function") {
                    throw new Error(`${p} is either not a node or it's dynamically computed`);
                }
                node = children.find((elt) => elt.id === p);
                if (!node) {
                    throw new Error(`Path ${root + ":" + path.join(":")} not found`);
                }
            }
            if (!node.children) {
                node.children = [];
            }
            node.children.push(value);
            return this;
        }
        getMenuItems() {
            return createActions(this.getAll());
        }
    }

    //------------------------------------------------------------------------------
    // Link Menu Registry
    //------------------------------------------------------------------------------
    const linkMenuRegistry = new MenuItemRegistry();
    linkMenuRegistry.add("sheet", {
        ...linkSheet,
        sequence: 10,
    });

    const MENU_OFFSET_X = 320;
    const MENU_OFFSET_Y = 100;
    const PADDING = 12;
    const LINK_EDITOR_WIDTH = 340;
    const LINK_EDITOR_HEIGHT = 165;
    css /* scss */ `
  .o-link-editor {
    font-size: 13px;
    background-color: white;
    box-shadow: 0 1px 4px 3px rgba(60, 64, 67, 0.15);
    padding: ${PADDING}px;
    display: flex;
    flex-direction: column;
    border-radius: 4px;
    height: ${LINK_EDITOR_HEIGHT}px;
    width: ${LINK_EDITOR_WIDTH}px;

    .o-section {
      .o-section-title {
        font-weight: bold;
        color: dimgrey;
        margin-bottom: 5px;
      }
    }
    .o-buttons {
      padding-left: 16px;
      padding-top: 16px;
      padding-bottom: 16px;
      text-align: right;
    }
    input {
      box-sizing: border-box;
      width: 100%;
      border-radius: 4px;
      padding: 4px 23px 4px 10px;
      border: none;
      height: 24px;
      border: 1px solid lightgrey;
    }
    .o-link-url {
      position: relative;
      flex-grow: 1;
      button {
        position: absolute;
        right: 0px;
        top: 0px;
        border: none;
        height: 20px;
        width: 20px;
        background-color: #fff;
        margin: 2px 3px 1px 0px;
        padding: 0px 1px 0px 0px;
      }
      button:hover {
        cursor: pointer;
      }
    }
  }
`;
    class LinkEditor extends owl.Component {
        static template = "o-spreadsheet-LinkEditor";
        static components = { Menu };
        menuItems = linkMenuRegistry.getMenuItems();
        link = owl.useState(this.defaultState);
        menu = owl.useState({
            isOpen: false,
        });
        linkEditorRef = owl.useRef("linkEditor");
        position = useAbsoluteBoundingRect(this.linkEditorRef);
        urlInput = owl.useRef("urlInput");
        setup() {
            owl.onMounted(() => this.urlInput.el?.focus());
        }
        get defaultState() {
            const { col, row } = this.props.cellPosition;
            const sheetId = this.env.model.getters.getActiveSheetId();
            const cell = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
            if (cell.link) {
                return {
                    url: cell.link.url,
                    label: cell.formattedValue,
                    isUrlEditable: cell.link.isUrlEditable,
                };
            }
            return {
                label: cell.formattedValue,
                url: "",
                isUrlEditable: true,
            };
        }
        get menuPosition() {
            return {
                x: this.position.x + MENU_OFFSET_X - PADDING - 2,
                y: this.position.y + MENU_OFFSET_Y,
            };
        }
        onSpecialLink(ev) {
            const { detail: markdownLink } = ev;
            const link = detectLink(markdownLink);
            if (!link) {
                return;
            }
            this.link.url = link.url;
            this.link.label = link.label;
            this.link.isUrlEditable = link.isUrlEditable;
        }
        getUrlRepresentation(link) {
            return urlRepresentation(link, this.env.model.getters);
        }
        openMenu() {
            this.menu.isOpen = true;
        }
        removeLink() {
            this.link.url = "";
            this.link.isUrlEditable = true;
        }
        save() {
            const { col, row } = this.props.cellPosition;
            const locale = this.env.model.getters.getLocale();
            const label = this.link.label
                ? canonicalizeNumberContent(this.link.label, locale)
                : this.link.url;
            this.env.model.dispatch("UPDATE_CELL", {
                col: col,
                row: row,
                sheetId: this.env.model.getters.getActiveSheetId(),
                content: markdownLink(label, this.link.url),
            });
            this.props.onClosed?.();
        }
        cancel() {
            this.props.onClosed?.();
        }
        onKeyDown(ev) {
            switch (ev.key) {
                case "Enter":
                    if (this.link.url) {
                        this.save();
                    }
                    ev.stopPropagation();
                    break;
                case "Escape":
                    this.cancel();
                    ev.stopPropagation();
                    break;
            }
        }
    }
    const LinkEditorPopoverBuilder = {
        onOpen: (position, getters) => {
            return {
                isOpen: true,
                props: { cellPosition: position },
                Component: LinkEditor,
                cellCorner: "BottomLeft",
            };
        },
    };
    LinkEditor.props = {
        cellPosition: Object,
        onClosed: { type: Function, optional: true },
    };

    const cellPopoverRegistry = new Registry();
    cellPopoverRegistry
        .add("ErrorToolTip", ErrorToolTipPopoverBuilder)
        .add("LinkCell", LinkCellPopoverBuilder)
        .add("LinkEditor", LinkEditorPopoverBuilder)
        .add("FilterMenu", FilterMenuPopoverBuilder);

    /**
     * Convert a JS color hexadecimal to an excel compatible color.
     *
     * In Excel the color don't start with a '#' and the format is AARRGGBB instead of RRGGBBAA
     */
    function toXlsxHexColor(color) {
        color = toHex(color).replace("#", "");
        // alpha channel goes first
        if (color.length === 8) {
            return color.slice(6) + color.slice(0, 6);
        }
        return color;
    }

    const PasteInteractiveContent = {
        wrongPasteSelection: _t("This operation is not allowed with multiple selections."),
        willRemoveExistingMerge: _t("This operation is not possible due to a merge. Please remove the merges first than try again."),
        wrongFigurePasteOption: _t("Cannot do a special paste of a figure."),
        frozenPaneOverlap: _t("This operation is not allowed due to an overlapping frozen pane."),
    };
    function handlePasteResult(env, result) {
        if (!result.isSuccessful) {
            if (result.reasons.includes("WrongPasteSelection" /* CommandResult.WrongPasteSelection */)) {
                env.raiseError(PasteInteractiveContent.wrongPasteSelection);
            }
            else if (result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
                env.raiseError(PasteInteractiveContent.willRemoveExistingMerge);
            }
            else if (result.reasons.includes("WrongFigurePasteOption" /* CommandResult.WrongFigurePasteOption */)) {
                env.raiseError(PasteInteractiveContent.wrongFigurePasteOption);
            }
            else if (result.reasons.includes("FrozenPaneOverlap" /* CommandResult.FrozenPaneOverlap */)) {
                env.raiseError(PasteInteractiveContent.frozenPaneOverlap);
            }
        }
    }
    function interactivePaste(env, target, pasteOption) {
        const result = env.model.dispatch("PASTE", { target, pasteOption });
        handlePasteResult(env, result);
    }
    function interactivePasteFromOS(env, target, text, pasteOption) {
        const result = env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", { target, text, pasteOption });
        handlePasteResult(env, result);
    }

    const CfTerms = {
        Errors: {
            ["InvalidRange" /* CommandResult.InvalidRange */]: _t("The range is invalid"),
            ["FirstArgMissing" /* CommandResult.FirstArgMissing */]: _t("The argument is missing. Please provide a value"),
            ["SecondArgMissing" /* CommandResult.SecondArgMissing */]: _t("The second argument is missing. Please provide a value"),
            ["MinNaN" /* CommandResult.MinNaN */]: _t("The minpoint must be a number"),
            ["MidNaN" /* CommandResult.MidNaN */]: _t("The midpoint must be a number"),
            ["MaxNaN" /* CommandResult.MaxNaN */]: _t("The maxpoint must be a number"),
            ["ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */]: _t("The first value must be a number"),
            ["ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */]: _t("The second value must be a number"),
            ["MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */]: _t("Minimum must be smaller then Maximum"),
            ["MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */]: _t("Minimum must be smaller then Midpoint"),
            ["MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */]: _t("Midpoint must be smaller then Maximum"),
            ["LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */]: _t("Lower inflection point must be smaller than upper inflection point"),
            ["MinInvalidFormula" /* CommandResult.MinInvalidFormula */]: _t("Invalid Minpoint formula"),
            ["MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */]: _t("Invalid Maxpoint formula"),
            ["MidInvalidFormula" /* CommandResult.MidInvalidFormula */]: _t("Invalid Midpoint formula"),
            ["ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */]: _t("Invalid upper inflection point formula"),
            ["ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */]: _t("Invalid lower inflection point formula"),
            ["EmptyRange" /* CommandResult.EmptyRange */]: _t("A range needs to be defined"),
            Unexpected: _t("The rule is invalid for an unknown reason"),
        },
        ColorScale: _t("Color scale"),
        IconSet: _t("Icon set"),
    };
    const CellIsOperators = {
        IsEmpty: _t("Is empty"),
        IsNotEmpty: _t("Is not empty"),
        ContainsText: _t("Contains"),
        NotContains: _t("Does not contain"),
        BeginsWith: _t("Starts with"),
        EndsWith: _t("Ends with"),
        Equal: _t("Is equal to"),
        NotEqual: _t("Is not equal to"),
        GreaterThan: _t("Is greater than"),
        GreaterThanOrEqual: _t("Is greater than or equal to"),
        LessThan: _t("Is less than"),
        LessThanOrEqual: _t("Is less than or equal to"),
        Between: _t("Is between"),
        NotBetween: _t("Is not between"),
    };
    const ChartTerms = {
        Series: _t("Series"),
        Errors: {
            Unexpected: _t("The chart definition is invalid for an unknown reason"),
            // BASIC CHART ERRORS (LINE | BAR | PIE)
            ["InvalidDataSet" /* CommandResult.InvalidDataSet */]: _t("The dataset is invalid"),
            ["InvalidLabelRange" /* CommandResult.InvalidLabelRange */]: _t("Labels are invalid"),
            // SCORECARD CHART ERRORS
            ["InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */]: _t("The key value is invalid"),
            ["InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */]: _t("The baseline value is invalid"),
            // GAUGE CHART ERRORS
            ["InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */]: _t("The data range is invalid"),
            ["EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */]: _t("A minimum range limit value is needed"),
            ["GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */]: _t("The minimum range limit value must be a number"),
            ["EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */]: _t("A maximum range limit value is needed"),
            ["GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */]: _t("The maximum range limit value must be a number"),
            ["GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */]: _t("Minimum range limit must be smaller than maximum range limit"),
            ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
            ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
        },
    };
    const CustomCurrencyTerms = {
        Custom: _t("Custom"),
    };
    const MergeErrorMessage = _t("Merged cells are preventing this operation. Unmerge those cells and try again.");
    const SplitToColumnsTerms = {
        Errors: {
            Unexpected: _t("Cannot split the selection for an unknown reason"),
            ["NoSplitSeparatorInSelection" /* CommandResult.NoSplitSeparatorInSelection */]: _t("There is no match for the selected separator in the selection"),
            ["MoreThanOneColumnSelected" /* CommandResult.MoreThanOneColumnSelected */]: _t("Only a selection from a single column can be split"),
            ["SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */]: _t("Splitting will overwrite existing content"),
        },
    };
    const RemoveDuplicateTerms = {
        Errors: {
            Unexpected: _t("Cannot remove duplicates for an unknown reason"),
            ["MoreThanOneRangeSelected" /* CommandResult.MoreThanOneRangeSelected */]: _t("Please select only one range of cells"),
            ["EmptyTarget" /* CommandResult.EmptyTarget */]: _t("Please select a range of cells containing values."),
            ["NoColumnsProvided" /* CommandResult.NoColumnsProvided */]: _t("Please select at latest one column to analyze."),
            //TODO: Remove it when accept to copy and paste merge cells
            ["WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */]: PasteInteractiveContent.willRemoveExistingMerge,
        },
    };
    const DVTerms = {
        DateIs: {
            today: _t("today"),
            yesterday: _t("yesterday"),
            tomorrow: _t("tomorrow"),
            lastWeek: _t("in the past week"),
            lastMonth: _t("in the past month"),
            lastYear: _t("in the past year"),
        },
        DateIsBefore: {
            today: _t("today"),
            yesterday: _t("yesterday"),
            tomorrow: _t("tomorrow"),
            lastWeek: _t("one week ago"),
            lastMonth: _t("one month ago"),
            lastYear: _t("one year ago"),
        },
        CriterionError: {
            notEmptyValue: _t("The value must not be empty"),
            numberValue: _t("The value must be a number"),
            dateValue: _t("The value must be a date"),
            validRange: _t("The value must be a valid range"),
        },
    };

    /**
     * This file contains helpers that are common to different runtime charts (mainly
     * line, bar and pie charts)
     */
    /**
     * Get the data from a dataSet
     */
    function getData(getters, ds) {
        if (ds.dataRange) {
            const labelCellZone = ds.labelCell ? [zoneToXc(ds.labelCell.zone)] : [];
            const dataXC = recomputeZones([zoneToXc(ds.dataRange.zone)], labelCellZone)[0];
            if (dataXC === undefined) {
                return [];
            }
            const dataRange = getters.getRangeFromSheetXC(ds.dataRange.sheetId, dataXC);
            return getters.getRangeValues(dataRange).map((value) => (value === "" ? undefined : value));
        }
        return [];
    }
    function filterEmptyDataPoints(labels, datasets) {
        const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
        const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
            const label = labels[dataPointIndex];
            const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
            return label || values.some((value) => value === 0 || Boolean(value));
        });
        return {
            labels: dataPointsIndexes.map((i) => labels[i] || ""),
            dataSetsValues: datasets.map((dataset) => ({
                ...dataset,
                data: dataPointsIndexes.map((i) => dataset.data[i]),
            })),
        };
    }
    /**
     * Aggregates data based on labels
     */
    function aggregateDataForLabels(labels, datasets) {
        const parseNumber = (value) => (typeof value === "number" ? value : 0);
        const labelSet = new Set(labels);
        const labelMap = {};
        labelSet.forEach((label) => {
            labelMap[label] = new Array(datasets.length).fill(0);
        });
        for (const indexOfLabel of range(0, labels.length)) {
            const label = labels[indexOfLabel];
            for (const indexOfDataset of range(0, datasets.length)) {
                labelMap[label][indexOfDataset] += parseNumber(datasets[indexOfDataset].data[indexOfLabel]);
            }
        }
        return {
            labels: Object.keys(labelMap),
            dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
                ...dataset,
                data: Object.values(labelMap).map((dataOfLabel) => dataOfLabel[indexOfDataset]),
            })),
        };
    }
    function truncateLabel(label) {
        if (!label) {
            return "";
        }
        if (label.length > MAX_CHAR_LABEL) {
            return label.substring(0, MAX_CHAR_LABEL) + "…";
        }
        return label;
    }
    /**
     * Get a default chart js configuration
     */
    function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale }) {
        const options = {
            // https://www.chartjs.org/docs/latest/general/responsive.html
            responsive: true,
            maintainAspectRatio: false,
            layout: {
                padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
            },
            elements: {
                line: {
                    fill: false, // do not fill the area under line charts
                },
                point: {
                    hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
                },
            },
            animation: false,
            plugins: {
                title: {
                    display: !!chart.title,
                    text: _t(chart.title),
                    color: fontColor,
                    font: { size: 22, weight: "normal" },
                },
                legend: {
                    // Disable default legend onClick (show/hide dataset), to allow us to set a global onClick on the chart container.
                    // If we want to re-enable this in the future, we need to override the default onClick to stop the event propagation
                    onClick: () => { },
                },
                tooltip: {
                    callbacks: {
                        label: function (tooltipItem) {
                            const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
                            // tooltipItem.parsed.y can be an object or a number for pie charts
                            const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;
                            const toolTipFormat = !format && yLabel > 1000 ? "#,##" : format;
                            const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
                            return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
                        },
                    },
                },
            },
        };
        return {
            type: chart.type,
            options,
            data: {
                labels: labels.map(truncateLabel),
                datasets: [],
            },
            plugins: [],
        };
    }
    function getChartLabelFormat(getters, range) {
        if (!range)
            return undefined;
        return getters.getEvaluatedCell({
            sheetId: range.sheetId,
            col: range.zone.left,
            row: range.zone.top,
        }).format;
    }
    function getChartLabelValues(getters, dataSets, labelRange) {
        let labels = { values: [], formattedValues: [] };
        if (labelRange) {
            if (!labelRange.invalidXc && !labelRange.invalidSheetName) {
                labels = {
                    formattedValues: getters.getRangeFormattedValues(labelRange),
                    values: getters.getRangeValues(labelRange).map((val) => String(val)),
                };
            }
        }
        else if (dataSets.length === 1) {
            for (let i = 0; i < getData(getters, dataSets[0]).length; i++) {
                labels.formattedValues.push("");
                labels.values.push("");
            }
        }
        else {
            if (dataSets[0]) {
                const ranges = getData(getters, dataSets[0]);
                labels = {
                    formattedValues: range(0, ranges.length).map((r) => r.toString()),
                    values: labels.formattedValues,
                };
            }
        }
        return labels;
    }
    /**
     * Get the format to apply to the the dataset values. This format is defined as the first format
     * found in the dataset ranges that isn't a date format.
     */
    function getChartDatasetFormat(getters, dataSets) {
        for (const ds of dataSets) {
            const formatsInDataset = getters.getRangeFormats(ds.dataRange);
            const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
            if (format)
                return format;
        }
        return undefined;
    }
    function getChartDatasetValues(getters, dataSets) {
        const datasetValues = [];
        for (const [dsIndex, ds] of Object.entries(dataSets)) {
            let label;
            if (ds.labelCell) {
                const labelRange = ds.labelCell;
                const cell = labelRange
                    ? getters.getEvaluatedCell({
                        sheetId: labelRange.sheetId,
                        col: labelRange.zone.left,
                        row: labelRange.zone.top,
                    })
                    : undefined;
                label =
                    cell && labelRange
                        ? truncateLabel(cell.formattedValue)
                        : (label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`);
            }
            else {
                label = label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`;
            }
            let data = ds.dataRange ? getData(getters, ds) : [];
            datasetValues.push({ data, label });
        }
        return datasetValues;
    }
    /** See https://www.chartjs.org/docs/latest/charts/area.html#filling-modes */
    function getFillingMode(index) {
        if (index === 0) {
            return "origin";
        }
        else {
            return index - 1;
        }
    }
    function chartToImage(runtime, figure, type) {
        // wrap the canvas in a div with a fixed size because chart.js would
        // fill the whole page otherwise
        const div = document.createElement("div");
        div.style.width = `${figure.width}px`;
        div.style.height = `${figure.height}px`;
        const canvas = document.createElement("canvas");
        div.append(canvas);
        canvas.setAttribute("width", figure.width.toString());
        canvas.setAttribute("height", figure.height.toString());
        // we have to add the canvas to the DOM otherwise it won't be rendered
        document.body.append(div);
        if ("chartJsConfig" in runtime) {
            runtime.chartJsConfig.plugins = [backgroundColorChartJSPlugin];
            // @ts-ignore
            const chart = new window.Chart(canvas, runtime.chartJsConfig);
            const imgContent = chart.toBase64Image();
            chart.destroy();
            div.remove();
            return imgContent;
        }
        else if (type === "scorecard") {
            const design = getScorecardConfiguration(figure, runtime);
            drawScoreChart(design, canvas);
            const imgContent = canvas.toDataURL();
            div.remove();
            return imgContent;
        }
        return "";
    }
    /**
     * Custom chart.js plugin to set the background color of the canvas
     * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
     */
    const backgroundColorChartJSPlugin = {
        id: "customCanvasBackgroundColor",
        beforeDraw: (chart) => {
            const { ctx } = chart;
            ctx.save();
            ctx.globalCompositeOperation = "destination-over";
            ctx.fillStyle = "#ffffff";
            ctx.fillRect(0, 0, chart.width, chart.height);
            ctx.restore();
        },
    };

    class BarChart extends AbstractChart {
        dataSets;
        labelRange;
        background;
        verticalAxisPosition;
        legendPosition;
        stacked;
        aggregated;
        type = "bar";
        dataSetsHaveTitle;
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
            this.labelRange = createRange(getters, sheetId, definition.labelRange);
            this.background = definition.background;
            this.verticalAxisPosition = definition.verticalAxisPosition;
            this.legendPosition = definition.legendPosition;
            this.stacked = definition.stacked;
            this.aggregated = definition.aggregated;
            this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        }
        static transformDefinition(definition, executed) {
            return transformChartDefinitionWithDataSetsWithZone(definition, executed);
        }
        static validateChartDefinition(validator, definition) {
            return validator.checkValidations(definition, checkDataset, checkLabelRange);
        }
        static getDefinitionFromContextCreation(context) {
            return {
                background: context.background,
                dataSets: context.range ? context.range : [],
                dataSetsHaveTitle: false,
                stacked: false,
                aggregated: false,
                legendPosition: "top",
                title: context.title || "",
                type: "bar",
                verticalAxisPosition: "left",
                labelRange: context.auxiliaryRange || undefined,
            };
        }
        getContextCreation() {
            return {
                background: this.background,
                title: this.title,
                range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
                auxiliaryRange: this.labelRange
                    ? this.getters.getRangeString(this.labelRange, this.sheetId)
                    : undefined,
            };
        }
        copyForSheetId(sheetId) {
            const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
            const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
            const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
            return new BarChart(definition, sheetId, this.getters);
        }
        copyInSheetId(sheetId) {
            const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
            return new BarChart(definition, sheetId, this.getters);
        }
        getDefinition() {
            return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
        }
        getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
            return {
                type: "bar",
                dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
                background: this.background,
                dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
                legendPosition: this.legendPosition,
                verticalAxisPosition: this.verticalAxisPosition,
                labelRange: labelRange
                    ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                    : undefined,
                title: this.title,
                stacked: this.stacked,
                aggregated: this.aggregated,
            };
        }
        getDefinitionForExcel() {
            // Excel does not support aggregating labels
            if (this.aggregated)
                return undefined;
            const dataSets = this.dataSets
                .map((ds) => toExcelDataset(this.getters, ds))
                .filter((ds) => ds.range !== "" && ds.range !== INCORRECT_RANGE_STRING);
            const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
            return {
                ...this.getDefinition(),
                backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
                fontColor: toXlsxHexColor(chartFontColor(this.background)),
                dataSets,
                labelRange,
            };
        }
        updateRanges(applyChange) {
            const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
            if (!isStale) {
                return this;
            }
            const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
            return new BarChart(definition, this.sheetId, this.getters);
        }
    }
    function getBarConfiguration(chart, labels, localeFormat) {
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
        const legend = {
            labels: { color: fontColor },
        };
        if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
            legend.display = false;
        }
        else {
            legend.position = chart.legendPosition;
        }
        config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
        config.options.layout = {
            padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
        };
        config.options.scales = {
            x: {
                ticks: {
                    padding: 5,
                    color: fontColor,
                },
            },
            y: {
                position: chart.verticalAxisPosition,
                beginAtZero: true,
                ticks: {
                    color: fontColor,
                    callback: (value) => {
                        value = Number(value);
                        if (isNaN(value))
                            return value;
                        const { locale, format } = localeFormat;
                        return formatValue(value, { locale, format: !format && value > 1000 ? "#,##" : format });
                    },
                },
            },
        };
        if (chart.stacked) {
            // @ts-ignore chart.js type is broken
            config.options.scales.x.stacked = true;
            // @ts-ignore chart.js type is broken
            config.options.scales.y.stacked = true;
        }
        return config;
    }
    function createBarChartRuntime(chart, getters) {
        const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
        let labels = labelValues.formattedValues;
        let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
        if (chart.dataSetsHaveTitle &&
            dataSetsValues[0] &&
            labels.length > dataSetsValues[0].data.length) {
            labels.shift();
        }
        ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
        if (chart.aggregated) {
            ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
        }
        const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
        const locale = getters.getLocale();
        const config = getBarConfiguration(chart, labels, { format: dataSetFormat, locale });
        const colors = new ChartColors();
        for (let { label, data } of dataSetsValues) {
            const color = colors.next();
            const dataset = {
                label,
                data,
                borderColor: color,
                backgroundColor: color,
            };
            config.data.datasets.push(dataset);
        }
        return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
    }

    function isDataRangeValid(definition) {
        return definition.dataRange && !rangeReference.test(definition.dataRange)
            ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
            : "Success" /* CommandResult.Success */;
    }
    function checkRangeLimits(check, batchValidations) {
        return batchValidations((definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.rangeMin, "rangeMin");
            }
            return "Success" /* CommandResult.Success */;
        }, (definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.rangeMax, "rangeMax");
            }
            return "Success" /* CommandResult.Success */;
        });
    }
    function checkInflectionPointsValue(check, batchValidations) {
        return batchValidations((definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.lowerInflectionPoint.value, "lowerInflectionPointValue");
            }
            return "Success" /* CommandResult.Success */;
        }, (definition) => {
            if (definition.sectionRule) {
                return check(definition.sectionRule.upperInflectionPoint.value, "upperInflectionPointValue");
            }
            return "Success" /* CommandResult.Success */;
        });
    }
    function checkRangeMinBiggerThanRangeMax(definition) {
        if (definition.sectionRule) {
            if (Number(definition.sectionRule.rangeMin) >= Number(definition.sectionRule.rangeMax)) {
                return "GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    function checkEmpty(value, valueName) {
        if (value === "") {
            switch (valueName) {
                case "rangeMin":
                    return "EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */;
                case "rangeMax":
                    return "EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    function checkNaN(value, valueName) {
        if (isNaN(value)) {
            switch (valueName) {
                case "rangeMin":
                    return "GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */;
                case "rangeMax":
                    return "GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */;
                case "lowerInflectionPointValue":
                    return "GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */;
                case "upperInflectionPointValue":
                    return "GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */;
            }
        }
        return "Success" /* CommandResult.Success */;
    }
    class GaugeChart extends AbstractChart {
        dataRange;
        sectionRule;
        background;
        type = "gauge";
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.dataRange = createRange(this.getters, this.sheetId, definition.dataRange);
            this.sectionRule = definition.sectionRule;
            this.background = definition.background;
        }
        static validateChartDefinition(validator, definition) {
            return validator.checkValidations(definition, isDataRangeValid, validator.chainValidations(checkRangeLimits(checkEmpty, validator.batchValidations), checkRangeLimits(checkNaN, validator.batchValidations), checkRangeMinBiggerThanRangeMax), validator.chainValidations(checkInflectionPointsValue(checkNaN, validator.batchValidations)));
        }
        static transformDefinition(definition, executed) {
            let dataRangeZone;
            if (definition.dataRange) {
                dataRangeZone = transformZone(toUnboundedZone(definition.dataRange), executed);
            }
            return {
                ...definition,
                dataRange: dataRangeZone ? zoneToXc(dataRangeZone) : undefined,
            };
        }
        static getDefinitionFromContextCreation(context) {
            return {
                background: context.background,
                title: context.title || "",
                type: "gauge",
                dataRange: context.range ? context.range[0] : undefined,
                sectionRule: {
                    colors: {
                        lowerColor: DEFAULT_GAUGE_LOWER_COLOR,
                        middleColor: DEFAULT_GAUGE_MIDDLE_COLOR,
                        upperColor: DEFAULT_GAUGE_UPPER_COLOR,
                    },
                    rangeMin: "0",
                    rangeMax: "100",
                    lowerInflectionPoint: {
                        type: "percentage",
                        value: "15",
                    },
                    upperInflectionPoint: {
                        type: "percentage",
                        value: "40",
                    },
                },
            };
        }
        copyForSheetId(sheetId) {
            const dataRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.dataRange);
            const definition = this.getDefinitionWithSpecificRanges(dataRange, sheetId);
            return new GaugeChart(definition, sheetId, this.getters);
        }
        copyInSheetId(sheetId) {
            const definition = this.getDefinitionWithSpecificRanges(this.dataRange, sheetId);
            return new GaugeChart(definition, sheetId, this.getters);
        }
        getDefinition() {
            return this.getDefinitionWithSpecificRanges(this.dataRange);
        }
        getDefinitionWithSpecificRanges(dataRange, targetSheetId) {
            return {
                background: this.background,
                sectionRule: this.sectionRule,
                title: this.title,
                type: "gauge",
                dataRange: dataRange
                    ? this.getters.getRangeString(dataRange, targetSheetId || this.sheetId)
                    : undefined,
            };
        }
        getDefinitionForExcel() {
            // This kind of graph is not exportable in Excel
            return undefined;
        }
        getContextCreation() {
            return {
                background: this.background,
                title: this.title,
                range: this.dataRange
                    ? [this.getters.getRangeString(this.dataRange, this.sheetId)]
                    : undefined,
            };
        }
        updateRanges(applyChange) {
            const range = adaptChartRange(this.dataRange, applyChange);
            if (this.dataRange === range) {
                return this;
            }
            const definition = this.getDefinitionWithSpecificRanges(range);
            return new GaugeChart(definition, this.sheetId, this.getters);
        }
    }
    function getGaugeConfiguration(chart, locale) {
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, [], fontColor, {
            locale,
        });
        config.options.hover = undefined;
        config.options.events = [];
        config.options.layout = {
            padding: { left: 30, right: 30, top: chart.title ? 10 : 25, bottom: 25 },
        };
        config.options.needle = {
            width: 10,
            borderColor: "#000000",
            backgroundColor: "#000000",
        };
        config.options.valueLabel = {
            display: false,
            font: {
                size: 30,
                color: "#FFFFFF",
            },
            backgroundColor: "#000000",
            borderColor: "#000000",
            borderRadius: 5,
            padding: {
                top: 5,
                right: 5,
                bottom: 5,
                left: 5,
            },
        };
        return config;
    }
    function createGaugeChartRuntime(chart, getters) {
        const locale = getters.getLocale();
        const config = getGaugeConfiguration(chart, locale);
        const colors = chart.sectionRule.colors;
        const lowerPoint = chart.sectionRule.lowerInflectionPoint;
        const upperPoint = chart.sectionRule.upperInflectionPoint;
        const lowerPointValue = Number(lowerPoint.value);
        const upperPointValue = Number(upperPoint.value);
        const minNeedleValue = Number(chart.sectionRule.rangeMin);
        const maxNeedleValue = Number(chart.sectionRule.rangeMax);
        const needleCoverage = maxNeedleValue - minNeedleValue;
        const needleInflectionPoint = [];
        if (lowerPoint.value !== "") {
            const lowerPointNeedleValue = lowerPoint.type === "number"
                ? lowerPointValue
                : minNeedleValue + (needleCoverage * lowerPointValue) / 100;
            needleInflectionPoint.push({
                value: clip(lowerPointNeedleValue, minNeedleValue, maxNeedleValue),
                color: colors.lowerColor,
            });
        }
        if (upperPoint.value !== "") {
            const upperPointNeedleValue = upperPoint.type === "number"
                ? upperPointValue
                : minNeedleValue + (needleCoverage * upperPointValue) / 100;
            needleInflectionPoint.push({
                value: clip(upperPointNeedleValue, minNeedleValue, maxNeedleValue),
                color: colors.middleColor,
            });
        }
        const data = [];
        const backgroundColor = [];
        needleInflectionPoint
            .sort((a, b) => a.value - b.value)
            .map((point) => {
            data.push(point.value);
            backgroundColor.push(point.color);
        });
        data.push(maxNeedleValue);
        backgroundColor.push(colors.upperColor);
        const dataRange = chart.dataRange;
        const deltaBeyondRangeLimit = needleCoverage / 30;
        let needleValue = minNeedleValue - deltaBeyondRangeLimit; // make needle value always at the minimum by default
        let cellFormatter = undefined;
        let displayValue = false;
        if (dataRange !== undefined) {
            const cell = getters.getEvaluatedCell({
                sheetId: dataRange.sheetId,
                col: dataRange.zone.left,
                row: dataRange.zone.top,
            });
            if (cell.type === CellValueType.number) {
                // in gauge graph "datasets.value" is used to calculate the angle of the
                // needle in the graph. To prevent the needle from making 360° turns, we
                // clip the value between a min and a max. This min and this max are slightly
                // smaller and slightly larger than minRange and maxRange to mark the fact
                // that the needle is out of the range limits
                needleValue = clip(cell.value, minNeedleValue - deltaBeyondRangeLimit, maxNeedleValue + deltaBeyondRangeLimit);
                // show the original value, not the clipped one
                cellFormatter = () => getters.getRangeFormattedValues(dataRange)[0];
                displayValue = true;
            }
        }
        config.options.valueLabel.display = displayValue;
        config.options.valueLabel.formatter = cellFormatter;
        config.data.datasets.push({
            data,
            minValue: Number(chart.sectionRule.rangeMin),
            value: needleValue,
            backgroundColor,
        });
        return {
            chartJsConfig: config,
            background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
        };
    }

    const UNIT_LENGTH = {
        second: 1000,
        minute: 1000 * 60,
        hour: 1000 * 3600,
        day: 1000 * 3600 * 24,
        month: 1000 * 3600 * 24 * 30,
        year: 1000 * 3600 * 24 * 365,
    };
    const Milliseconds = {
        inSeconds: function (milliseconds) {
            return Math.floor(milliseconds / UNIT_LENGTH.second);
        },
        inMinutes: function (milliseconds) {
            return Math.floor(milliseconds / UNIT_LENGTH.minute);
        },
        inHours: function (milliseconds) {
            return Math.floor(milliseconds / UNIT_LENGTH.hour);
        },
        inDays: function (milliseconds) {
            return Math.floor(milliseconds / UNIT_LENGTH.day);
        },
        inMonths: function (milliseconds) {
            return Math.floor(milliseconds / UNIT_LENGTH.month);
        },
        inYears: function (milliseconds) {
            return Math.floor(milliseconds / UNIT_LENGTH.year);
        },
    };
    /**
     * Regex to test if a format string is a date format that can be translated into a luxon time format
     */
    const timeFormatLuxonCompatible = /^((d|dd|m|mm|yyyy|yy|hh|h|ss|a)(-|:|\s|\/))*(d|dd|m|mm|yyyy|yy|hh|h|ss|a)$/i;
    /** Get the time options for the XAxis of ChartJS */
    function getChartTimeOptions(labels, labelFormat, locale) {
        const luxonFormat = convertDateFormatForLuxon(labelFormat);
        const timeUnit = getBestTimeUnitForScale(labels, luxonFormat, locale);
        const displayFormats = {};
        if (timeUnit) {
            displayFormats[timeUnit] = luxonFormat;
        }
        return {
            parser: luxonFormat,
            displayFormats,
            unit: timeUnit ?? false,
        };
    }
    /**
     * Convert the given date format into a format that moment.js understands.
     *
     * https://github.com/moment/luxon/blob/master/docs/formatting.md#table-of-tokens
     */
    function convertDateFormatForLuxon(format) {
        // "m" before "h" === month, "m" after "h" === minute
        const indexH = format.indexOf("h");
        if (indexH >= 0) {
            format = format.slice(0, indexH).replace(/m/g, "M") + format.slice(indexH);
        }
        else {
            format = format.replace(/m/g, "M");
        }
        // If we have an "a", we should display hours as AM/PM (h), otherwise display 24 hours format (H)
        if (!format.includes("a")) {
            format = format.replace(/h/g, "H");
        }
        return format;
    }
    /** Get the minimum time unit that the format is able to display */
    function getFormatMinDisplayUnit(format) {
        if (format.includes("s")) {
            return "second";
        }
        else if (format.includes("m")) {
            return "minute";
        }
        else if (format.includes("h") || format.includes("H")) {
            return "hour";
        }
        else if (format.includes("d")) {
            return "day";
        }
        else if (format.includes("M")) {
            return "month";
        }
        return "year";
    }
    /**
     * Returns the best time unit that should be used for the X axis of a chart in order to display all
     * the labels correctly.
     *
     * There is two conditions :
     *  - the format of the labels should be able to display the unit. For example if the format is "DD/MM/YYYY"
     *    it makes no sense to try to use minutes in the X axis
     *  - we want the "best fit" unit. For example if the labels span a period of several days, we want to use days
     *    as a unit, but if they span 200 days, we'd like to use months instead
     *
     */
    function getBestTimeUnitForScale(labels, format, locale) {
        const labelDates = labels.map((label) => parseDateTime(label, locale)?.jsDate);
        if (labelDates.some((date) => date === undefined) || labels.length < 2) {
            return undefined;
        }
        const labelsTimestamps = labelDates.map((date) => date.getTime());
        const period = Math.max(...labelsTimestamps) - Math.min(...labelsTimestamps);
        const minUnit = getFormatMinDisplayUnit(format);
        if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
            return "second";
        }
        else if (UNIT_LENGTH.minute >= UNIT_LENGTH[minUnit] && Milliseconds.inMinutes(period) < 180) {
            return "minute";
        }
        else if (UNIT_LENGTH.hour >= UNIT_LENGTH[minUnit] && Milliseconds.inHours(period) < 96) {
            return "hour";
        }
        else if (UNIT_LENGTH.day >= UNIT_LENGTH[minUnit] && Milliseconds.inDays(period) < 90) {
            return "day";
        }
        else if (UNIT_LENGTH.month >= UNIT_LENGTH[minUnit] && Milliseconds.inMonths(period) < 36) {
            return "month";
        }
        return "year";
    }

    class LineChart extends AbstractChart {
        dataSets;
        labelRange;
        background;
        verticalAxisPosition;
        legendPosition;
        labelsAsText;
        stacked;
        aggregated;
        type = "line";
        dataSetsHaveTitle;
        cumulative;
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.dataSets = createDataSets(this.getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
            this.labelRange = createRange(this.getters, sheetId, definition.labelRange);
            this.background = definition.background;
            this.verticalAxisPosition = definition.verticalAxisPosition;
            this.legendPosition = definition.legendPosition;
            this.labelsAsText = definition.labelsAsText;
            this.stacked = definition.stacked;
            this.aggregated = definition.aggregated;
            this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
            this.cumulative = definition.cumulative;
        }
        static validateChartDefinition(validator, definition) {
            return validator.checkValidations(definition, checkDataset, checkLabelRange);
        }
        static transformDefinition(definition, executed) {
            return transformChartDefinitionWithDataSetsWithZone(definition, executed);
        }
        static getDefinitionFromContextCreation(context) {
            return {
                background: context.background,
                dataSets: context.range ? context.range : [],
                dataSetsHaveTitle: false,
                labelsAsText: false,
                legendPosition: "top",
                title: context.title || "",
                type: "line",
                verticalAxisPosition: "left",
                labelRange: context.auxiliaryRange || undefined,
                stacked: false,
                aggregated: false,
                cumulative: false,
            };
        }
        getDefinition() {
            return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
        }
        getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
            return {
                type: "line",
                dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
                background: this.background,
                dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
                legendPosition: this.legendPosition,
                verticalAxisPosition: this.verticalAxisPosition,
                labelRange: labelRange
                    ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                    : undefined,
                title: this.title,
                labelsAsText: this.labelsAsText,
                stacked: this.stacked,
                aggregated: this.aggregated,
                cumulative: this.cumulative,
            };
        }
        getContextCreation() {
            return {
                background: this.background,
                title: this.title,
                range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
                auxiliaryRange: this.labelRange
                    ? this.getters.getRangeString(this.labelRange, this.sheetId)
                    : undefined,
            };
        }
        updateRanges(applyChange) {
            const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
            if (!isStale) {
                return this;
            }
            const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
            return new LineChart(definition, this.sheetId, this.getters);
        }
        getDefinitionForExcel() {
            // Excel does not support aggregating labels
            if (this.aggregated)
                return undefined;
            const dataSets = this.dataSets
                .map((ds) => toExcelDataset(this.getters, ds))
                .filter((ds) => ds.range !== "" && ds.range !== INCORRECT_RANGE_STRING);
            const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
            return {
                ...this.getDefinition(),
                backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
                fontColor: toXlsxHexColor(chartFontColor(this.background)),
                dataSets,
                labelRange,
            };
        }
        copyForSheetId(sheetId) {
            const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
            const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
            const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
            return new LineChart(definition, sheetId, this.getters);
        }
        copyInSheetId(sheetId) {
            const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
            return new LineChart(definition, sheetId, this.getters);
        }
    }
    function fixEmptyLabelsForDateCharts(labels, dataSetsValues) {
        if (labels.length === 0 || labels.every((label) => !label)) {
            return { labels, dataSetsValues };
        }
        const newLabels = [...labels];
        const newDatasets = deepCopy(dataSetsValues);
        for (let i = 0; i < newLabels.length; i++) {
            if (!newLabels[i]) {
                newLabels[i] = findNextDefinedValue(newLabels, i);
                for (let ds of newDatasets) {
                    ds.data[i] = undefined;
                }
            }
        }
        return { labels: newLabels, dataSetsValues: newDatasets };
    }
    function canChartParseLabels(labelRange, getters) {
        return canBeDateChart(labelRange, getters) || canBeLinearChart(labelRange, getters);
    }
    function getChartAxisType(chart, getters) {
        if (isDateChart(chart, getters) && isLuxonTimeAdapterInstalled()) {
            return "time";
        }
        if (isLinearChart(chart, getters)) {
            return "linear";
        }
        return "category";
    }
    function isDateChart(chart, getters) {
        return !chart.labelsAsText && canBeDateChart(chart.labelRange, getters);
    }
    function isLinearChart(chart, getters) {
        return !chart.labelsAsText && canBeLinearChart(chart.labelRange, getters);
    }
    function canBeDateChart(labelRange, getters) {
        if (!labelRange || !canBeLinearChart(labelRange, getters)) {
            return false;
        }
        const labelFormat = getters.getEvaluatedCell({
            sheetId: labelRange.sheetId,
            col: labelRange.zone.left,
            row: labelRange.zone.top,
        }).format;
        return Boolean(labelFormat && timeFormatLuxonCompatible.test(labelFormat));
    }
    function canBeLinearChart(labelRange, getters) {
        if (!labelRange) {
            return false;
        }
        const labels = getters.getRangeValues(labelRange);
        if (labels.some((label) => isNaN(Number(label)) && label)) {
            return false;
        }
        if (labels.every((label) => !label)) {
            return false;
        }
        return true;
    }
    function getLineConfiguration(chart, labels, localeFormat) {
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
        const legend = {
            labels: {
                color: fontColor,
                generateLabels(chart) {
                    // color the legend labels with the dataset color, without any transparency
                    const { data } = chart;
                    /** @ts-ignore */
                    const labels = window.Chart.defaults.plugins.legend.labels
                        .generateLabels(chart);
                    for (const [index, label] of labels.entries()) {
                        label.fillStyle = data.datasets[index].borderColor;
                    }
                    return labels;
                },
            },
        };
        if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
            legend.display = false;
        }
        else {
            legend.position = chart.legendPosition;
        }
        Object.assign(config.options.plugins.legend || {}, legend);
        config.options.layout = {
            padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
        };
        config.options.scales = {
            x: {
                ticks: {
                    padding: 5,
                    color: fontColor,
                },
            },
            y: {
                position: chart.verticalAxisPosition,
                beginAtZero: true,
                ticks: {
                    color: fontColor,
                    callback: (value) => {
                        value = Number(value);
                        if (isNaN(value))
                            return value;
                        const { locale, format } = localeFormat;
                        return formatValue(value, { locale, format: !format && value > 1000 ? "#,##" : format });
                    },
                },
            },
        };
        if (chart.stacked && config.options?.scales?.y) {
            // @ts-ignore chart.js type is wrong
            config.options.scales.y.stacked = true;
        }
        return config;
    }
    function createLineChartRuntime(chart, getters) {
        const axisType = getChartAxisType(chart, getters);
        const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
        let labels = axisType === "linear" ? labelValues.values : labelValues.formattedValues;
        let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
        if (chart.dataSetsHaveTitle &&
            dataSetsValues[0] &&
            labels.length > dataSetsValues[0].data.length) {
            labels.shift();
        }
        ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
        if (axisType === "time") {
            ({ labels, dataSetsValues } = fixEmptyLabelsForDateCharts(labels, dataSetsValues));
        }
        if (chart.aggregated) {
            ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
        }
        const locale = getters.getLocale();
        const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
        const localeFormat = { format: dataSetFormat, locale };
        const config = getLineConfiguration(chart, labels, localeFormat);
        const labelFormat = getChartLabelFormat(getters, chart.labelRange);
        if (axisType === "time") {
            const axis = {
                type: "time",
                time: getChartTimeOptions(labels, labelFormat, locale),
            };
            Object.assign(config.options.scales.x, axis);
            config.options.scales.x.ticks.maxTicksLimit = 15;
        }
        else if (axisType === "linear") {
            config.options.scales.x.type = "linear";
            config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });
            config.options.plugins.tooltip.callbacks.title = (tooltipItem) => {
                return formatValue(tooltipItem[0].parsed.x || tooltipItem[0].label, {
                    locale,
                    format: labelFormat,
                });
            };
        }
        const colors = new ChartColors();
        for (let [index, { label, data }] of dataSetsValues.entries()) {
            if (["linear", "time"].includes(axisType)) {
                // Replace empty string labels by undefined to make sure chartJS doesn't decide that "" is the same as 0
                data = data.map((y, index) => ({ x: labels[index] || undefined, y }));
            }
            const color = colors.next();
            let backgroundRGBA = colorToRGBA(color);
            if (chart.stacked) {
                backgroundRGBA.a = LINE_FILL_TRANSPARENCY;
            }
            if (chart.cumulative) {
                let accumulator = 0;
                data = data.map((value) => {
                    if (!isNaN(value)) {
                        accumulator += parseFloat(value);
                        return accumulator;
                    }
                    return value;
                });
            }
            const backgroundColor = rgbaToHex(backgroundRGBA);
            const dataset = {
                label,
                data,
                tension: 0,
                borderColor: color,
                backgroundColor,
                pointBackgroundColor: color,
                fill: chart.stacked ? getFillingMode(index) : false,
            };
            config.data.datasets.push(dataset);
        }
        return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
    }
    let missingTimeAdapterAlreadyWarned = false;
    function isLuxonTimeAdapterInstalled() {
        // @ts-ignore
        if (!window.Chart) {
            return false;
        }
        // @ts-ignore
        const adapter = new window.Chart._adapters._date({});
        // @ts-ignore
        const isInstalled = adapter._id === "luxon";
        if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
            missingTimeAdapterAlreadyWarned = true;
            console.warn("'chartjs-adapter-luxon' time adapter is not installed. Time scale axes are disabled.");
        }
        return isInstalled;
    }

    class PieChart extends AbstractChart {
        dataSets;
        labelRange;
        background;
        legendPosition;
        type = "pie";
        aggregated;
        dataSetsHaveTitle;
        constructor(definition, sheetId, getters) {
            super(definition, sheetId, getters);
            this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
            this.labelRange = createRange(getters, sheetId, definition.labelRange);
            this.background = definition.background;
            this.legendPosition = definition.legendPosition;
            this.aggregated = definition.aggregated;
            this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
        }
        static transformDefinition(definition, executed) {
            return transformChartDefinitionWithDataSetsWithZone(definition, executed);
        }
        static validateChartDefinition(validator, definition) {
            return validator.checkValidations(definition, checkDataset, checkLabelRange);
        }
        static getDefinitionFromContextCreation(context) {
            return {
                background: context.background,
                dataSets: context.range ? context.range : [],
                dataSetsHaveTitle: false,
                legendPosition: "top",
                title: context.title || "",
                type: "pie",
                labelRange: context.auxiliaryRange || undefined,
                aggregated: false,
            };
        }
        getDefinition() {
            return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
        }
        getContextCreation() {
            return {
                background: this.background,
                title: this.title,
                range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
                auxiliaryRange: this.labelRange
                    ? this.getters.getRangeString(this.labelRange, this.sheetId)
                    : undefined,
            };
        }
        getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
            return {
                type: "pie",
                dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
                background: this.background,
                dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
                legendPosition: this.legendPosition,
                labelRange: labelRange
                    ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
                    : undefined,
                title: this.title,
                aggregated: this.aggregated,
            };
        }
        copyForSheetId(sheetId) {
            const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
            const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
            const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
            return new PieChart(definition, sheetId, this.getters);
        }
        copyInSheetId(sheetId) {
            const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
            return new PieChart(definition, sheetId, this.getters);
        }
        getDefinitionForExcel() {
            // Excel does not support aggregating labels
            if (this.aggregated)
                return undefined;
            const dataSets = this.dataSets
                .map((ds) => toExcelDataset(this.getters, ds))
                .filter((ds) => ds.range !== "" && ds.range !== INCORRECT_RANGE_STRING);
            const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
            return {
                ...this.getDefinition(),
                backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
                fontColor: toXlsxHexColor(chartFontColor(this.background)),
                verticalAxisPosition: "left",
                dataSets,
                labelRange,
            };
        }
        updateRanges(applyChange) {
            const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
            if (!isStale) {
                return this;
            }
            const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
            return new PieChart(definition, this.sheetId, this.getters);
        }
    }
    function getPieConfiguration(chart, labels, localeFormat) {
        const fontColor = chartFontColor(chart.background);
        const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
        const legend = {
            labels: { color: fontColor },
        };
        if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
            legend.display = false;
        }
        else {
            legend.position = chart.legendPosition;
        }
        Object.assign(config.options.plugins.legend || {}, legend);
        config.options.layout = {
            padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
        };
        config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
            return tooltipItems[0].dataset.label;
        };
        config.options.plugins.tooltip.callbacks.label = function (tooltipItem) {
            const { format, locale } = localeFormat;
            const data = tooltipItem.dataset.data;
            const dataIndex = tooltipItem.dataIndex;
            const percentage = calculatePercentage(data, dataIndex);
            const xLabel = tooltipItem.label || tooltipItem.dataset.label;
            const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;
            const toolTipFormat = !format && yLabel > 1000 ? "#,##" : format;
            const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
            return xLabel ? `${xLabel}: ${yLabelStr} (${percentage}%)` : `${yLabelStr} (${percentage}%)`;
        };
        return config;
    }
    function getPieColors(colors, dataSetsValues) {
        const pieColors = [];
        const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
        for (let i = 0; i <= maxLength; i++) {
            pieColors.push(colors.next());
        }
        return pieColors;
    }
    function calculatePercentage(dataset, dataIndex) {
        const numericData = dataset.filter((value) => typeof value === "number");
        const total = numericData.reduce((sum, value) => sum + value, 0);
        if (!total) {
            return "";
        }
        const percentage = (dataset[dataIndex] / total) * 100;
        return percentage.toFixed(2);
    }
    function createPieChartRuntime(chart, getters) {
        const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
        let labels = labelValues.formattedValues;
        let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
        if (chart.dataSetsHaveTitle &&
            dataSetsValues[0] &&
            labels.length > dataSetsValues[0].data.length) {
            labels.shift();
        }
        ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
        if (chart.aggregated) {
            ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
        }
        const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
        const locale = getters.getLocale();
        const config = getPieConfiguration(chart, labels, { format: dataSetFormat, locale });
        const colors = new ChartColors();
        for (let { label, data } of dataSetsValues) {
            const backgroundColor = getPieColors(colors, dataSetsValues);
            const dataset = {
                label,
                data,
                borderColor: "#FFFFFF",
                backgroundColor,
            };
            config.data.datasets.push(dataset);
        }
        return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
    }

    /**
     * This registry is intended to map a cell content (raw string) to
     * an instance of a cell.
     */
    const chartRegistry = new Registry();
    chartRegistry.add("bar", {
        match: (type) => type === "bar",
        createChart: (definition, sheetId, getters) => new BarChart(definition, sheetId, getters),
        getChartRuntime: createBarChartRuntime,
        validateChartDefinition: (validator, definition) => BarChart.validateChartDefinition(validator, definition),
        transformDefinition: (definition, executed) => BarChart.transformDefinition(definition, executed),
        getChartDefinitionFromContextCreation: (context) => BarChart.getDefinitionFromContextCreation(context),
        name: _t("Bar"),
        sequence: 10,
    });
    chartRegistry.add("line", {
        match: (type) => type === "line",
        createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),
        getChartRuntime: createLineChartRuntime,
        validateChartDefinition: (validator, definition) => LineChart.validateChartDefinition(validator, definition),
        transformDefinition: (definition, executed) => LineChart.transformDefinition(definition, executed),
        getChartDefinitionFromContextCreation: (context) => LineChart.getDefinitionFromContextCreation(context),
        name: _t("Line"),
        sequence: 20,
    });
    chartRegistry.add("pie", {
        match: (type) => type === "pie",
        createChart: (definition, sheetId, getters) => new PieChart(definition, sheetId, getters),
        getChartRuntime: createPieChartRuntime,
        validateChartDefinition: (validator, definition) => PieChart.validateChartDefinition(validator, definition),
        transformDefinition: (definition, executed) => PieChart.transformDefinition(definition, executed),
        getChartDefinitionFromContextCreation: (context) => PieChart.getDefinitionFromContextCreation(context),
        name: _t("Pie"),
        sequence: 30,
    });
    chartRegistry.add("scorecard", {
        match: (type) => type === "scorecard",
        createChart: (definition, sheetId, getters) => new ScorecardChart$1(definition, sheetId, getters),
        getChartRuntime: createScorecardChartRuntime,
        validateChartDefinition: (validator, definition) => ScorecardChart$1.validateChartDefinition(validator, definition),
        transformDefinition: (definition, executed) => ScorecardChart$1.transformDefinition(definition, executed),
        getChartDefinitionFromContextCreation: (context) => ScorecardChart$1.getDefinitionFromContextCreation(context),
        name: _t("Scorecard"),
        sequence: 40,
    });
    chartRegistry.add("gauge", {
        match: (type) => type === "gauge",
        createChart: (definition, sheetId, getters) => new GaugeChart(definition, sheetId, getters),
        getChartRuntime: createGaugeChartRuntime,
        validateChartDefinition: (validator, definition) => GaugeChart.validateChartDefinition(validator, definition),
        transformDefinition: (definition, executed) => GaugeChart.transformDefinition(definition, executed),
        getChartDefinitionFromContextCreation: (context) => GaugeChart.getDefinitionFromContextCreation(context),
        name: _t("Gauge"),
        sequence: 50,
    });
    const chartComponentRegistry = new Registry();
    chartComponentRegistry.add("line", ChartJsComponent);
    chartComponentRegistry.add("bar", ChartJsComponent);
    chartComponentRegistry.add("pie", ChartJsComponent);
    chartComponentRegistry.add("gauge", ChartJsComponent);
    chartComponentRegistry.add("scorecard", ScorecardChart);

    /**
     * Registry intended to support usual currencies. It is mainly used to create
     * currency formats that can be selected or modified when customizing formats.
     */
    const currenciesRegistry = new Registry();

    // -----------------------------------------------------------------------------
    // STYLE
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-chart-container {
    width: 100%;
    height: 100%;
    position: relative;
  }
`;
    class ChartFigure extends owl.Component {
        static template = "o-spreadsheet-ChartFigure";
        static components = {};
        onDoubleClick() {
            this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id });
            this.env.openSidePanel("ChartPanel");
        }
        get chartType() {
            return this.env.model.getters.getChartType(this.props.figure.id);
        }
        get chartComponent() {
            const type = this.chartType;
            const component = chartComponentRegistry.get(type);
            if (!component) {
                throw new Error(`Component is not defined for type ${type}`);
            }
            return component;
        }
    }
    ChartFigure.props = {
        figure: Object,
        onFigureDeleted: Function,
    };

    class ImageFigure extends owl.Component {
        static template = "o-spreadsheet-ImageFigure";
        static components = {};
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        get figureId() {
            return this.props.figure.id;
        }
        get getImagePath() {
            return this.env.model.getters.getImagePath(this.figureId);
        }
    }
    ImageFigure.props = {
        figure: Object,
        onFigureDeleted: Function,
    };

    function centerFigurePosition(getters, size) {
        const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();
        const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
        const dim = getters.getSheetViewDimension();
        const rect = getters.getVisibleRect(getters.getActiveMainViewport());
        const scrollableViewportWidth = Math.min(rect.width, dim.width - offsetCorrectionX);
        const scrollableViewportHeight = Math.min(rect.height, dim.height - offsetCorrectionY);
        const position = {
            x: offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2),
            y: offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2),
        }; // Position at the center of the scrollable viewport
        return position;
    }
    function getMaxFigureSize(getters, figureSize) {
        const size = deepCopy(figureSize);
        const dim = getters.getSheetViewDimension();
        const maxWidth = dim.width;
        const maxHeight = dim.height;
        if (size.width > maxWidth) {
            const ratio = maxWidth / size.width;
            size.width = maxWidth;
            size.height = size.height * ratio;
        }
        if (size.height > maxHeight) {
            const ratio = maxHeight / size.height;
            size.height = maxHeight;
            size.width = size.width * ratio;
        }
        return size;
    }

    const figureRegistry = new Registry();
    figureRegistry.add("chart", {
        Component: ChartFigure,
        SidePanelComponent: "ChartPanel",
        menuBuilder: getChartMenu,
    });
    figureRegistry.add("image", {
        Component: ImageFigure,
        keepRatio: true,
        minFigSize: 20,
        borderWidth: 0,
        menuBuilder: getImageMenuRegistry,
    });
    function getChartMenu(figureId, onFigureDeleted, env) {
        const menuItemSpecs = [
            {
                id: "edit",
                name: _t("Edit"),
                sequence: 1,
                execute: () => {
                    env.model.dispatch("SELECT_FIGURE", { id: figureId });
                    env.openSidePanel("ChartPanel");
                },
                icon: "o-spreadsheet-Icon.EDIT",
            },
            getCopyMenuItem(figureId, env),
            getCutMenuItem(figureId, env),
            getDeleteMenuItem(figureId, onFigureDeleted, env),
        ];
        return createActions(menuItemSpecs);
    }
    function getImageMenuRegistry(figureId, onFigureDeleted, env) {
        const menuItemSpecs = [
            getCopyMenuItem(figureId, env),
            getCutMenuItem(figureId, env),
            {
                id: "reset_size",
                name: _t("Reset size"),
                sequence: 4,
                execute: async () => {
                    const imagePath = env.model.getters.getImagePath(figureId);
                    const size = env.model.getters.getImageSize(figureId) ??
                        (await env.imageProvider?.getImageOriginalSize(imagePath));
                    if (!env.model.getters.getImageSize(figureId)) {
                        const image = env.model.getters.getImage(figureId);
                        image.size = size;
                    }
                    const { height, width } = getMaxFigureSize(env.model.getters, size);
                    env.model.dispatch("UPDATE_FIGURE", {
                        sheetId: env.model.getters.getActiveSheetId(),
                        id: figureId,
                        height,
                        width,
                    });
                },
                icon: "o-spreadsheet-Icon.REFRESH",
            },
            getDeleteMenuItem(figureId, onFigureDeleted, env),
        ];
        return createActions(menuItemSpecs);
    }
    function getCopyMenuItem(figureId, env) {
        return {
            id: "copy",
            name: _t("Copy"),
            sequence: 2,
            description: "Ctrl+C",
            execute: async () => {
                env.model.dispatch("SELECT_FIGURE", { id: figureId });
                env.model.dispatch("COPY");
                await env.clipboard.write(env.model.getters.getClipboardContent());
            },
            icon: "o-spreadsheet-Icon.COPY",
        };
    }
    function getCutMenuItem(figureId, env) {
        return {
            id: "cut",
            name: _t("Cut"),
            sequence: 3,
            description: "Ctrl+X",
            execute: async () => {
                env.model.dispatch("SELECT_FIGURE", { id: figureId });
                env.model.dispatch("CUT");
                await env.clipboard.write(env.model.getters.getClipboardContent());
            },
            icon: "o-spreadsheet-Icon.CUT",
        };
    }
    function getDeleteMenuItem(figureId, onFigureDeleted, env) {
        return {
            id: "delete",
            name: _t("Delete"),
            sequence: 10,
            execute: () => {
                env.model.dispatch("DELETE_FIGURE", {
                    sheetId: env.model.getters.getActiveSheetId(),
                    id: figureId,
                });
                onFigureDeleted();
            },
            icon: "o-spreadsheet-Icon.DELETE",
        };
    }

    const inverseCommandRegistry = new Registry()
        .add("ADD_COLUMNS_ROWS", inverseAddColumnsRows)
        .add("REMOVE_COLUMNS_ROWS", inverseRemoveColumnsRows)
        .add("ADD_MERGE", inverseAddMerge)
        .add("REMOVE_MERGE", inverseRemoveMerge)
        .add("CREATE_SHEET", inverseCreateSheet)
        .add("DELETE_SHEET", inverseDeleteSheet)
        .add("DUPLICATE_SHEET", inverseDuplicateSheet)
        .add("CREATE_FIGURE", inverseCreateFigure)
        .add("CREATE_CHART", inverseCreateChart)
        .add("HIDE_COLUMNS_ROWS", inverseHideColumnsRows)
        .add("UNHIDE_COLUMNS_ROWS", inverseUnhideColumnsRows);
    for (const cmd of coreTypes.values()) {
        if (!inverseCommandRegistry.contains(cmd)) {
            inverseCommandRegistry.add(cmd, identity);
        }
    }
    function identity(cmd) {
        return [cmd];
    }
    function inverseAddColumnsRows(cmd) {
        const elements = [];
        let start = cmd.base;
        if (cmd.position === "after") {
            start++;
        }
        for (let i = 0; i < cmd.quantity; i++) {
            elements.push(i + start);
        }
        return [
            {
                type: "REMOVE_COLUMNS_ROWS",
                dimension: cmd.dimension,
                elements,
                sheetId: cmd.sheetId,
            },
        ];
    }
    function inverseAddMerge(cmd) {
        return [{ type: "REMOVE_MERGE", sheetId: cmd.sheetId, target: cmd.target }];
    }
    function inverseRemoveMerge(cmd) {
        return [{ type: "ADD_MERGE", sheetId: cmd.sheetId, target: cmd.target }];
    }
    function inverseCreateSheet(cmd) {
        return [{ type: "DELETE_SHEET", sheetId: cmd.sheetId }];
    }
    function inverseDuplicateSheet(cmd) {
        return [{ type: "DELETE_SHEET", sheetId: cmd.sheetIdTo }];
    }
    function inverseRemoveColumnsRows(cmd) {
        const commands = [];
        const elements = [...cmd.elements].sort((a, b) => a - b);
        for (let group of groupConsecutive(elements)) {
            const column = group[0] === 0 ? 0 : group[0] - 1;
            const position = group[0] === 0 ? "before" : "after";
            commands.push({
                type: "ADD_COLUMNS_ROWS",
                dimension: cmd.dimension,
                quantity: group.length,
                base: column,
                sheetId: cmd.sheetId,
                position,
            });
        }
        return commands;
    }
    function inverseDeleteSheet(cmd) {
        return [{ type: "CREATE_SHEET", sheetId: cmd.sheetId, position: 1 }];
    }
    function inverseCreateFigure(cmd) {
        return [{ type: "DELETE_FIGURE", id: cmd.figure.id, sheetId: cmd.sheetId }];
    }
    function inverseCreateChart(cmd) {
        return [{ type: "DELETE_FIGURE", id: cmd.id, sheetId: cmd.sheetId }];
    }
    function inverseHideColumnsRows(cmd) {
        return [
            {
                type: "UNHIDE_COLUMNS_ROWS",
                sheetId: cmd.sheetId,
                dimension: cmd.dimension,
                elements: cmd.elements,
            },
        ];
    }
    function inverseUnhideColumnsRows(cmd) {
        return [
            {
                type: "HIDE_COLUMNS_ROWS",
                sheetId: cmd.sheetId,
                dimension: cmd.dimension,
                elements: cmd.elements,
            },
        ];
    }

    function interactiveCut(env) {
        const result = env.model.dispatch("CUT");
        if (!result.isSuccessful) {
            if (result.isCancelledBecause("WrongCutSelection" /* CommandResult.WrongCutSelection */)) {
                env.raiseError(_t("This operation is not allowed with multiple selections."));
            }
        }
    }

    const AddMergeInteractiveContent = {
        MergeIsDestructive: _t("Merging these cells will only preserve the top-leftmost value. Merge anyway?"),
        MergeInFilter: _t("You can't merge cells inside of an existing filter."),
    };
    function interactiveAddMerge(env, sheetId, target) {
        const result = env.model.dispatch("ADD_MERGE", { sheetId, target });
        if (result.isCancelledBecause("MergeInFilter" /* CommandResult.MergeInFilter */)) {
            env.raiseError(AddMergeInteractiveContent.MergeInFilter);
        }
        else if (result.isCancelledBecause("MergeIsDestructive" /* CommandResult.MergeIsDestructive */)) {
            env.askConfirmation(AddMergeInteractiveContent.MergeIsDestructive, () => {
                env.model.dispatch("ADD_MERGE", { sheetId, target, force: true });
            });
        }
    }

    /**
     * Create a function used to create a Chart based on the definition
     */
    function chartFactory(getters) {
        const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
        function createChart(id, definition, sheetId) {
            const builder = builders.find((builder) => builder.match(definition.type));
            if (!builder) {
                throw new Error(`No builder for this chart: ${definition.type}`);
            }
            return builder.createChart(definition, sheetId, getters);
        }
        return createChart;
    }
    /**
     * Create a function used to create a Chart Runtime based on the chart class
     * instance
     */
    function chartRuntimeFactory(getters) {
        const builders = chartRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
        function createRuntimeChart(chart) {
            const builder = builders.find((builder) => builder.match(chart.type));
            if (!builder) {
                throw new Error("No runtime builder for this chart.");
            }
            return builder.getChartRuntime(chart, getters);
        }
        return createRuntimeChart;
    }
    /**
     * Validate the chart definition given in arguments
     */
    function validateChartDefinition(validator, definition) {
        const validators = chartRegistry.getAll().find((validator) => validator.match(definition.type));
        if (!validators) {
            throw new Error("Unknown chart type.");
        }
        return validators.validateChartDefinition(validator, definition);
    }
    /**
     * Get a new chart definition transformed with the executed command. This
     * functions will be called during operational transform process
     */
    function transformDefinition(definition, executed) {
        const transformation = chartRegistry.getAll().find((factory) => factory.match(definition.type));
        if (!transformation) {
            throw new Error("Unknown chart type.");
        }
        return transformation.transformDefinition(definition, executed);
    }
    /**
     * Get an empty definition based on the given context and the given type
     */
    function getChartDefinitionFromContextCreation(context, type) {
        const chartClass = chartRegistry.get(type);
        return chartClass.getChartDefinitionFromContextCreation(context);
    }
    function getChartTypes() {
        const result = {};
        for (const key of chartRegistry.getKeys()) {
            result[key] = chartRegistry.get(key).name;
        }
        return result;
    }
    /**
     * Return a "smart" chart definition in the given zone. The definition is "smart" because it will
     * use the best type of chart to display the data of the zone.
     *
     * It will also try to find labels and datasets in the range, and try to find title for the datasets.
     *
     * The type of chart will be :
     * - If the zone is a single non-empty cell, returns a scorecard
     * - If the all the labels are numbers/date, returns a line chart
     * - Else returns a bar chart
     */
    function getSmartChartDefinition(zone, getters) {
        let dataSetZone = zone;
        if (zone.left !== zone.right) {
            dataSetZone = { ...zone, left: zone.left + 1 };
        }
        const dataSets = [zoneToXc(dataSetZone)];
        const sheetId = getters.getActiveSheetId();
        const topLeftCell = getters.getCell({ sheetId, col: zone.left, row: zone.top });
        if (getZoneArea(zone) === 1 && topLeftCell?.content) {
            return {
                type: "scorecard",
                title: "",
                background: topLeftCell.style?.fillColor || undefined,
                keyValue: zoneToXc(zone),
                baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
                baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
                baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
            };
        }
        let title = "";
        const cellsInFirstRow = getters.getEvaluatedCellsInZone(sheetId, {
            ...dataSetZone,
            bottom: dataSetZone.top,
        });
        const dataSetsHaveTitle = !!cellsInFirstRow.find((cell) => cell.type !== CellValueType.empty && cell.type !== CellValueType.number);
        if (dataSetsHaveTitle) {
            const texts = cellsInFirstRow
                .filter((cell) => cell.type !== CellValueType.error && cell.type !== CellValueType.empty)
                .map((cell) => cell.formattedValue);
            const lastElement = texts.splice(-1)[0];
            title = texts.join(", ");
            if (lastElement) {
                title += (title ? " " + _t("and") + " " : "") + lastElement;
            }
        }
        let labelRangeXc;
        if (zone.left !== zone.right) {
            labelRangeXc = zoneToXc({
                ...zone,
                right: zone.left,
            });
        }
        // Only display legend for several datasets.
        const newLegendPos = dataSetZone.right === dataSetZone.left ? "none" : "top";
        const labelRange = labelRangeXc ? getters.getRangeFromSheetXC(sheetId, labelRangeXc) : undefined;
        if (canChartParseLabels(labelRange, getters)) {
            return {
                title,
                dataSets,
                labelsAsText: false,
                stacked: false,
                aggregated: false,
                cumulative: false,
                labelRange: labelRangeXc,
                type: "line",
                dataSetsHaveTitle,
                verticalAxisPosition: "left",
                legendPosition: newLegendPos,
            };
        }
        return {
            title,
            dataSets,
            labelRange: labelRangeXc,
            type: "bar",
            stacked: false,
            aggregated: false,
            dataSetsHaveTitle,
            verticalAxisPosition: "left",
            legendPosition: newLegendPos,
        };
    }

    // @ts-ignore
    if (window.Chart) {
        // @ts-ignore
        const DoughnutController = window.Chart?.DoughnutController;
        /**
         * Example :
         * const chart = new Chart(ctx, {
         *   type: "gauge",
         *   data: {
         *     datasets: [
         *       {
         *         borderWidth: 5,
         *         data: [10, 20, 10],
         *         value: 23,
         *         backgroundColor: [
         *           "#dc3545", //red
         *           "#ffc107", //orange
         *           "#28a745", //green
         *         ],
         *       },
         *     ],
         *   },
         *   options: {
         *     plugins: {
         *       title: {
         *         display: true,
         *         text: "Custom Chart Title",
         *         padding: {
         *           top: 10,
         *           bottom: 30,
         *         },
         *         font : {
         *           size : 30
         *         }
         *       },
         *     },
         *     valueLabel: {
         *       display: true,
         *       format: (value) => {
         *         return value.toFixed(2) + "%";
         *       },
         *       font : {
         *         size : 35,
         *         family : "Arial",
         *         color : "#FC0",
         *       },
         *       backgroundColor : "#ccc",
         *       borderColor : "#090",
         *       borderRadius : 50
         *     },
         *     needle: {
         *       display: true,
         *       width: 50,
         *       color: "#DC5",
         *       backgroundColor : "#ccc",
         *     },
         *   },
         * });
         */
        class GaugeController extends DoughnutController {
            static id = "gauge";
            static defaults = {
                ...DoughnutController.defaults,
                circumference: 180,
                rotation: 270,
            };
            static overrides = {
                aspectRatio: 2,
                plugins: {
                    legend: {
                        display: false,
                    },
                    tooltip: {
                        enabled: false,
                    },
                },
            };
            get chartHeight() {
                return Math.abs(this.chart.chartArea.top - this.chart.chartArea.bottom);
            }
            get chartWidth() {
                return Math.abs(this.chart.chartArea.left - this.chart.chartArea.right);
            }
            get needleOptions() {
                const { config } = this.chart;
                const options = config.options;
                // @ts-ignore
                return options?.needle || {};
            }
            get valueLabelOptions() {
                const { config } = this.chart;
                const options = config.options;
                // @ts-ignore
                return options?.valueLabel || {};
            }
            get minValue() {
                const dataset = this.getDataset();
                // @ts-ignore
                return dataset.minValue || 0;
            }
            getValueAngleInPercent(value) {
                const dataset = this.getDataset();
                const data = dataset.data || [];
                const max = Math.max(...data);
                const min = this.minValue;
                return (value - min) / (max - min);
            }
            drawValueLabel(params) {
                const { ctx } = this.chart;
                if (this.valueLabelOptions.display === false) {
                    return;
                }
                // draw background rectangle
                ctx.save();
                ctx.beginPath();
                ctx.fillStyle = params.valueLabel.backgroundColor;
                ctx.strokeStyle = params.valueLabel.borderColor;
                ctx.roundRect(params.valueLabel.rect.x, params.valueLabel.rect.y, params.valueLabel.rect.width, params.valueLabel.rect.height, params.valueLabel.borderRadius);
                ctx.stroke();
                ctx.fill();
                // draw value text
                ctx.textAlign = "center";
                ctx.textBaseline = "middle";
                ctx.font = params.valueLabel.font;
                ctx.fillStyle = params.valueLabel.textColor;
                ctx.fillText(params.valueLabel.valueText, params.valueLabel.textPosition.x, params.valueLabel.textPosition.y);
                ctx.restore();
            }
            drawNeedle(params) {
                const { ctx } = this.chart;
                if (this.needleOptions.display === false) {
                    return;
                }
                // translate & rotate next paths
                ctx.save();
                ctx.translate(params.needle.position.x, params.needle.position.y);
                ctx.rotate(-Math.PI / 2 + Math.PI * params.needle.percent);
                // draw circle
                ctx.beginPath();
                ctx.fillStyle = params.needle.backgroundColor;
                ctx.strokeStyle = params.needle.borderColor;
                ctx.ellipse(0, 0, params.needle.width / 2, params.needle.width / 2, 0, 0, 2 * Math.PI);
                ctx.fill();
                ctx.stroke();
                ctx.closePath();
                // draw needle
                ctx.fillStyle = params.needle.backgroundColor;
                ctx.strokeStyle = params.needle.borderColor;
                ctx.beginPath();
                ctx.moveTo(-params.needle.width / 2, 0);
                ctx.lineTo(0, -params.needle.height + 10);
                ctx.lineTo(+params.needle.width / 2, 0);
                ctx.closePath();
                ctx.fill();
                ctx.stroke();
                ctx.restore();
            }
            computeRenderingParams() {
                const { ctx, config } = this.chart;
                const options = config.options;
                options.layout = options.layout || {};
                options.layout.padding = options.layout.padding || {};
                const formatter = this.valueLabelOptions.formatter;
                const fmt = typeof formatter === "function" ? formatter : (value) => value;
                const dataset = this.getDataset();
                // @ts-ignore value is a custom property for gauge charts
                const value = dataset.value || 0;
                const valueText = fmt(value).toString();
                const percent = this.getValueAngleInPercent(value);
                const fontFamily = this.valueLabelOptions.font?.family || "Arial";
                const optionsFontSize = this.valueLabelOptions.font?.size || 0;
                const fontSize = optionsFontSize >= 1 ? optionsFontSize : 30;
                const font = `${fontSize}px ${fontFamily}`;
                const padding = {
                    right: 0,
                    left: 0,
                    top: 0,
                    bottom: 0,
                    ...this.valueLabelOptions.padding,
                };
                padding.left = padding.left >= 0 ? padding.left : 10;
                padding.right = padding.right >= 0 ? padding.right : 10;
                padding.top = padding.top >= 0 ? padding.top : 10;
                padding.bottom = padding.bottom >= 0 ? padding.bottom : 10;
                const chartArea = this.chart.chartArea;
                const offsetX = this.offsetX || 0;
                const offsetY = this.offsetY || 0;
                const centerX = (chartArea.left + chartArea.right) / 2;
                const centerY = (chartArea.top + chartArea.bottom) / 2;
                const center = {
                    x: centerX + offsetX,
                    y: centerY + offsetY,
                };
                const textPosition = {
                    x: center.x,
                    y: center.y,
                };
                ctx.save();
                ctx.font = font;
                const metrics = ctx.measureText(valueText);
                ctx.restore();
                const textHeight = metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent;
                return {
                    needle: {
                        borderColor: this.needleOptions.borderColor || "#000",
                        backgroundColor: this.needleOptions.backgroundColor || "#000",
                        width: this.needleOptions.width || 10,
                        height: Math.min(this.chartHeight, this.chartWidth / 2),
                        position: {
                            x: textPosition.x,
                            y: textPosition.y,
                        },
                        percent,
                    },
                    valueLabel: {
                        textColor: this.valueLabelOptions.font?.color || "#FFF",
                        backgroundColor: this.valueLabelOptions.backgroundColor || "#000",
                        borderColor: this.valueLabelOptions.borderColor || "#000",
                        borderRadius: this.valueLabelOptions.borderRadius || 10,
                        rect: {
                            x: center.x - metrics.width / 2 - padding.left,
                            y: center.y + textHeight / 2 + padding.top,
                            width: metrics.width + padding.left + padding.right,
                            height: -(textHeight + 6 + padding.top + padding.bottom),
                        },
                        valueText,
                        textPosition,
                        textHeight,
                        font,
                    },
                };
            }
            draw() {
                super.draw();
                const params = this.computeRenderingParams();
                this.drawNeedle(params);
                this.drawValueLabel(params);
            }
            updateElements(elements, start, count, mode) {
                super.updateElements(elements, start, count, mode);
                const dataset = this.getDataset();
                const data = this.getDataset().data;
                // @ts-ignore
                const minValue = dataset.minValue;
                // @ts-ignore
                const rotation = this.chart.options.rotation || 0;
                // @ts-ignore
                const circumference = this.chart.options.circumference || 0;
                for (let arcIndex = 0; arcIndex < data.length; arcIndex++) {
                    const previousValue = arcIndex === 0 ? minValue : data[arcIndex - 1];
                    const startAngleInPercent = this.getValueAngleInPercent(previousValue);
                    const endAngleInPercent = this.getValueAngleInPercent(data[arcIndex]);
                    const startAngle = degreesToRadians(rotation + circumference * startAngleInPercent) - Math.PI / 2;
                    const endAngle = degreesToRadians(rotation + circumference * endAngleInPercent) - Math.PI / 2;
                    const arcCircumference = endAngle - startAngle;
                    const arc = elements[arcIndex];
                    const propertiesUpdates = {
                        startAngle,
                        endAngle,
                        circumference: arcCircumference,
                    };
                    this.updateElement(arc, arcIndex, propertiesUpdates, mode);
                }
            }
        }
        function degreesToRadians(degrees) {
            return (degrees * Math.PI) / 180;
        }
        // @ts-ignore
        window.Chart.register(GaugeController);
    }

    //------------------------------------------------------------------------------
    // Helpers
    //------------------------------------------------------------------------------
    function setFormatter(env, format) {
        env.model.dispatch("CANCEL_EDITION");
        env.model.dispatch("SET_FORMATTING", {
            sheetId: env.model.getters.getActiveSheetId(),
            target: env.model.getters.getSelectedZones(),
            format,
        });
    }
    function setStyle(env, style) {
        env.model.dispatch("SET_FORMATTING", {
            sheetId: env.model.getters.getActiveSheetId(),
            target: env.model.getters.getSelectedZones(),
            style,
        });
    }
    //------------------------------------------------------------------------------
    // Simple actions
    //------------------------------------------------------------------------------
    const PASTE_ACTION = async (env) => paste$1(env);
    const PASTE_VALUE_ACTION = async (env) => paste$1(env, "onlyValue");
    async function paste$1(env, pasteOption) {
        const spreadsheetClipboard = env.model.getters.getClipboardTextContent();
        const osClipboard = await env.clipboard.readText();
        switch (osClipboard.status) {
            case "ok":
                const target = env.model.getters.getSelectedZones();
                if (osClipboard && osClipboard.content !== spreadsheetClipboard) {
                    interactivePasteFromOS(env, target, osClipboard.content, pasteOption);
                }
                else {
                    interactivePaste(env, target, pasteOption);
                }
                if (env.model.getters.isCutOperation() && pasteOption !== "onlyValue") {
                    await env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
                }
                break;
            case "notImplemented":
                env.raiseError(_t("Pasting from the context menu is not supported in this browser. Use keyboard shortcuts ctrl+c / ctrl+v instead."));
                break;
            case "permissionDenied":
                env.raiseError(_t("Access to the clipboard denied by the browser. Please enable clipboard permission for this page in your browser settings."));
                break;
        }
    }
    const PASTE_FORMAT_ACTION = (env) => paste$1(env, "onlyFormat");
    //------------------------------------------------------------------------------
    // Grid manipulations
    //------------------------------------------------------------------------------
    const DELETE_CONTENT_ROWS_NAME = (env) => {
        if (env.model.getters.getSelectedZones().length > 1) {
            return _t("Clear rows");
        }
        let first;
        let last;
        const activesRows = env.model.getters.getActiveRows();
        if (activesRows.size !== 0) {
            first = Math.min(...activesRows);
            last = Math.max(...activesRows);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.top;
            last = zone.bottom;
        }
        if (first === last) {
            return _t("Clear row %s", (first + 1).toString());
        }
        return _t("Clear rows %s - %s", (first + 1).toString(), (last + 1).toString());
    };
    const DELETE_CONTENT_ROWS_ACTION = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const target = [...env.model.getters.getActiveRows()].map((index) => env.model.getters.getRowsZone(sheetId, index, index));
        env.model.dispatch("DELETE_CONTENT", {
            target,
            sheetId: env.model.getters.getActiveSheetId(),
        });
    };
    const DELETE_CONTENT_COLUMNS_NAME = (env) => {
        if (env.model.getters.getSelectedZones().length > 1) {
            return _t("Clear columns");
        }
        let first;
        let last;
        const activeCols = env.model.getters.getActiveCols();
        if (activeCols.size !== 0) {
            first = Math.min(...activeCols);
            last = Math.max(...activeCols);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.left;
            last = zone.right;
        }
        if (first === last) {
            return _t("Clear column %s", numberToLetters(first));
        }
        return _t("Clear columns %s - %s", numberToLetters(first), numberToLetters(last));
    };
    const DELETE_CONTENT_COLUMNS_ACTION = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const target = [...env.model.getters.getActiveCols()].map((index) => env.model.getters.getColsZone(sheetId, index, index));
        env.model.dispatch("DELETE_CONTENT", {
            target,
            sheetId: env.model.getters.getActiveSheetId(),
        });
    };
    const REMOVE_ROWS_NAME = (env) => {
        if (env.model.getters.getSelectedZones().length > 1) {
            return _t("Delete rows");
        }
        let first;
        let last;
        const activesRows = env.model.getters.getActiveRows();
        if (activesRows.size !== 0) {
            first = Math.min(...activesRows);
            last = Math.max(...activesRows);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.top;
            last = zone.bottom;
        }
        if (first === last) {
            return _t("Delete row %s", (first + 1).toString());
        }
        return _t("Delete rows %s - %s", (first + 1).toString(), (last + 1).toString());
    };
    const REMOVE_ROWS_ACTION = (env) => {
        let rows = [...env.model.getters.getActiveRows()];
        if (!rows.length) {
            const zone = env.model.getters.getSelectedZones()[0];
            for (let i = zone.top; i <= zone.bottom; i++) {
                rows.push(i);
            }
        }
        env.model.dispatch("REMOVE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            dimension: "ROW",
            elements: rows,
        });
    };
    const CAN_REMOVE_COLUMNS_ROWS = (dimension, env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const selectedElements = env.model.getters.getElementsFromSelection(dimension);
        const includesAllVisibleHeaders = env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, dimension, selectedElements);
        const includesAllNonFrozenHeaders = env.model.getters.checkElementsIncludeAllNonFrozenHeaders(sheetId, dimension, selectedElements);
        return !includesAllVisibleHeaders && !includesAllNonFrozenHeaders;
    };
    const REMOVE_COLUMNS_NAME = (env) => {
        if (env.model.getters.getSelectedZones().length > 1) {
            return _t("Delete columns");
        }
        let first;
        let last;
        const activeCols = env.model.getters.getActiveCols();
        if (activeCols.size !== 0) {
            first = Math.min(...activeCols);
            last = Math.max(...activeCols);
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            first = zone.left;
            last = zone.right;
        }
        if (first === last) {
            return _t("Delete column %s", numberToLetters(first));
        }
        return _t("Delete columns %s - %s", numberToLetters(first), numberToLetters(last));
    };
    const NOT_ALL_VISIBLE_ROWS_SELECTED = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const selectedRows = env.model.getters.getElementsFromSelection("ROW");
        return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, "ROW", selectedRows);
    };
    const REMOVE_COLUMNS_ACTION = (env) => {
        let columns = [...env.model.getters.getActiveCols()];
        if (!columns.length) {
            const zone = env.model.getters.getSelectedZones()[0];
            for (let i = zone.left; i <= zone.right; i++) {
                columns.push(i);
            }
        }
        env.model.dispatch("REMOVE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            dimension: "COL",
            elements: columns,
        });
    };
    const NOT_ALL_VISIBLE_COLS_SELECTED = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const selectedCols = env.model.getters.getElementsFromSelection("COL");
        return !env.model.getters.checkElementsIncludeAllVisibleHeaders(sheetId, "COL", selectedCols);
    };
    const INSERT_ROWS_BEFORE_ACTION = (env) => {
        const activeRows = env.model.getters.getActiveRows();
        let row;
        let quantity;
        if (activeRows.size) {
            row = Math.min(...activeRows);
            quantity = activeRows.size;
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            row = zone.top;
            quantity = zone.bottom - zone.top + 1;
        }
        env.model.dispatch("ADD_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            position: "before",
            base: row,
            quantity,
            dimension: "ROW",
        });
    };
    const INSERT_ROWS_AFTER_ACTION = (env) => {
        const activeRows = env.model.getters.getActiveRows();
        let row;
        let quantity;
        if (activeRows.size) {
            row = Math.max(...activeRows);
            quantity = activeRows.size;
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            row = zone.bottom;
            quantity = zone.bottom - zone.top + 1;
        }
        env.model.dispatch("ADD_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            position: "after",
            base: row,
            quantity,
            dimension: "ROW",
        });
    };
    const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
        const activeCols = env.model.getters.getActiveCols();
        let column;
        let quantity;
        if (activeCols.size) {
            column = Math.min(...activeCols);
            quantity = activeCols.size;
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            column = zone.left;
            quantity = zone.right - zone.left + 1;
        }
        env.model.dispatch("ADD_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            position: "before",
            dimension: "COL",
            base: column,
            quantity,
        });
    };
    const INSERT_COLUMNS_AFTER_ACTION = (env) => {
        const activeCols = env.model.getters.getActiveCols();
        let column;
        let quantity;
        if (activeCols.size) {
            column = Math.max(...activeCols);
            quantity = activeCols.size;
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            column = zone.right;
            quantity = zone.right - zone.left + 1;
        }
        env.model.dispatch("ADD_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
            position: "after",
            dimension: "COL",
            base: column,
            quantity,
        });
    };
    const HIDE_COLUMNS_NAME = (env) => {
        const cols = env.model.getters.getElementsFromSelection("COL");
        let first = cols[0];
        let last = cols[cols.length - 1];
        if (cols.length === 1) {
            return _t("Hide column %s", numberToLetters(first).toString());
        }
        else if (last - first + 1 === cols.length) {
            return _t("Hide columns %s - %s", numberToLetters(first).toString(), numberToLetters(last).toString());
        }
        else {
            return _t("Hide columns");
        }
    };
    const HIDE_ROWS_NAME = (env) => {
        const rows = env.model.getters.getElementsFromSelection("ROW");
        let first = rows[0];
        let last = rows[rows.length - 1];
        if (rows.length === 1) {
            return _t("Hide row %s", (first + 1).toString());
        }
        else if (last - first + 1 === rows.length) {
            return _t("Hide rows %s - %s", (first + 1).toString(), (last + 1).toString());
        }
        else {
            return _t("Hide rows");
        }
    };
    //------------------------------------------------------------------------------
    // Charts
    //------------------------------------------------------------------------------
    const CREATE_CHART = (env) => {
        const getters = env.model.getters;
        const id = env.model.uuidGenerator.uuidv4();
        const sheetId = getters.getActiveSheetId();
        if (getZoneArea(env.model.getters.getSelectedZone()) === 1) {
            env.model.selection.selectTableAroundSelection();
        }
        const size = { width: DEFAULT_FIGURE_WIDTH, height: DEFAULT_FIGURE_HEIGHT };
        const position = getChartPositionAtCenterOfViewport(getters, size);
        const result = env.model.dispatch("CREATE_CHART", {
            sheetId,
            id,
            position,
            size,
            definition: getSmartChartDefinition(env.model.getters.getSelectedZone(), env.model.getters),
        });
        if (result.isSuccessful) {
            env.model.dispatch("SELECT_FIGURE", { id });
            env.openSidePanel("ChartPanel");
        }
    };
    //------------------------------------------------------------------------------
    // Image
    //------------------------------------------------------------------------------
    async function requestImage(env) {
        try {
            return await env.imageProvider.requestImage();
        }
        catch {
            env.raiseError(_t("An unexpected error occurred during the image transfer"));
            return undefined;
        }
    }
    const CREATE_IMAGE = async (env) => {
        if (env.imageProvider) {
            const sheetId = env.model.getters.getActiveSheetId();
            const figureId = env.model.uuidGenerator.uuidv4();
            const image = await requestImage(env);
            if (!image) {
                throw new Error("No image provider was given to the environment");
            }
            const size = getMaxFigureSize(env.model.getters, image.size);
            const position = centerFigurePosition(env.model.getters, size);
            env.model.dispatch("CREATE_IMAGE", {
                sheetId,
                figureId,
                position,
                size,
                definition: image,
            });
        }
    };
    //------------------------------------------------------------------------------
    // Style/Format
    //------------------------------------------------------------------------------
    const FORMAT_PERCENT_ACTION = (env) => setFormatter(env, "0.00%");
    //------------------------------------------------------------------------------
    // Side panel
    //------------------------------------------------------------------------------
    const OPEN_CF_SIDEPANEL_ACTION = (env) => {
        env.openSidePanel("ConditionalFormatting", { selection: env.model.getters.getSelectedZones() });
    };
    const INSERT_LINK = (env) => {
        let { col, row } = env.model.getters.getActivePosition();
        env.model.dispatch("OPEN_CELL_POPOVER", { col, row, popoverType: "LinkEditor" });
    };
    const INSERT_LINK_NAME = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const { col, row } = env.model.getters.getActivePosition();
        const cell = env.model.getters.getEvaluatedCell({ sheetId, col, row });
        return cell && cell.link ? _t("Edit link") : _t("Insert link");
    };
    //------------------------------------------------------------------------------
    // Filters action
    //------------------------------------------------------------------------------
    const SELECTION_CONTAINS_FILTER = (env) => {
        const sheetId = env.model.getters.getActiveSheetId();
        const selectedZones = env.model.getters.getSelectedZones();
        return env.model.getters.doesZonesContainFilter(sheetId, selectedZones);
    };
    //------------------------------------------------------------------------------
    // Sorting action
    //------------------------------------------------------------------------------
    const IS_ONLY_ONE_RANGE = (env) => {
        return env.model.getters.getSelectedZones().length === 1;
    };
    const CAN_INSERT_HEADER = (env, dimension) => {
        if (!IS_ONLY_ONE_RANGE(env)) {
            return false;
        }
        const activeHeaders = dimension === "COL" ? env.model.getters.getActiveCols() : env.model.getters.getActiveRows();
        const ortogonalActiveHeaders = dimension === "COL" ? env.model.getters.getActiveRows() : env.model.getters.getActiveCols();
        const sheetId = env.model.getters.getActiveSheetId();
        const zone = env.model.getters.getSelectedZone();
        const allSheetSelected = isEqual(zone, env.model.getters.getSheetZone(sheetId));
        return isConsecutive(activeHeaders) && (ortogonalActiveHeaders.size === 0 || allSheetSelected);
    };

    const undo = {
        name: _t("Undo"),
        description: "Ctrl+Z",
        execute: (env) => env.model.dispatch("REQUEST_UNDO"),
        isEnabled: (env) => env.model.getters.canUndo(),
        icon: "o-spreadsheet-Icon.UNDO",
    };
    const redo = {
        name: _t("Redo"),
        description: "Ctrl+Y",
        execute: (env) => env.model.dispatch("REQUEST_REDO"),
        isEnabled: (env) => env.model.getters.canRedo(),
        icon: "o-spreadsheet-Icon.REDO",
    };
    const copy = {
        name: _t("Copy"),
        description: "Ctrl+C",
        isReadonlyAllowed: true,
        execute: async (env) => {
            env.model.dispatch("COPY");
            await env.clipboard.write(env.model.getters.getClipboardContent());
        },
        icon: "o-spreadsheet-Icon.COPY",
    };
    const cut = {
        name: _t("Cut"),
        description: "Ctrl+X",
        execute: async (env) => {
            interactiveCut(env);
            await env.clipboard.write(env.model.getters.getClipboardContent());
        },
        icon: "o-spreadsheet-Icon.CUT",
    };
    const paste = {
        name: _t("Paste"),
        description: "Ctrl+V",
        execute: PASTE_ACTION,
        icon: "o-spreadsheet-Icon.PASTE",
    };
    const pasteSpecial = {
        name: _t("Paste special"),
        isVisible: (env) => {
            return !env.model.getters.isCutOperation();
        },
        icon: "o-spreadsheet-Icon.PASTE",
    };
    const pasteSpecialValue = {
        name: _t("Paste value only"),
        description: "Ctrl+Shift+V",
        execute: PASTE_VALUE_ACTION,
    };
    const pasteSpecialFormat = {
        name: _t("Paste format only"),
        execute: PASTE_FORMAT_ACTION,
    };
    const findAndReplace = {
        name: _t("Find and replace"),
        description: "Ctrl+H",
        isReadonlyAllowed: true,
        execute: (env) => {
            env.openSidePanel("FindAndReplace", {});
        },
        icon: "o-spreadsheet-Icon.FIND_AND_REPLACE",
    };
    const deleteValues = {
        name: _t("Delete values"),
        execute: (env) => env.model.dispatch("DELETE_CONTENT", {
            sheetId: env.model.getters.getActiveSheetId(),
            target: env.model.getters.getSelectedZones(),
        }),
    };
    const deleteRows = {
        name: REMOVE_ROWS_NAME,
        execute: REMOVE_ROWS_ACTION,
        isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("ROW", env),
    };
    const deleteRow = {
        ...deleteRows,
        isVisible: IS_ONLY_ONE_RANGE,
    };
    const clearRows = {
        name: DELETE_CONTENT_ROWS_NAME,
        execute: DELETE_CONTENT_ROWS_ACTION,
    };
    const deleteCols = {
        name: REMOVE_COLUMNS_NAME,
        execute: REMOVE_COLUMNS_ACTION,
        isVisible: (env) => CAN_REMOVE_COLUMNS_ROWS("COL", env),
    };
    const deleteCol = {
        ...deleteCols,
        isVisible: IS_ONLY_ONE_RANGE,
    };
    const clearCols = {
        name: DELETE_CONTENT_COLUMNS_NAME,
        execute: DELETE_CONTENT_COLUMNS_ACTION,
    };
    const deleteCells = {
        name: _t("Delete cells"),
        isVisible: IS_ONLY_ONE_RANGE,
    };
    const deleteCellShiftUp = {
        name: _t("Delete cell and shift up"),
        execute: (env) => {
            const zone = env.model.getters.getSelectedZone();
            const result = env.model.dispatch("DELETE_CELL", { zone, shiftDimension: "ROW" });
            handlePasteResult(env, result);
        },
    };
    const deleteCellShiftLeft = {
        name: _t("Delete cell and shift left"),
        execute: (env) => {
            const zone = env.model.getters.getSelectedZone();
            const result = env.model.dispatch("DELETE_CELL", { zone, shiftDimension: "COL" });
            handlePasteResult(env, result);
        },
    };
    const mergeCells = {
        name: _t("Merge cells"),
        isEnabled: (env) => !cannotMerge(env),
        isActive: (env) => isInMerge(env),
        execute: (env) => toggleMerge(env),
        icon: "o-spreadsheet-Icon.MERGE_CELL",
    };
    function cannotMerge(env) {
        const zones = env.model.getters.getSelectedZones();
        const { top, left, right, bottom } = env.model.getters.getSelectedZone();
        const { sheetId } = env.model.getters.getActivePosition();
        const { xSplit, ySplit } = env.model.getters.getPaneDivisions(sheetId);
        return (zones.length > 1 ||
            (top === bottom && left === right) ||
            (left < xSplit && xSplit <= right) ||
            (top < ySplit && ySplit <= bottom));
    }
    function isInMerge(env) {
        if (!cannotMerge(env)) {
            const zones = env.model.getters.getSelectedZones();
            const { col, row, sheetId } = env.model.getters.getActivePosition();
            const zone = env.model.getters.expandZone(sheetId, positionToZone({ col, row }));
            return isEqual(zones[0], zone);
        }
        return false;
    }
    function toggleMerge(env) {
        if (cannotMerge(env)) {
            return;
        }
        const zones = env.model.getters.getSelectedZones();
        const target = [zones[zones.length - 1]];
        const sheetId = env.model.getters.getActiveSheetId();
        if (isInMerge(env)) {
            env.model.dispatch("REMOVE_MERGE", { sheetId, target });
        }
        else {
            interactiveAddMerge(env, sheetId, target);
        }
    }

    var ACTION_EDIT = /*#__PURE__*/Object.freeze({
        __proto__: null,
        clearCols: clearCols,
        clearRows: clearRows,
        copy: copy,
        cut: cut,
        deleteCellShiftLeft: deleteCellShiftLeft,
        deleteCellShiftUp: deleteCellShiftUp,
        deleteCells: deleteCells,
        deleteCol: deleteCol,
        deleteCols: deleteCols,
        deleteRow: deleteRow,
        deleteRows: deleteRows,
        deleteValues: deleteValues,
        findAndReplace: findAndReplace,
        mergeCells: mergeCells,
        paste: paste,
        pasteSpecial: pasteSpecial,
        pasteSpecialFormat: pasteSpecialFormat,
        pasteSpecialValue: pasteSpecialValue,
        redo: redo,
        undo: undo
    });

    //------------------------------------------------------------------------------
    // Arg description DSL
    //------------------------------------------------------------------------------
    const ARG_REGEXP = /(.*?)\((.*?)\)(.*)/;
    const ARG_TYPES = [
        "ANY",
        "BOOLEAN",
        "DATE",
        "NUMBER",
        "STRING",
        "RANGE",
        "RANGE<BOOLEAN>",
        "RANGE<DATE>",
        "RANGE<NUMBER>",
        "RANGE<STRING>",
        "META",
    ];
    function arg(definition, description = "") {
        return makeArg(definition, description);
    }
    function makeArg(str, description) {
        let parts = str.match(ARG_REGEXP);
        let name = parts[1].trim();
        if (!name) {
            throw new Error(`Function argument definition is missing a name: '${str}'.`);
        }
        let types = [];
        let isOptional = false;
        let isRepeating = false;
        let isLazy = false;
        let defaultValue;
        for (let param of parts[2].split(",")) {
            const key = param.trim().toUpperCase();
            let type = ARG_TYPES.find((t) => key === t);
            if (type) {
                types.push(type);
            }
            else if (key === "RANGE<ANY>") {
                types.push("RANGE");
            }
            else if (key === "OPTIONAL") {
                isOptional = true;
            }
            else if (key === "REPEATING") {
                isRepeating = true;
            }
            else if (key === "LAZY") {
                isLazy = true;
            }
            else if (key.startsWith("DEFAULT=")) {
                defaultValue = param.trim().slice(8);
            }
        }
        const result = {
            name,
            description,
            type: types,
        };
        if (isOptional) {
            result.optional = true;
        }
        if (isRepeating) {
            result.repeating = true;
        }
        if (isLazy) {
            result.lazy = true;
        }
        if (defaultValue !== undefined) {
            result.default = true;
            result.defaultValue = defaultValue;
        }
        return result;
    }
    /**
     * This function adds on description more general information derived from the
     * arguments.
     *
     * This information is useful during compilation.
     */
    function addMetaInfoFromArg(addDescr) {
        let countArg = 0;
        let minArg = 0;
        let repeatingArg = 0;
        for (let arg of addDescr.args) {
            countArg++;
            if (!arg.optional && !arg.repeating && !arg.default) {
                minArg++;
            }
            if (arg.repeating) {
                repeatingArg++;
            }
        }
        const descr = addDescr;
        descr.minArgRequired = minArg;
        descr.maxArgPossible = repeatingArg ? Infinity : countArg;
        descr.nbrArgRepeating = repeatingArg;
        descr.getArgToFocus = argTargeting(countArg, repeatingArg);
        descr.hidden = addDescr.hidden || false;
        return descr;
    }
    /**
     * Returns a function allowing finding which argument corresponds a position
     * in a function. This is particularly useful for functions with repeatable
     * arguments.
     *
     * Indeed the function makes it possible to etablish corespondance between
     * arguments when the number of arguments supplied is greater than the number of
     * arguments defined by the function.
     *
     * Ex:
     *
     * in the formula "=SUM(11, 55, 66)" which is defined like this "SUM(value1, [value2, ...])"
     * - 11 corresponds to the value1 argument => position will be 1
     * - 55 corresponds to the [value2, ...] argument => position will be 2
     * - 66 corresponds to the [value2, ...] argument => position will be 2
     *
     * in the formula "=AVERAGE.WEIGHTED(1, 2, 3, 4, 5, 6)" which is defined like this
     * "AVERAGE.WEIGHTED(values, weights, [additional_values, ...], [additional_weights, ...])"
     * - 1 corresponds to the values argument => position will be 1
     * - 2 corresponds to the weights argument => position will be 2
     * - 3 corresponds to the [additional_values, ...] argument => position will be 3
     * - 4 corresponds to the [additional_weights, ...] argument => position will be 4
     * - 5 corresponds to the [additional_values, ...] argument => position will be 3
     * - 6 corresponds to the [additional_weights, ...] argument => position will be 4
     */
    function argTargeting(countArg, repeatingArg) {
        if (!repeatingArg) {
            return (argPosition) => argPosition;
        }
        if (repeatingArg === 1) {
            return (argPosition) => Math.min(argPosition, countArg);
        }
        const argBeforeRepeat = countArg - repeatingArg;
        return (argPosition) => {
            if (argPosition <= argBeforeRepeat) {
                return argPosition;
            }
            const argAfterRepeat = (argPosition - argBeforeRepeat) % repeatingArg || repeatingArg;
            return argBeforeRepeat + argAfterRepeat;
        };
    }
    //------------------------------------------------------------------------------
    // Argument validation
    //------------------------------------------------------------------------------
    function validateArguments(args) {
        let previousArgRepeating = false;
        let previousArgOptional = false;
        let previousArgDefault = false;
        for (let current of args) {
            if (current.type.includes("META") && current.type.length > 1) {
                throw new Error(_t("Function ${name} has an argument that has been declared with more than one type whose type 'META'. The 'META' type can only be declared alone."));
            }
            if (previousArgRepeating && !current.repeating) {
                throw new Error(_t("Function ${name} has no-repeatable arguments declared after repeatable ones. All repeatable arguments must be declared last."));
            }
            const previousIsOptional = previousArgOptional || previousArgRepeating || previousArgDefault;
            const currentIsntOptional = !(current.optional || current.repeating || current.default);
            if (previousIsOptional && currentIsntOptional) {
                throw new Error(_t("Function ${name} has at mandatory arguments declared after optional ones. All optional arguments must be after all mandatory arguments."));
            }
            previousArgRepeating = current.repeating;
            previousArgOptional = current.optional;
            previousArgDefault = current.default;
        }
    }

    function assertSingleColOrRow(errorStr, arg) {
        assert(() => arg.length === 1 || arg[0].length === 1, errorStr);
    }
    function assertSameDimensions(errorStr, ...args) {
        if (args.every(isMatrix)) {
            const cols = args[0].length;
            const rows = args[0][0].length;
            for (const arg of args) {
                assert(() => arg.length === cols && arg[0].length === rows, errorStr);
            }
            return;
        }
        if (args.some((arg) => Array.isArray(arg) && (arg.length !== 1 || arg[0].length !== 1))) {
            throw new Error(errorStr);
        }
    }
    function assertPositive(errorStr, arg) {
        assert(() => arg > 0, errorStr);
    }
    function assertSquareMatrix(errorStr, arg) {
        assert(() => arg.length === arg[0].length, errorStr);
    }
    function isNumberMatrix(arg) {
        return arg.every((row) => row.every((val) => typeof val === "number"));
    }

    function getUnitMatrix(n) {
        const matrix = Array(n);
        for (let i = 0; i < n; i++) {
            matrix[i] = Array(n).fill(0);
            matrix[i][i] = 1;
        }
        return matrix;
    }
    /**
     * Invert a matrix and compute its determinant using Gaussian Elimination.
     *
     * The Matrix should be a square matrix, and should be indexed [col][row] instead of the
     * standard mathematical indexing [row][col].
     */
    function invertMatrix(M) {
        // Use Gaussian Elimination to calculate the inverse:
        // (1) 'augment' the matrix (left) by the identity (on the right)
        // (2) Turn the matrix on the left into the identity using elementary row operations
        // (3) The matrix on the right becomes the inverse (was the identity matrix)
        //
        // There are 3 elementary row operations:
        // (a) Swap 2 rows. This multiply the determinant by -1.
        // (b) Multiply a row by a scalar. This multiply the determinant by that scalar.
        // (c) Add to a row a multiple of another row. This does not change the determinant.
        if (M.length !== M[0].length) {
            throw new Error(`Function [[FUNCTION_NAME]] invert matrix error, only square matrices are invertible`);
        }
        let determinant = 1;
        const dim = M.length;
        const I = getUnitMatrix(dim);
        const C = M.map((row) => row.slice());
        // Perform elementary row operations
        for (let pivot = 0; pivot < dim; pivot++) {
            let diagonalElement = C[pivot][pivot];
            // if we have a 0 on the diagonal we'll need to swap with a lower row
            if (diagonalElement === 0) {
                //look through every row below the i'th row
                for (let row = pivot + 1; row < dim; row++) {
                    //if the ii'th row has a non-0 in the i'th col, swap it with that row
                    if (C[pivot][row] != 0) {
                        swapMatrixRows(C, pivot, row);
                        swapMatrixRows(I, pivot, row);
                        determinant *= -1;
                        break;
                    }
                }
                diagonalElement = C[pivot][pivot];
                //if it's still 0, matrix isn't invertible
                if (diagonalElement === 0) {
                    return { determinant: 0 };
                }
            }
            // Scale this row down by e (so we have a 1 on the diagonal)
            for (let col = 0; col < dim; col++) {
                C[col][pivot] = C[col][pivot] / diagonalElement;
                I[col][pivot] = I[col][pivot] / diagonalElement;
            }
            determinant *= diagonalElement;
            // Subtract a multiple of the current row from ALL of
            // the other rows so that there will be 0's in this column in the
            // rows above and below this one
            for (let row = 0; row < dim; row++) {
                if (row === pivot) {
                    continue;
                }
                // We want to change this element to 0
                const e = C[pivot][row];
                // Subtract (the row above(or below) scaled by e) from (the
                // current row) but start at the i'th column and assume all the
                // stuff left of diagonal is 0 (which it should be if we made this
                // algorithm correctly)
                for (let col = 0; col < dim; col++) {
                    C[col][row] -= e * C[col][pivot];
                    I[col][row] -= e * I[col][pivot];
                }
            }
        }
        // We've done all operations, C should be the identity matrix I should be the inverse
        return { inverted: I, determinant };
    }
    function swapMatrixRows(matrix, row1, row2) {
        for (let i = 0; i < matrix.length; i++) {
            const tmp = matrix[i][row1];
            matrix[i][row1] = matrix[i][row2];
            matrix[i][row2] = tmp;
        }
    }
    /**
     * Matrix multiplication of 2 matrices.
     * ex: matrix1 : n x l, matrix2 : m x n => result : m x l
     *
     * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col]
     */
    function multiplyMatrices(matrix1, matrix2) {
        if (matrix1.length !== matrix2[0].length) {
            throw new Error(_t("Cannot multiply matrices : incompatible matrices size."));
        }
        const rowsM1 = matrix1[0].length;
        const colsM2 = matrix2.length;
        const n = matrix1.length;
        const result = Array(colsM2);
        for (let col = 0; col < colsM2; col++) {
            result[col] = Array(rowsM1);
            for (let row = 0; row < rowsM1; row++) {
                let sum = 0;
                for (let k = 0; k < n; k++) {
                    sum += matrix1[k][row] * matrix2[col][k];
                }
                result[col][row] = sum;
            }
        }
        return result;
    }
    /**
     * Return the input if it's a scalar or the first element of the input if it's a matrix.
     */
    function toScalar(matrix) {
        if (!isMatrix(matrix)) {
            return matrix;
        }
        if (matrix.length !== 1 || matrix[0].length !== 1) {
            throw new Error("toScalar: matrix should be a scalar or a 1x1 matrix");
        }
        return matrix[0][0];
    }

    // -----------------------------------------------------------------------------
    // ARRAY_CONSTRAIN
    // -----------------------------------------------------------------------------
    const ARRAY_CONSTRAIN = {
        description: _t("Returns a result array constrained to a specific width and height."),
        args: [
            arg("input_range (any, range<any>)", _t("The range to constrain.")),
            arg("rows (number)", _t("The number of rows in the constrained array.")),
            arg("columns (number)", _t("The number of columns in the constrained array.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (array, rows, columns) {
            const _array = toMatrix(array);
            const _rowsArg = toInteger(rows?.value, this.locale);
            const _columnsArg = toInteger(columns?.value, this.locale);
            assertPositive(_t("The rows argument (%s) must be strictly positive.", _rowsArg.toString()), _rowsArg);
            assertPositive(_t("The columns argument (%s) must be strictly positive.", _rowsArg.toString()), _columnsArg);
            const _nbRows = Math.min(_rowsArg, _array[0].length);
            const _nbColumns = Math.min(_columnsArg, _array.length);
            return generateMatrix(_nbColumns, _nbRows, (col, row) => _array[col][row]);
        },
        isExported: false,
    };
    // -----------------------------------------------------------------------------
    // CHOOSECOLS
    // -----------------------------------------------------------------------------
    const CHOOSECOLS = {
        description: _t("Creates a new array from the selected columns in the existing range."),
        args: [
            arg("array (any, range<any>)", _t("The array that contains the columns to be returned.")),
            arg("col_num (number, range<number>)", _t("The first column index of the columns to be returned.")),
            arg("col_num2 (number, range<number>, repeating)", _t("The columns indexes of the columns to be returned.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (array, ...columns) {
            const _array = toMatrix(array);
            const _columns = flattenRowFirst(columns, (item) => toInteger(item?.value, this.locale));
            assert(() => _columns.every((col) => col > 0 && col <= _array.length), _t("The columns arguments must be between 1 and %s (got %s).", _array.length.toString(), (_columns.find((col) => col <= 0 || col > _array.length) || 0).toString()));
            const result = Array(_columns.length);
            for (let col = 0; col < _columns.length; col++) {
                const colIndex = _columns[col] - 1; // -1 because columns arguments are 1-indexed
                result[col] = _array[colIndex];
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CHOOSEROWS
    // -----------------------------------------------------------------------------
    const CHOOSEROWS = {
        description: _t("Creates a new array from the selected rows in the existing range."),
        args: [
            arg("array (any, range<any>)", _t("The array that contains the rows to be returned.")),
            arg("row_num (number, range<number>)", _t("The first row index of the rows to be returned.")),
            arg("row_num2 (number, range<number>, repeating)", _t("The rows indexes of the rows to be returned.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (array, ...rows) {
            const _array = toMatrix(array);
            const _rows = flattenRowFirst(rows, (item) => toInteger(item?.value, this.locale));
            const _nbColumns = _array.length;
            assert(() => _rows.every((row) => row > 0 && row <= _array[0].length), _t("The rows arguments must be between 1 and %s (got %s).", _array[0].length.toString(), (_rows.find((row) => row <= 0 || row > _array[0].length) || 0).toString()));
            return generateMatrix(_nbColumns, _rows.length, (col, row) => _array[col][_rows[row] - 1]); // -1 because rows arguments are 1-indexed
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EXPAND
    // -----------------------------------------------------------------------------
    const EXPAND = {
        description: _t("Expands or pads an array to specified row and column dimensions."),
        args: [
            arg("array (any, range<any>)", _t("The array to expand.")),
            arg("rows (number)", _t("The number of rows in the expanded array. If missing, rows will not be expanded.")),
            arg("columns (number, optional)", _t("The number of columns in the expanded array. If missing, columns will not be expanded.")),
            arg("pad_with (any, default=0)", _t("The value with which to pad.")), // @compatibility: on Excel, pad with #N/A
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (arg, rows, columns, padWith = { value: 0 } // TODO : Replace with #N/A errors once it's supported
        ) {
            const _array = toMatrix(arg);
            const _nbRows = toInteger(rows?.value, this.locale);
            const _nbColumns = columns !== undefined ? toInteger(columns.value, this.local) : _array.length;
            assert(() => _nbRows >= _array[0].length, _t("The rows arguments (%s) must be greater or equal than the number of rows of the array.", _nbRows.toString()));
            assert(() => _nbColumns >= _array.length, _t("The columns arguments (%s) must be greater or equal than the number of columns of the array.", _nbColumns.toString()));
            return generateMatrix(_nbColumns, _nbRows, (col, row) => col >= _array.length || row >= _array[col].length ? padWith : _array[col][row]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FLATTEN
    // -----------------------------------------------------------------------------
    const FLATTEN = {
        description: _t("Flattens all the values from one or more ranges into a single column."),
        args: [
            arg("range (any, range<any>)", _t("The first range to flatten.")),
            arg("range2 (any, range<any>, repeating)", _t("Additional ranges to flatten.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (...ranges) {
            return [flattenRowFirst(ranges, (val) => (val === undefined ? { value: "" } : val))];
        },
        isExported: false,
    };
    // -----------------------------------------------------------------------------
    // FREQUENCY
    // -----------------------------------------------------------------------------
    const FREQUENCY = {
        description: _t("Calculates the frequency distribution of a range."),
        args: [
            arg("data (range<number>)", _t("The array of ranges containing the values to be counted.")),
            arg("classes (number, range<number>)", _t("The range containing the set of classes.")),
        ],
        returns: ["RANGE<NUMBER>"],
        compute: function (data, classes) {
            const _data = flattenRowFirst([data], (val) => val).filter((val) => typeof val === "number");
            const _classes = flattenRowFirst([classes], (val) => val).filter((val) => typeof val === "number");
            /**
             * Returns the frequency distribution of the data in the classes, ie. the number of elements in the range
             * between each classes.
             *
             * For example:
             * - data = [1, 3, 2, 5, 4]
             * - classes = [3, 5, 1]
             *
             * The result will be:
             * - 2 ==> number of elements 1 > el >= 3
             * - 2 ==> number of elements 3 > el >= 5
             * - 1 ==> number of elements <= 1
             * - 0 ==> number of elements > 5
             *
             * @compatibility: GSheet sort the input classes. We do the implemntation of Excel, where we kee the classes unsorted.
             */
            const sortedClasses = _classes
                .map((value, index) => ({ initialIndex: index, value, count: 0 }))
                .sort((a, b) => a.value - b.value);
            sortedClasses.push({ initialIndex: sortedClasses.length, value: Infinity, count: 0 });
            const sortedData = _data.sort((a, b) => a - b);
            let index = 0;
            for (const val of sortedData) {
                while (val > sortedClasses[index].value && index < sortedClasses.length - 1) {
                    index++;
                }
                sortedClasses[index].count++;
            }
            const result = sortedClasses
                .sort((a, b) => a.initialIndex - b.initialIndex)
                .map((val) => val.count);
            return [result];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // HSTACK
    // -----------------------------------------------------------------------------
    const HSTACK = {
        description: _t("Appends ranges horizontally and in sequence to return a larger array."),
        args: [
            arg("range1 (any, range<any>)", _t("The first range to be appended.")),
            arg("range2 (any, range<any>, repeating)", _t("Additional ranges to add to range1.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (...ranges) {
            const nbRows = Math.max(...ranges.map((r) => r?.[0]?.length ?? 0));
            const result = [];
            for (const range of ranges) {
                const _range = toMatrix(range);
                for (let col = 0; col < _range.length; col++) {
                    //TODO: fill with #N/A for unavailable values instead of zeroes
                    const array = Array(nbRows).fill({ value: null });
                    for (let row = 0; row < _range[col].length; row++) {
                        array[row] = _range[col][row];
                    }
                    result.push(array);
                }
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MDETERM
    // -----------------------------------------------------------------------------
    const MDETERM = {
        description: _t("Returns the matrix determinant of a square matrix."),
        args: [
            arg("square_matrix (number, range<number>)", _t("An range with an equal number of rows and columns representing a matrix whose determinant will be calculated.")),
        ],
        returns: ["NUMBER"],
        compute: function (matrix) {
            const _matrix = toMatrix(matrix);
            assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix);
            if (!isNumberMatrix(_matrix)) {
                throw new Error(_t("The argument square_matrix must be a matrix of numbers."));
            }
            const { determinant } = invertMatrix(_matrix);
            return determinant;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MINVERSE
    // -----------------------------------------------------------------------------
    const MINVERSE = {
        description: _t("Returns the multiplicative inverse of a square matrix."),
        args: [
            arg("square_matrix (number, range<number>)", _t("An range with an equal number of rows and columns representing a matrix whose multiplicative inverse will be calculated.")),
        ],
        returns: ["RANGE<NUMBER>"],
        compute: function (matrix) {
            const _matrix = toMatrix(matrix);
            assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix);
            if (!isNumberMatrix(_matrix)) {
                throw new Error(_t("The argument square_matrix must be a matrix of numbers."));
            }
            const { inverted } = invertMatrix(_matrix);
            if (!inverted) {
                throw new Error(_t("The matrix is not invertible."));
            }
            return inverted;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MMULT
    // -----------------------------------------------------------------------------
    const MMULT = {
        description: _t("Calculates the matrix product of two matrices."),
        args: [
            arg("matrix1 (number, range<number>)", _t("The first matrix in the matrix multiplication operation.")),
            arg("matrix2 (number, range<number>)", _t("The second matrix in the matrix multiplication operation.")),
        ],
        returns: ["RANGE<NUMBER>"],
        compute: function (matrix1, matrix2) {
            const _matrix1 = toMatrix(matrix1);
            const _matrix2 = toMatrix(matrix2);
            assert(() => _matrix1.length === _matrix2[0].length, _t("In [[FUNCTION_NAME]], the number of columns of the first matrix (%s) must be equal to the \
        number of rows of the second matrix (%s).", _matrix1.length.toString(), _matrix2[0].length.toString()));
            if (!isNumberMatrix(_matrix1) || !isNumberMatrix(_matrix2)) {
                throw new Error(_t("The arguments matrix1 and matrix2 must be matrices of numbers."));
            }
            return multiplyMatrices(_matrix1, _matrix2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMPRODUCT
    // -----------------------------------------------------------------------------
    const SUMPRODUCT = {
        description: _t("Calculates the sum of the products of corresponding entries in equal-sized ranges."),
        args: [
            arg("range1 (number, range<number>)", _t("The first range whose entries will be multiplied with corresponding entries in the other ranges.")),
            arg("range2 (number, range<number>, repeating)", _t("The other range whose entries will be multiplied with corresponding entries in the other ranges.")),
        ],
        returns: ["NUMBER"],
        compute: function (...args) {
            assertSameDimensions(_t("All the ranges must have the same dimensions."), ...args);
            const _args = args.map(toMatrix);
            let result = 0;
            for (let col = 0; col < _args[0].length; col++) {
                for (let row = 0; row < _args[0][col].length; row++) {
                    if (!_args.every((range) => typeof range[col][row] === "number")) {
                        continue;
                    }
                    let product = 1;
                    for (const range of _args) {
                        product *= toNumber(range[col][row], this.locale);
                    }
                    result += product;
                }
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMX2MY2
    // -----------------------------------------------------------------------------
    /**
     * Return the sum of the callback applied to each pair of values in the two arrays.
     *
     * Ignore the pairs X,Y where one of the value isn't a number. Throw an error if no pair of numbers is found.
     */
    function getSumXAndY(arrayX, arrayY, cb) {
        assertSameDimensions("The arguments array_x and array_y must have the same dimensions.", arrayX, arrayY);
        const _arrayX = toMatrix(arrayX);
        const _arrayY = toMatrix(arrayY);
        let validPairFound = false;
        let result = 0;
        for (const col in _arrayX) {
            for (const row in _arrayX[col]) {
                const arrayXValue = _arrayX[col][row];
                const arrayYValue = _arrayY[col][row];
                if (typeof arrayXValue !== "number" || typeof arrayYValue !== "number") {
                    continue;
                }
                validPairFound = true;
                result += cb(arrayXValue, arrayYValue);
            }
        }
        if (!validPairFound) {
            throw new Error("The arguments array_x and array_y must contain at least one pair of numbers.");
        }
        return result;
    }
    const SUMX2MY2 = {
        description: _t("Calculates the sum of the difference of the squares of the values in two array."),
        args: [
            arg("array_x (number, range<number>)", _t("The array or range of values whose squares will be reduced by the squares of corresponding entries in array_y and added together.")),
            arg("array_y (number, range<number>)", _t("The array or range of values whose squares will be subtracted from the squares of corresponding entries in array_x and added together.")),
        ],
        returns: ["NUMBER"],
        compute: function (arrayX, arrayY) {
            return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 - y ** 2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMX2PY2
    // -----------------------------------------------------------------------------
    const SUMX2PY2 = {
        description: _t("Calculates the sum of the sum of the squares of the values in two array."),
        args: [
            arg("array_x (number, range<number>)", _t("The array or range of values whose squares will be added to the squares of corresponding entries in array_y and added together.")),
            arg("array_y (number, range<number>)", _t("The array or range of values whose squares will be added to the squares of corresponding entries in array_x and added together.")),
        ],
        returns: ["NUMBER"],
        compute: function (arrayX, arrayY) {
            return getSumXAndY(arrayX, arrayY, (x, y) => x ** 2 + y ** 2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMXMY2
    // -----------------------------------------------------------------------------
    const SUMXMY2 = {
        description: _t("Calculates the sum of squares of the differences of values in two array."),
        args: [
            arg("array_x (number, range<number>)", _t("The array or range of values that will be reduced by corresponding entries in array_y, squared, and added together.")),
            arg("array_y (number, range<number>)", _t("The array or range of values that will be subtracted from corresponding entries in array_x, the result squared, and all such results added together.")),
        ],
        returns: ["NUMBER"],
        compute: function (arrayX, arrayY) {
            return getSumXAndY(arrayX, arrayY, (x, y) => (x - y) ** 2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TOCOL
    // -----------------------------------------------------------------------------
    const TO_COL_ROW_DEFAULT_IGNORE = 0;
    const TO_COL_ROW_DEFAULT_SCAN = false;
    const TO_COL_ROW_ARGS = [
        arg("array (any, range<any>)", _t("The array which will be transformed.")),
        arg(`ignore (number, default=${TO_COL_ROW_DEFAULT_IGNORE})`, _t("The control to ignore blanks and errors. 0 (default) is to keep all values, 1 is to ignore blanks, 2 is to ignore errors, and 3 is to ignore blanks and errors.")),
        arg(`scan_by_column (number, default=${TO_COL_ROW_DEFAULT_SCAN})`, _t("Whether the array should be scanned by column. True scans the array by column and false (default) \
      scans the array by row.")),
    ];
    const TOCOL = {
        description: _t("Transforms a range of cells into a single column."),
        args: TO_COL_ROW_ARGS,
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) {
            const _array = toMatrix(array);
            const _ignore = toInteger(ignore.value, this.locale);
            const _scanByColumn = toBoolean(scanByColumn.value);
            assert(() => _ignore >= 0 && _ignore <= 3, _t("Argument ignore must be between 0 and 3"));
            // TODO : implement ignore value 2 (ignore error) & 3 (ignore blanks and errors) once we can have errors in
            // the array w/o crashing
            const result = (_scanByColumn ? _array : transposeMatrix(_array))
                .flat()
                .filter((item) => (_ignore !== 1 && _ignore !== 3) || (item.value !== undefined && item.value !== null));
            if (result.length === 0) {
                throw new NotAvailableError(_t("No results for the given arguments of TOCOL."));
            }
            return [result];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TOROW
    // -----------------------------------------------------------------------------
    const TOROW = {
        description: _t("Transforms a range of cells into a single row."),
        args: TO_COL_ROW_ARGS,
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (array, ignore = { value: TO_COL_ROW_DEFAULT_IGNORE }, scanByColumn = { value: TO_COL_ROW_DEFAULT_SCAN }) {
            const _array = toMatrix(array);
            const _ignore = toInteger(ignore.value, this.locale);
            const _scanByColumn = toBoolean(scanByColumn.value);
            assert(() => _ignore >= 0 && _ignore <= 3, _t("Argument ignore must be between 0 and 3"));
            // TODO : implement ignore value 2 (ignore error) & 3 (ignore blanks and errors) once we can have errors in
            // the array w/o crashing
            const result = (_scanByColumn ? _array : transposeMatrix(_array))
                .flat()
                .filter((item) => (_ignore !== 1 && _ignore !== 3) || (item.value !== undefined && item.value !== null))
                .map((item) => [item]);
            if (result.length === 0 || result[0].length === 0) {
                throw new NotAvailableError(_t("No results for the given arguments of TOROW."));
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TRANSPOSE
    // -----------------------------------------------------------------------------
    const TRANSPOSE = {
        description: _t("Transposes the rows and columns of a range."),
        args: [arg("range (any, range<any>)", _t("The range to be transposed."))],
        returns: ["RANGE"],
        computeValueAndFormat: function (arg) {
            const _array = toMatrix(arg);
            const nbColumns = _array[0].length;
            const nbRows = _array.length;
            return generateMatrix(nbColumns, nbRows, (col, row) => _array[row][col]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VSTACK
    // -----------------------------------------------------------------------------
    const VSTACK = {
        description: _t("Appends ranges vertically and in sequence to return a larger array."),
        args: [
            arg("range1 (any, range<any>)", _t("The first range to be appended.")),
            arg("range2 (any, range<any>, repeating)", _t("Additional ranges to add to range1.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (...ranges) {
            const nbColumns = Math.max(...ranges.map((range) => toMatrix(range).length));
            const nbRows = ranges.reduce((acc, range) => acc + toMatrix(range)[0].length, 0);
            const result = Array(nbColumns)
                .fill([])
                .map(() => Array(nbRows).fill({ value: 0 })); // TODO fill with #N/A
            let currentRow = 0;
            for (const range of ranges) {
                const _array = toMatrix(range);
                for (let col = 0; col < _array.length; col++) {
                    for (let row = 0; row < _array[col].length; row++) {
                        result[col][currentRow + row] = _array[col][row];
                    }
                }
                currentRow += _array[0].length;
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // WRAPCOLS
    // -----------------------------------------------------------------------------
    const WRAPCOLS = {
        description: _t("Wraps the provided row or column of cells by columns after a specified number of elements to form a new array."),
        args: [
            arg("range (any, range<any>)", _t("The range to wrap.")),
            arg("wrap_count (number)", _t("The maximum number of cells for each column, rounded down to the nearest whole number.")),
            arg("pad_with  (any, default=0)", // TODO : replace with #N/A
            _t("The value with which to fill the extra cells in the range.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (range, wrapCount, padWith = { value: 0 }) {
            const _array = toMatrix(range);
            const nbRows = toInteger(wrapCount?.value, this.locale);
            assertSingleColOrRow(_t("Argument range must be a single row or column."), _array);
            const array = _array.flat();
            const nbColumns = Math.ceil(array.length / nbRows);
            return generateMatrix(nbColumns, nbRows, (col, row) => {
                const index = col * nbRows + row;
                return index < array.length ? array[index] : padWith;
            });
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // WRAPROWS
    // -----------------------------------------------------------------------------
    const WRAPROWS = {
        description: _t("Wraps the provided row or column of cells by rows after a specified number of elements to form a new array."),
        args: [
            arg("range (any, range<any>)", _t("The range to wrap.")),
            arg("wrap_count (number)", _t("The maximum number of cells for each row, rounded down to the nearest whole number.")),
            arg("pad_with  (any, default=0)", // TODO : replace with #N/A
            _t("The value with which to fill the extra cells in the range.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (range, wrapCount, padWith = { value: 0 }) {
            const _array = toMatrix(range);
            const nbColumns = toInteger(wrapCount?.value, this.locale);
            assertSingleColOrRow(_t("Argument range must be a single row or column."), _array);
            const array = _array.flat();
            const nbRows = Math.ceil(array.length / nbColumns);
            return generateMatrix(nbColumns, nbRows, (col, row) => {
                const index = row * nbColumns + col;
                return index < array.length ? array[index] : padWith;
            });
        },
        isExported: true,
    };

    var array = /*#__PURE__*/Object.freeze({
        __proto__: null,
        ARRAY_CONSTRAIN: ARRAY_CONSTRAIN,
        CHOOSECOLS: CHOOSECOLS,
        CHOOSEROWS: CHOOSEROWS,
        EXPAND: EXPAND,
        FLATTEN: FLATTEN,
        FREQUENCY: FREQUENCY,
        HSTACK: HSTACK,
        MDETERM: MDETERM,
        MINVERSE: MINVERSE,
        MMULT: MMULT,
        SUMPRODUCT: SUMPRODUCT,
        SUMX2MY2: SUMX2MY2,
        SUMX2PY2: SUMX2PY2,
        SUMXMY2: SUMXMY2,
        TOCOL: TOCOL,
        TOROW: TOROW,
        TRANSPOSE: TRANSPOSE,
        VSTACK: VSTACK,
        WRAPCOLS: WRAPCOLS,
        WRAPROWS: WRAPROWS
    });

    // -----------------------------------------------------------------------------
    // FORMAT.LARGE.NUMBER
    // -----------------------------------------------------------------------------
    const FORMAT_LARGE_NUMBER = {
        description: _t("Apply a large number format"),
        args: [
            arg("value (number)", _t("The number.")),
            arg("unit (string, optional)", _t("The formatting unit. Use 'k', 'm', or 'b' to force the unit")),
        ],
        returns: ["NUMBER"],
        computeFormat: function (arg, unit) {
            const value = Math.abs(toNumber(arg?.value, this.locale));
            const format = arg?.format;
            if (unit !== undefined) {
                const postFix = unit?.value;
                switch (postFix) {
                    case "k":
                        return createLargeNumberFormat(format, 1e3, "k", this.locale);
                    case "m":
                        return createLargeNumberFormat(format, 1e6, "m", this.locale);
                    case "b":
                        return createLargeNumberFormat(format, 1e9, "b", this.locale);
                    default:
                        throw new Error(_t("The formatting unit should be 'k', 'm' or 'b'."));
                }
            }
            if (value < 1e5) {
                return createLargeNumberFormat(format, 0, "", this.locale);
            }
            else if (value < 1e8) {
                return createLargeNumberFormat(format, 1e3, "k", this.locale);
            }
            else if (value < 1e11) {
                return createLargeNumberFormat(format, 1e6, "m", this.locale);
            }
            return createLargeNumberFormat(format, 1e9, "b", this.locale);
        },
        compute: function (value) {
            return toNumber(value, this.locale);
        },
    };

    var misc = /*#__PURE__*/Object.freeze({
        __proto__: null,
        FORMAT_LARGE_NUMBER: FORMAT_LARGE_NUMBER
    });

    const DEFAULT_FACTOR = 1;
    const DEFAULT_MODE = 0;
    const DEFAULT_PLACES = 0;
    const DEFAULT_SIGNIFICANCE = 1;
    const DECIMAL_REPRESENTATION = /^-?[a-z0-9]+$/i;
    // -----------------------------------------------------------------------------
    // ABS
    // -----------------------------------------------------------------------------
    const ABS = {
        description: _t("Absolute value of a number."),
        args: [arg("value (number)", _t("The number of which to return the absolute value."))],
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.abs(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOS
    // -----------------------------------------------------------------------------
    const ACOS = {
        description: _t("Inverse cosine of a value, in radians."),
        args: [
            arg("value (number)", _t("The value for which to calculate the inverse cosine. Must be between -1 and 1, inclusive.")),
        ],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => Math.abs(_value) <= 1, _t("The value (%s) must be between -1 and 1 inclusive.", _value.toString()));
            return Math.acos(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOSH
    // -----------------------------------------------------------------------------
    const ACOSH = {
        description: _t("Inverse hyperbolic cosine of a number."),
        args: [
            arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cosine. Must be greater than or equal to 1.")),
        ],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => _value >= 1, _t("The value (%s) must be greater than or equal to 1.", _value.toString()));
            return Math.acosh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOT
    // -----------------------------------------------------------------------------
    const ACOT = {
        description: _t("Inverse cotangent of a value."),
        args: [arg("value (number)", _t("The value for which to calculate the inverse cotangent."))],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            const sign = Math.sign(_value) || 1;
            // ACOT has two possible configurations:
            // @compatibility Excel: return Math.PI / 2 - Math.atan(toNumber(_value, this.locale));
            // @compatibility Google: return sign * Math.PI / 2 - Math.atan(toNumber(_value, this.locale));
            return (sign * Math.PI) / 2 - Math.atan(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ACOTH
    // -----------------------------------------------------------------------------
    const ACOTH = {
        description: _t("Inverse hyperbolic cotangent of a value."),
        args: [
            arg("value (number)", _t("The value for which to calculate the inverse hyperbolic cotangent. Must not be between -1 and 1, inclusive.")),
        ],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => Math.abs(_value) > 1, _t("The value (%s) cannot be between -1 and 1 inclusive.", _value.toString()));
            return Math.log((_value + 1) / (_value - 1)) / 2;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ASIN
    // -----------------------------------------------------------------------------
    const ASIN = {
        description: _t("Inverse sine of a value, in radians."),
        args: [
            arg("value (number)", _t("The value for which to calculate the inverse sine. Must be between -1 and 1, inclusive.")),
        ],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => Math.abs(_value) <= 1, _t("The value (%s) must be between -1 and 1 inclusive.", _value.toString()));
            return Math.asin(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ASINH
    // -----------------------------------------------------------------------------
    const ASINH = {
        description: _t("Inverse hyperbolic sine of a number."),
        args: [
            arg("value (number)", _t("The value for which to calculate the inverse hyperbolic sine.")),
        ],
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.asinh(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ATAN
    // -----------------------------------------------------------------------------
    const ATAN = {
        description: _t("Inverse tangent of a value, in radians."),
        args: [arg("value (number)", _t("The value for which to calculate the inverse tangent."))],
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.atan(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ATAN2
    // -----------------------------------------------------------------------------
    const ATAN2 = {
        description: _t("Angle from the X axis to a point (x,y), in radians."),
        args: [
            arg("x (number)", _t("The x coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")),
            arg("y (number)", _t("The y coordinate of the endpoint of the line segment for which to calculate the angle from the x-axis.")),
        ],
        returns: ["NUMBER"],
        compute: function (x, y) {
            const _x = toNumber(x, this.locale);
            const _y = toNumber(y, this.locale);
            assert(() => _x !== 0 || _y !== 0, _t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
            return Math.atan2(_y, _x);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ATANH
    // -----------------------------------------------------------------------------
    const ATANH = {
        description: _t("Inverse hyperbolic tangent of a number."),
        args: [
            arg("value (number)", _t("The value for which to calculate the inverse hyperbolic tangent. Must be between -1 and 1, exclusive.")),
        ],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => Math.abs(_value) < 1, _t("The value (%s) must be between -1 and 1 exclusive.", _value.toString()));
            return Math.atanh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CEILING
    // -----------------------------------------------------------------------------
    const CEILING = {
        description: _t("Rounds number up to nearest multiple of factor."),
        args: [
            arg("value (number)", _t("The value to round up to the nearest integer multiple of factor.")),
            arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value) => value?.format,
        compute: function (value, factor = DEFAULT_FACTOR) {
            const _value = toNumber(value, this.locale);
            const _factor = toNumber(factor, this.locale);
            assert(() => _factor >= 0 || _value <= 0, _t("The factor (%s) must be positive when the value (%s) is positive.", _factor.toString(), _value.toString()));
            return _factor ? Math.ceil(_value / _factor) * _factor : 0;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CEILING.MATH
    // -----------------------------------------------------------------------------
    const CEILING_MATH = {
        description: _t("Rounds number up to nearest multiple of factor."),
        args: [
            arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")),
            arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")),
            arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded towards zero. Otherwise, it is rounded away from zero.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (number) => number?.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) {
            let _significance = toNumber(significance, this.locale);
            if (_significance === 0) {
                return 0;
            }
            const _number = toNumber(number, this.locale);
            _significance = Math.abs(_significance);
            if (_number >= 0) {
                return Math.ceil(_number / _significance) * _significance;
            }
            const _mode = toNumber(mode, this.locale);
            if (_mode === 0) {
                return -Math.floor(Math.abs(_number) / _significance) * _significance;
            }
            return -Math.ceil(Math.abs(_number) / _significance) * _significance;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CEILING.PRECISE
    // -----------------------------------------------------------------------------
    const CEILING_PRECISE = {
        description: _t("Rounds number up to nearest multiple of factor."),
        args: [
            arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")),
            arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (number) => number?.format,
        compute: function (number, significance) {
            return CEILING_MATH.compute.bind(this)(number, significance, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COS
    // -----------------------------------------------------------------------------
    const COS = {
        description: _t("Cosine of an angle provided in radians."),
        args: [arg("angle (number)", _t("The angle to find the cosine of, in radians."))],
        returns: ["NUMBER"],
        compute: function (angle) {
            return Math.cos(toNumber(angle, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COSH
    // -----------------------------------------------------------------------------
    const COSH = {
        description: _t("Hyperbolic cosine of any real number."),
        args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosine of."))],
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.cosh(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COT
    // -----------------------------------------------------------------------------
    const COT = {
        description: _t("Cotangent of an angle provided in radians."),
        args: [arg("angle (number)", _t("The angle to find the cotangent of, in radians."))],
        returns: ["NUMBER"],
        compute: function (angle) {
            const _angle = toNumber(angle, this.locale);
            assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return 1 / Math.tan(_angle);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COTH
    // -----------------------------------------------------------------------------
    const COTH = {
        description: _t("Hyperbolic cotangent of any real number."),
        args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cotangent of."))],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return 1 / Math.tanh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTBLANK
    // -----------------------------------------------------------------------------
    const COUNTBLANK = {
        description: _t("Number of empty values."),
        args: [
            arg("value1 (any, range)", _t("The first value or range in which to count the number of blanks.")),
            arg("value2 (any, range, repeating)", _t("Additional values or ranges in which to count the number of blanks.")),
        ],
        returns: ["NUMBER"],
        compute: function (...argsValues) {
            return reduceAny(argsValues, (acc, a) => (a === null || a === undefined || a === "" ? acc + 1 : acc), 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTIF
    // -----------------------------------------------------------------------------
    const COUNTIF = {
        description: _t("A conditional count across a range."),
        args: [
            arg("range (range)", _t("The range that is tested against criterion.")),
            arg("criterion (string)", _t("The pattern or test to apply to range.")),
        ],
        returns: ["NUMBER"],
        compute: function (...argsValues) {
            let count = 0;
            visitMatchingRanges(argsValues, (i, j) => {
                count += 1;
            }, this.locale);
            return count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTIFS
    // -----------------------------------------------------------------------------
    const COUNTIFS = {
        description: _t("Count values depending on multiple criteria."),
        args: [
            arg("criteria_range1 (range)", _t("The range to check against criterion1.")),
            arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")),
            arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
            arg("criterion2 (string, repeating)", _t("Additional criteria to check.")),
        ],
        returns: ["NUMBER"],
        compute: function (...argsValues) {
            let count = 0;
            visitMatchingRanges(argsValues, (i, j) => {
                count += 1;
            }, this.locale);
            return count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTUNIQUE
    // -----------------------------------------------------------------------------
    function isDefined(value) {
        switch (value) {
            case undefined:
                return false;
            case "":
                return false;
            case null:
                return false;
            default:
                return true;
        }
    }
    const COUNTUNIQUE = {
        description: _t("Counts number of unique values in a range."),
        args: [
            arg("value1 (any, range)", _t("The first value or range to consider for uniqueness.")),
            arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider for uniqueness.")),
        ],
        returns: ["NUMBER"],
        compute: function (...argsValues) {
            return reduceAny(argsValues, (acc, a) => (isDefined(a) ? acc.add(a) : acc), new Set()).size;
        },
    };
    // -----------------------------------------------------------------------------
    // COUNTUNIQUEIFS
    // -----------------------------------------------------------------------------
    const COUNTUNIQUEIFS = {
        description: _t("Counts number of unique values in a range, filtered by a set of criteria."),
        args: [
            arg("range (range)", _t("The range of cells from which the number of unique values will be counted.")),
            arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")),
            arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")),
            arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
            arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
        ],
        returns: ["NUMBER"],
        compute: function (range, ...argsValues) {
            let uniqueValues = new Set();
            visitMatchingRanges(argsValues, (i, j) => {
                const value = range[i][j];
                if (isDefined(value)) {
                    uniqueValues.add(value);
                }
            }, this.locale);
            return uniqueValues.size;
        },
    };
    // -----------------------------------------------------------------------------
    // CSC
    // -----------------------------------------------------------------------------
    const CSC = {
        description: _t("Cosecant of an angle provided in radians."),
        args: [arg("angle (number)", _t("The angle to find the cosecant of, in radians."))],
        returns: ["NUMBER"],
        compute: function (angle) {
            const _angle = toNumber(angle, this.locale);
            assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return 1 / Math.sin(_angle);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CSCH
    // -----------------------------------------------------------------------------
    const CSCH = {
        description: _t("Hyperbolic cosecant of any real number."),
        args: [arg("value (number)", _t("Any real value to calculate the hyperbolic cosecant of."))],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return 1 / Math.sinh(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DECIMAL
    // -----------------------------------------------------------------------------
    const DECIMAL = {
        description: _t("Converts from another base to decimal."),
        args: [
            arg("value (string)", _t("The number to convert.")),
            arg(",base (number)", _t("The base to convert the value from.")),
        ],
        returns: ["NUMBER"],
        compute: function (value, base) {
            let _base = toNumber(base, this.locale);
            _base = Math.floor(_base);
            assert(() => 2 <= _base && _base <= 36, _t("The base (%s) must be between 2 and 36 inclusive.", _base.toString()));
            const _value = toString(value);
            if (_value === "") {
                return 0;
            }
            /**
             * @compatibility: on Google sheets, expects the parameter 'value' to be positive.
             * Return error if 'value' is positive.
             * Remove '-?' in the next regex to catch this error.
             */
            assert(() => !!DECIMAL_REPRESENTATION.test(_value), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
            const deci = parseInt(_value, _base);
            assert(() => !isNaN(deci), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
            return deci;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DEGREES
    // -----------------------------------------------------------------------------
    const DEGREES = {
        description: _t("Converts an angle value in radians to degrees."),
        args: [arg("angle (number)", _t("The angle to convert from radians to degrees."))],
        returns: ["NUMBER"],
        compute: function (angle) {
            return (toNumber(angle, this.locale) * 180) / Math.PI;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EXP
    // -----------------------------------------------------------------------------
    const EXP = {
        description: _t("Euler's number, e (~2.718) raised to a power."),
        args: [arg("value (number)", _t("The exponent to raise e."))],
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.exp(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FLOOR
    // -----------------------------------------------------------------------------
    const FLOOR = {
        description: _t("Rounds number down to nearest multiple of factor."),
        args: [
            arg("value (number)", _t("The value to round down to the nearest integer multiple of factor.")),
            arg(`factor (number, default=${DEFAULT_FACTOR})`, _t("The number to whose multiples value will be rounded.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value) => value?.format,
        compute: function (value, factor = DEFAULT_FACTOR) {
            const _value = toNumber(value, this.locale);
            const _factor = toNumber(factor, this.locale);
            assert(() => _factor >= 0 || _value <= 0, _t("The factor (%s) must be positive when the value (%s) is positive.", _factor.toString(), _value.toString()));
            return _factor ? Math.floor(_value / _factor) * _factor : 0;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FLOOR.MATH
    // -----------------------------------------------------------------------------
    const FLOOR_MATH = {
        description: _t("Rounds number down to nearest multiple of factor."),
        args: [
            arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")),
            arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded. The sign of significance will be ignored.")),
            arg(`mode (number, default=${DEFAULT_MODE})`, _t("If number is negative, specifies the rounding direction. If 0 or blank, it is rounded away from zero. Otherwise, it is rounded towards zero.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (number) => number?.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE, mode = DEFAULT_MODE) {
            let _significance = toNumber(significance, this.locale);
            if (_significance === 0) {
                return 0;
            }
            const _number = toNumber(number, this.locale);
            _significance = Math.abs(_significance);
            if (_number >= 0) {
                return Math.floor(_number / _significance) * _significance;
            }
            const _mode = toNumber(mode, this.locale);
            if (_mode === 0) {
                return -Math.ceil(Math.abs(_number) / _significance) * _significance;
            }
            return -Math.floor(Math.abs(_number) / _significance) * _significance;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FLOOR.PRECISE
    // -----------------------------------------------------------------------------
    const FLOOR_PRECISE = {
        description: _t("Rounds number down to nearest multiple of factor."),
        args: [
            arg("number (number)", _t("The value to round down to the nearest integer multiple of significance.")),
            arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (number) => number?.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE) {
            return FLOOR_MATH.compute.bind(this)(number, significance, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISEVEN
    // -----------------------------------------------------------------------------
    const ISEVEN = {
        description: _t("Whether the provided value is even."),
        args: [arg("value (number)", _t("The value to be verified as even."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            const _value = strictToNumber(value, this.locale);
            return Math.floor(Math.abs(_value)) & 1 ? false : true;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISO.CEILING
    // -----------------------------------------------------------------------------
    const ISO_CEILING = {
        description: _t("Rounds number up to nearest multiple of factor."),
        args: [
            arg("number (number)", _t("The value to round up to the nearest integer multiple of significance.")),
            arg(`significance (number, default=${DEFAULT_SIGNIFICANCE})`, _t("The number to whose multiples number will be rounded.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (number) => number?.format,
        compute: function (number, significance = DEFAULT_SIGNIFICANCE) {
            return CEILING_MATH.compute.bind(this)(number, significance, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISODD
    // -----------------------------------------------------------------------------
    const ISODD = {
        description: _t("Whether the provided value is even."),
        args: [arg("value (number)", _t("The value to be verified as even."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            const _value = strictToNumber(value, this.locale);
            return Math.floor(Math.abs(_value)) & 1 ? true : false;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LN
    // -----------------------------------------------------------------------------
    const LN = {
        description: _t("The logarithm of a number, base e (euler's number)."),
        args: [arg("value (number)", _t("The value for which to calculate the logarithm, base e."))],
        returns: ["NUMBER"],
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => _value > 0, _t("The value (%s) must be strictly positive.", _value.toString()));
            return Math.log(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MOD
    // -----------------------------------------------------------------------------
    const MOD = {
        description: _t("Modulo (remainder) operator."),
        args: [
            arg("dividend (number)", _t("The number to be divided to find the remainder.")),
            arg("divisor (number)", _t("The number to divide by.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (dividend) => dividend?.format,
        compute: function (dividend, divisor) {
            const _divisor = toNumber(divisor, this.locale);
            assert(() => _divisor !== 0, _t("The divisor must be different from 0."));
            const _dividend = toNumber(dividend, this.locale);
            const modulus = _dividend % _divisor;
            // -42 % 10 = -2 but we want 8, so need the code below
            if ((modulus > 0 && _divisor < 0) || (modulus < 0 && _divisor > 0)) {
                return modulus + _divisor;
            }
            return modulus;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MUNIT
    // -----------------------------------------------------------------------------
    const MUNIT = {
        description: _t("Returns a n x n unit matrix, where n is the input dimension."),
        args: [
            arg("dimension (number)", _t("An integer specifying the dimension size of the unit matrix. It must be positive.")),
        ],
        returns: ["RANGE<NUMBER>"],
        compute: function (n) {
            const _n = toInteger(n, this.locale);
            assertPositive(_t("The argument dimension must be positive"), _n);
            return getUnitMatrix(_n);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ODD
    // -----------------------------------------------------------------------------
    const ODD = {
        description: _t("Rounds a number up to the nearest odd integer."),
        args: [arg("value (number)", _t("The value to round to the next greatest odd number."))],
        returns: ["NUMBER"],
        computeFormat: (number) => number?.format,
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            let temp = Math.ceil(Math.abs(_value));
            temp = temp & 1 ? temp : temp + 1;
            return _value < 0 ? -temp : temp;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PI
    // -----------------------------------------------------------------------------
    const PI = {
        description: _t("The number pi."),
        args: [],
        returns: ["NUMBER"],
        compute: function () {
            return Math.PI;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // POWER
    // -----------------------------------------------------------------------------
    const POWER = {
        description: _t("A number raised to a power."),
        args: [
            arg("base (number)", _t("The number to raise to the exponent power.")),
            arg("exponent (number)", _t("The exponent to raise base to.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (base) => base?.format,
        compute: function (base, exponent) {
            const _base = toNumber(base, this.locale);
            const _exponent = toNumber(exponent, this.locale);
            assert(() => _base >= 0 || Number.isInteger(_exponent), _t("The exponent (%s) must be an integer when the base is negative.", _exponent.toString()));
            return Math.pow(_base, _exponent);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PRODUCT
    // -----------------------------------------------------------------------------
    const PRODUCT = {
        description: _t("Result of multiplying a series of numbers together."),
        args: [
            arg("factor1 (number, range<number>)", _t("The first number or range to calculate for the product.")),
            arg("factor2 (number, range<number>, repeating)", _t("More numbers or ranges to calculate for the product.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (factor1) => {
            return isMatrix(factor1) ? factor1[0][0]?.format : factor1?.format;
        },
        compute: function (...factors) {
            let count = 0;
            let acc = 1;
            for (let n of factors) {
                if (isMatrix(n)) {
                    for (let i of n) {
                        for (let j of i) {
                            if (typeof j === "number") {
                                acc *= j;
                                count += 1;
                            }
                        }
                    }
                }
                else if (n !== null && n !== undefined) {
                    acc *= strictToNumber(n, this.locale);
                    count += 1;
                }
            }
            if (count === 0) {
                return 0;
            }
            return acc;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RAND
    // -----------------------------------------------------------------------------
    const RAND = {
        description: _t("A random number between 0 inclusive and 1 exclusive."),
        args: [],
        returns: ["NUMBER"],
        compute: function () {
            return Math.random();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RANDARRAY
    // -----------------------------------------------------------------------------
    const RANDARRAY = {
        description: _t("Returns a grid of random numbers between 0 inclusive and 1 exclusive."),
        args: [
            arg("rows (number, default=1)", _t("The number of rows to be returned.")),
            arg("columns (number, default=1)", _t("The number of columns to be returned.")),
            arg("min (number, default=0)", _t("The minimum number you would like returned.")),
            arg("max (number, default=1)", _t("The maximum number you would like returned.")),
            arg("whole_number (number, default=FALSE)", _t("Return a whole number or a decimal value.")),
        ],
        returns: ["RANGE<NUMBER>"],
        compute: function (rows = 1, columns = 1, min = 0, max = 1, whole_number = false) {
            const _cols = toInteger(columns, this.locale);
            const _rows = toInteger(rows, this.locale);
            const _min = toNumber(min, this.locale);
            const _max = toNumber(max, this.locale);
            const _whole_number = toBoolean(whole_number);
            assertPositive(_t("The number columns (%s) must be positive.", _cols.toString()), _cols);
            assertPositive(_t("The number rows (%s) must be positive.", _rows.toString()), _rows);
            assert(() => _min <= _max, _t("The maximum (%s) must be greater than or equal to the minimum (%s).", _max.toString(), _min.toString()));
            if (_whole_number) {
                assert(() => Number.isInteger(_min) && Number.isInteger(_max), _t("The maximum (%s) and minimum (%s) must be integers when whole_number is TRUE.", _max.toString(), _min.toString()));
            }
            const result = Array(_cols);
            for (let col = 0; col < _cols; col++) {
                result[col] = Array(_rows);
                for (let row = 0; row < _rows; row++) {
                    if (!_whole_number) {
                        result[col][row] = _min + Math.random() * (_max - _min);
                    }
                    else {
                        result[col][row] = Math.floor(Math.random() * (_max - _min + 1) + _min);
                    }
                }
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RANDBETWEEN
    // -----------------------------------------------------------------------------
    const RANDBETWEEN = {
        description: _t("Random integer between two values, inclusive."),
        args: [
            arg("low (number)", _t("The low end of the random range.")),
            arg("high (number)", _t("The high end of the random range.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (low) => low?.format,
        compute: function (low, high) {
            let _low = toNumber(low, this.locale);
            if (!Number.isInteger(_low)) {
                _low = Math.ceil(_low);
            }
            let _high = toNumber(high, this.locale);
            if (!Number.isInteger(_high)) {
                _high = Math.floor(_high);
            }
            assert(() => _low <= _high, _t("The high (%s) must be greater than or equal to the low (%s).", _high.toString(), _low.toString()));
            return _low + Math.ceil((_high - _low + 1) * Math.random()) - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ROUND
    // -----------------------------------------------------------------------------
    const ROUND = {
        description: _t("Rounds a number according to standard rules."),
        args: [
            arg("value (number)", _t("The value to round to places number of places.")),
            arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value) => value?.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value, this.locale);
            let _places = toNumber(places, this.locale);
            const absValue = Math.abs(_value);
            let tempResult;
            if (_places === 0) {
                tempResult = Math.round(absValue);
            }
            else {
                if (!Number.isInteger(_places)) {
                    _places = Math.trunc(_places);
                }
                tempResult = Math.round(absValue * Math.pow(10, _places)) / Math.pow(10, _places);
            }
            return _value >= 0 ? tempResult : -tempResult;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ROUNDDOWN
    // -----------------------------------------------------------------------------
    const ROUNDDOWN = {
        description: _t("Rounds down a number."),
        args: [
            arg("value (number)", _t("The value to round to places number of places, always rounding down.")),
            arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value) => value?.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value, this.locale);
            let _places = toNumber(places, this.locale);
            const absValue = Math.abs(_value);
            let tempResult;
            if (_places === 0) {
                tempResult = Math.floor(absValue);
            }
            else {
                if (!Number.isInteger(_places)) {
                    _places = Math.trunc(_places);
                }
                tempResult = Math.floor(absValue * Math.pow(10, _places)) / Math.pow(10, _places);
            }
            return _value >= 0 ? tempResult : -tempResult;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ROUNDUP
    // -----------------------------------------------------------------------------
    const ROUNDUP = {
        description: _t("Rounds up a number."),
        args: [
            arg("value (number)", _t("The value to round to places number of places, always rounding up.")),
            arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of decimal places to which to round.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value) => value?.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value, this.locale);
            let _places = toNumber(places, this.locale);
            const absValue = Math.abs(_value);
            let tempResult;
            if (_places === 0) {
                tempResult = Math.ceil(absValue);
            }
            else {
                if (!Number.isInteger(_places)) {
                    _places = Math.trunc(_places);
                }
                tempResult = Math.ceil(absValue * Math.pow(10, _places)) / Math.pow(10, _places);
            }
            return _value >= 0 ? tempResult : -tempResult;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SEC
    // -----------------------------------------------------------------------------
    const SEC = {
        description: _t("Secant of an angle provided in radians."),
        args: [arg("angle (number)", _t("The angle to find the secant of, in radians."))],
        returns: ["NUMBER"],
        compute: function (angle) {
            return 1 / Math.cos(toNumber(angle, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SECH
    // -----------------------------------------------------------------------------
    const SECH = {
        description: _t("Hyperbolic secant of any real number."),
        args: [arg("value (number)", _t("Any real value to calculate the hyperbolic secant of."))],
        returns: ["NUMBER"],
        compute: function (value) {
            return 1 / Math.cosh(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SIN
    // -----------------------------------------------------------------------------
    const SIN = {
        description: _t("Sine of an angle provided in radians."),
        args: [arg("angle (number)", _t("The angle to find the sine of, in radians."))],
        returns: ["NUMBER"],
        compute: function (angle) {
            return Math.sin(toNumber(angle, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SINH
    // -----------------------------------------------------------------------------
    const SINH = {
        description: _t("Hyperbolic sine of any real number."),
        args: [arg("value (number)", _t("Any real value to calculate the hyperbolic sine of."))],
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.sinh(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SQRT
    // -----------------------------------------------------------------------------
    const SQRT = {
        description: _t("Positive square root of a positive number."),
        args: [arg("value (number)", _t("The number for which to calculate the positive square root."))],
        returns: ["NUMBER"],
        computeFormat: (value) => value?.format,
        compute: function (value) {
            const _value = toNumber(value, this.locale);
            assert(() => _value >= 0, _t("The value (%s) must be positive or null.", _value.toString()));
            return Math.sqrt(_value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUM
    // -----------------------------------------------------------------------------
    const SUM = {
        description: _t("Sum of a series of numbers and/or cells."),
        args: [
            arg("value1 (number, range<number>)", _t("The first number or range to add together.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional numbers or ranges to add to value1.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            return reduceNumbers(values, (acc, a) => acc + a, 0, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMIF
    // -----------------------------------------------------------------------------
    const SUMIF = {
        description: _t("A conditional sum across a range."),
        args: [
            arg("criteria_range (range)", _t("The range which is tested against criterion.")),
            arg("criterion (string)", _t("The pattern or test to apply to range.")),
            arg("sum_range (range, default=criteria_range)", _t("The range to be summed, if different from range.")),
        ],
        returns: ["NUMBER"],
        compute: function (criteriaRange, criterion, sumRange) {
            if (sumRange === undefined) {
                sumRange = criteriaRange;
            }
            let sum = 0;
            visitMatchingRanges([criteriaRange, criterion], (i, j) => {
                const value = sumRange[i][j];
                if (typeof value === "number") {
                    sum += value;
                }
            }, this.locale);
            return sum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUMIFS
    // -----------------------------------------------------------------------------
    const SUMIFS = {
        description: _t("Sums a range depending on multiple criteria."),
        args: [
            arg("sum_range (range)", _t("The range to sum.")),
            arg("criteria_range1 (range)", _t("The range to check against criterion1.")),
            arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")),
            arg("criteria_range2 (any, range, repeating)", _t("Additional ranges to check.")),
            arg("criterion2 (string, repeating)", _t("Additional criteria to check.")),
        ],
        returns: ["NUMBER"],
        compute: function (sumRange, ...criters) {
            let sum = 0;
            visitMatchingRanges(criters, (i, j) => {
                const value = sumRange[i][j];
                if (typeof value === "number") {
                    sum += value;
                }
            }, this.locale);
            return sum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TAN
    // -----------------------------------------------------------------------------
    const TAN = {
        description: _t("Tangent of an angle provided in radians."),
        args: [arg("angle (number)", _t("The angle to find the tangent of, in radians."))],
        returns: ["NUMBER"],
        compute: function (angle) {
            return Math.tan(toNumber(angle, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TANH
    // -----------------------------------------------------------------------------
    const TANH = {
        description: _t("Hyperbolic tangent of any real number."),
        args: [arg("value (number)", _t("Any real value to calculate the hyperbolic tangent of."))],
        returns: ["NUMBER"],
        compute: function (value) {
            return Math.tanh(toNumber(value, this.locale));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TRUNC
    // -----------------------------------------------------------------------------
    const TRUNC = {
        description: _t("Truncates a number."),
        args: [
            arg("value (number)", _t("The value to be truncated.")),
            arg(`places (number, default=${DEFAULT_PLACES})`, _t("The number of significant digits to the right of the decimal point to retain.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value) => value?.format,
        compute: function (value, places = DEFAULT_PLACES) {
            const _value = toNumber(value, this.locale);
            let _places = toNumber(places, this.locale);
            if (_places === 0) {
                return Math.trunc(_value);
            }
            if (!Number.isInteger(_places)) {
                _places = Math.trunc(_places);
            }
            return Math.trunc(_value * Math.pow(10, _places)) / Math.pow(10, _places);
        },
        isExported: true,
    };

    var math = /*#__PURE__*/Object.freeze({
        __proto__: null,
        ABS: ABS,
        ACOS: ACOS,
        ACOSH: ACOSH,
        ACOT: ACOT,
        ACOTH: ACOTH,
        ASIN: ASIN,
        ASINH: ASINH,
        ATAN: ATAN,
        ATAN2: ATAN2,
        ATANH: ATANH,
        CEILING: CEILING,
        CEILING_MATH: CEILING_MATH,
        CEILING_PRECISE: CEILING_PRECISE,
        COS: COS,
        COSH: COSH,
        COT: COT,
        COTH: COTH,
        COUNTBLANK: COUNTBLANK,
        COUNTIF: COUNTIF,
        COUNTIFS: COUNTIFS,
        COUNTUNIQUE: COUNTUNIQUE,
        COUNTUNIQUEIFS: COUNTUNIQUEIFS,
        CSC: CSC,
        CSCH: CSCH,
        DECIMAL: DECIMAL,
        DEGREES: DEGREES,
        EXP: EXP,
        FLOOR: FLOOR,
        FLOOR_MATH: FLOOR_MATH,
        FLOOR_PRECISE: FLOOR_PRECISE,
        ISEVEN: ISEVEN,
        ISODD: ISODD,
        ISO_CEILING: ISO_CEILING,
        LN: LN,
        MOD: MOD,
        MUNIT: MUNIT,
        ODD: ODD,
        PI: PI,
        POWER: POWER,
        PRODUCT: PRODUCT,
        RAND: RAND,
        RANDARRAY: RANDARRAY,
        RANDBETWEEN: RANDBETWEEN,
        ROUND: ROUND,
        ROUNDDOWN: ROUNDDOWN,
        ROUNDUP: ROUNDUP,
        SEC: SEC,
        SECH: SECH,
        SIN: SIN,
        SINH: SINH,
        SQRT: SQRT,
        SUM: SUM,
        SUMIF: SUMIF,
        SUMIFS: SUMIFS,
        TAN: TAN,
        TANH: TANH,
        TRUNC: TRUNC
    });

    function assertSameNumberOfElements(...args) {
        const dims = args[0].length;
        args.forEach((arg, i) => assert(() => arg.length === dims, _t("[[FUNCTION_NAME]] has mismatched dimensions for argument %s (%s vs %s).", i.toString(), dims.toString(), arg.length.toString())));
    }

    function filterAndFlatData(dataY, dataX) {
        const _flatDataY = [];
        const _flatDataX = [];
        let lenY = 0;
        let lenX = 0;
        visitAny([dataY], (y) => {
            _flatDataY.push(y);
            lenY += 1;
        });
        visitAny([dataX], (x) => {
            _flatDataX.push(x);
            lenX += 1;
        });
        assert(() => lenY === lenX, _t("[[FUNCTION_NAME]] has mismatched argument count %s vs %s.", lenY, lenX));
        const flatDataX = [];
        const flatDataY = [];
        for (let i = 0; i < lenY; i++) {
            const valueY = _flatDataY[i];
            const valueX = _flatDataX[i];
            if (typeof valueY === "number" && typeof valueX === "number") {
                flatDataY.push(valueY);
                flatDataX.push(valueX);
            }
        }
        return { flatDataX, flatDataY };
    }
    // Note: dataY and dataX may not have the same dimension
    function covariance(dataY, dataX, isSample) {
        const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
        const count = flatDataY.length;
        assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
        let sumY = 0;
        let sumX = 0;
        for (let i = 0; i < count; i++) {
            sumY += flatDataY[i];
            sumX += flatDataX[i];
        }
        const averageY = sumY / count;
        const averageX = sumX / count;
        let acc = 0;
        for (let i = 0; i < count; i++) {
            acc += (flatDataY[i] - averageY) * (flatDataX[i] - averageX);
        }
        return acc / (count - (isSample ? 1 : 0));
    }
    function variance(args, isSample, textAs0, locale) {
        let count = 0;
        let sum = 0;
        const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers;
        sum = reduceFunction(args, (acc, a) => {
            count += 1;
            return acc + a;
        }, 0, locale);
        assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
        const average = sum / count;
        return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) /
            (count - (isSample ? 1 : 0)));
    }
    function centile(data, percent, isInclusive, locale) {
        const _percent = toNumber(percent, locale);
        assert(() => (isInclusive ? 0 <= _percent && _percent <= 1 : 0 < _percent && _percent < 1), _t("Function [[FUNCTION_NAME]] parameter 2 value is out of range."));
        let sortedArray = [];
        let index;
        let count = 0;
        visitAny(data, (d) => {
            if (typeof d === "number") {
                index = dichotomicSearch(sortedArray, d, "nextSmaller", "asc", sortedArray.length, (array, i) => array[i]);
                sortedArray.splice(index + 1, 0, d);
                count++;
            }
        });
        assert(() => count !== 0, _t("[[FUNCTION_NAME]] has no valid input data."));
        if (!isInclusive) {
            // 2nd argument must be between 1/(n+1) and n/(n+1) with n the number of data
            assert(() => 1 / (count + 1) <= _percent && _percent <= count / (count + 1), _t("Function [[FUNCTION_NAME]] parameter 2 value is out of range."));
        }
        return percentile(sortedArray, _percent, isInclusive);
    }
    function prepareDataForRegression(X, Y, newX) {
        const _X = X.length ? X : [range(1, Y.flat().length + 1)];
        const nVar = _X.length;
        let _newX = newX.length ? newX : _X;
        _newX = _newX.length === nVar ? transposeMatrix(_newX) : _newX;
        return { _X, _newX };
    }
    /*
     * This function performs a linear regression on the data set. It returns an array with two elements.
     * The first element is the slope, and the second element is the intercept.
     * The linear regression line is: y = slope*x + intercept
     * The function use the least squares method to find the best fit for the data set :
     * see https://www.mathsisfun.com/data/least-squares-regression.html
     *     https://www.statology.org/standard-error-of-estimate/
     *     https://agronomy4future.org/?p=16670
     *     https://vitalflux.com/interpreting-f-statistics-in-linear-regression-formula-examples/
     *     https://web.ist.utl.pt/~ist11038/compute/errtheory/,regression/regrthroughorigin.pdf
     */
    function fullLinearRegression(X, Y, computeIntercept = true, verbose = false) {
        const y = Y.flat();
        const n = y.length;
        let { _X } = prepareDataForRegression(X, Y, []);
        _X = _X.length === n ? transposeMatrix(_X) : _X.slice();
        assertSameNumberOfElements(_X[0], y);
        const nVar = _X.length;
        const nDeg = n - nVar - (computeIntercept ? 1 : 0);
        const yMatrix = [y];
        const xMatrix = transposeMatrix(_X.reverse());
        let avgX = [];
        for (let i = 0; i < nVar; i++) {
            avgX.push(0);
            if (computeIntercept) {
                for (const xij of _X[i]) {
                    avgX[i] += xij;
                }
                avgX[i] /= n;
            }
        }
        let avgY = 0;
        if (computeIntercept) {
            for (const yi of y) {
                avgY += yi;
            }
            avgY /= n;
        }
        const redX = xMatrix.map((row) => row.map((value, i) => value - avgX[i]));
        if (computeIntercept) {
            xMatrix.forEach((row) => row.push(1));
        }
        const coeffs = getLMSCoefficients(xMatrix, yMatrix);
        if (!computeIntercept) {
            coeffs.push([0]);
        }
        if (!verbose) {
            return coeffs;
        }
        const dot1 = multiplyMatrices(redX, transposeMatrix(redX));
        const { inverted: dotInv } = invertMatrix(dot1);
        if (dotInv === undefined) {
            throw new Error(_t("Matrix is not invertible"));
        }
        let SSE = 0, SSR = 0;
        for (let i = 0; i < n; i++) {
            const yi = y[i] - avgY;
            let temp = 0;
            for (let j = 0; j < nVar; j++) {
                const xi = redX[i][j];
                temp += xi * coeffs[j][0];
            }
            const ei = yi - temp;
            SSE += ei * ei;
            SSR += temp * temp;
        }
        const RMSE = Math.sqrt(SSE / nDeg);
        const r2 = SSR / (SSR + SSE);
        const f_stat = SSR / nVar / (SSE / nDeg);
        const deltaCoeffs = [];
        for (let i = 0; i < nVar; i++) {
            deltaCoeffs.push(RMSE * Math.sqrt(dotInv[i][i]));
        }
        if (computeIntercept) {
            const dot2 = multiplyMatrices(dotInv, [avgX]);
            const dot3 = multiplyMatrices(transposeMatrix([avgX]), dot2);
            deltaCoeffs.push(RMSE * Math.sqrt(dot3[0][0] + 1 / y.length));
        }
        const returned = [
            [coeffs[0][0], deltaCoeffs[0], r2, f_stat, SSR],
            [coeffs[1][0], deltaCoeffs[1], RMSE, nDeg, SSE],
        ];
        for (let i = 2; i < nVar; i++) {
            returned.push([coeffs[i][0], deltaCoeffs[i], "", "", ""]);
        }
        if (computeIntercept) {
            returned.push([coeffs[nVar][0], deltaCoeffs[nVar], "", "", ""]);
        }
        else {
            returned.push([0, "", "", "", ""]);
        }
        return returned;
    }
    /*
      This function performs a polynomial regression on the data set. It returns the coefficients of
      the polynomial function that best fits the data set.
      The polynomial function is: y = c0 + c1*x + c2*x^2 + ... + cn*x^n, where n is the order (degree)
      of the polynomial. The returned coefficients are then in the form: [c0, c1, c2, ..., cn]
      The function is based on the method of least squares :
      see: https://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html
    */
    function polynomialRegression(flatY, flatX, order, intercept) {
        assertSameNumberOfElements(flatX, flatY);
        assert(() => order >= 1, _t("Function [[FUNCTION_NAME]] A regression of order less than 1 cannot be possible."));
        const yMatrix = [flatY];
        const xMatrix = flatX.map((x) => range(0, order).map((i) => Math.pow(x, order - i)));
        if (intercept) {
            xMatrix.forEach((row) => row.push(1));
        }
        const coeffs = getLMSCoefficients(xMatrix, yMatrix);
        if (!intercept) {
            coeffs.push([0]);
        }
        return coeffs;
    }
    function getLMSCoefficients(xMatrix, yMatrix) {
        const xMatrixT = transposeMatrix(xMatrix);
        const dot1 = multiplyMatrices(xMatrix, xMatrixT);
        const { inverted: dotInv } = invertMatrix(dot1);
        if (dotInv === undefined) {
            throw new Error(_t("Matrix is not invertible"));
        }
        const dot2 = multiplyMatrices(xMatrix, yMatrix);
        return transposeMatrix(multiplyMatrices(dotInv, dot2));
    }
    function evaluatePolynomial(coeffs, x, order) {
        return coeffs.reduce((acc, coeff, i) => acc + coeff * Math.pow(x, order - i), 0);
    }
    function expM(M) {
        return M.map((col) => col.map((cell) => Math.exp(cell)));
    }
    function logM(M) {
        return M.map((col) => col.map((cell) => Math.log(cell)));
    }
    function predictLinearValues(Y, X, newX, computeIntercept) {
        const { _X, _newX } = prepareDataForRegression(X, Y, newX);
        const coeffs = fullLinearRegression(_X, Y, computeIntercept, false);
        const nVar = coeffs.length - 1;
        const newY = _newX.map((col) => {
            let value = 0;
            for (let i = 0; i < nVar; i++) {
                value += coeffs[i][0] * col[nVar - i - 1];
            }
            value += coeffs[nVar][0];
            return [value];
        });
        return newY.length === newX.length ? newY : transposeMatrix(newY);
    }
    // -----------------------------------------------------------------------------
    // AVEDEV
    // -----------------------------------------------------------------------------
    const AVEDEV = {
        description: _t("Average magnitude of deviations from mean."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            let count = 0;
            const sum = reduceNumbers(values, (acc, a) => {
                count += 1;
                return acc + a;
            }, 0, this.locale);
            assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            const average = sum / count;
            return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGE
    // -----------------------------------------------------------------------------
    const AVERAGE = {
        description: _t("Numerical average value in a dataset, ignoring text."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the average value.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the average value.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            let count = 0;
            const sum = reduceNumbers(values, (acc, a) => {
                count += 1;
                return acc + a;
            }, 0, this.locale);
            assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGE.WEIGHTED
    // -----------------------------------------------------------------------------
    const rangeError = _t("[[FUNCTION_NAME]] has mismatched range sizes.");
    const negativeWeightError = _t("[[FUNCTION_NAME]] expects the weight to be positive or equal to 0.");
    const AVERAGE_WEIGHTED = {
        description: _t("Weighted average."),
        args: [
            arg("values (number, range<number>)", _t("Values to average.")),
            arg("weights (number, range<number>)", _t("Weights for each corresponding value.")),
            arg("additional_values (number, range<number>, repeating)", _t("Additional values to average.")),
            arg("additional_weights (number, range<number>, repeating)", _t("Additional weights.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (values) => {
            return isMatrix(values) ? values[0][0]?.format : values?.format;
        },
        compute: function (...values) {
            let sum = 0;
            let count = 0;
            let value;
            let weight;
            assert(() => values.length % 2 === 0, _t("Wrong number of Argument[]. Expected an even number of Argument[]."));
            for (let n = 0; n < values.length - 1; n += 2) {
                value = values[n];
                weight = values[n + 1];
                // if (typeof value != typeof weight) {
                //   throw new Error(rangeError);
                // }
                if (isMatrix(value)) {
                    assert(() => isMatrix(weight), rangeError);
                    let dimColValue = value.length;
                    let dimLinValue = value[0].length;
                    assert(() => dimColValue === weight.length && dimLinValue === weight[0].length, rangeError);
                    for (let i = 0; i < dimColValue; i++) {
                        for (let j = 0; j < dimLinValue; j++) {
                            let subValue = value[i][j];
                            let subWeight = weight[i][j];
                            let subValueIsNumber = typeof subValue === "number";
                            let subWeightIsNumber = typeof subWeight === "number";
                            // typeof subValue or subWeight can be 'number' or 'undefined'
                            assert(() => subValueIsNumber === subWeightIsNumber, _t("[[FUNCTION_NAME]] expects number values."));
                            if (subWeightIsNumber) {
                                assert(() => subWeight >= 0, negativeWeightError);
                                sum += subValue * subWeight;
                                count += subWeight;
                            }
                        }
                    }
                }
                else {
                    weight = toNumber(weight, this.locale);
                    value = toNumber(value, this.locale);
                    assert(() => weight >= 0, negativeWeightError);
                    sum += value * weight;
                    count += weight;
                }
            }
            assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return sum / count;
        },
    };
    // -----------------------------------------------------------------------------
    // AVERAGEA
    // -----------------------------------------------------------------------------
    const AVERAGEA = {
        description: _t("Numerical average value in a dataset."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the average value.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the average value.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            let count = 0;
            const sum = reduceNumbersTextAs0(values, (acc, a) => {
                count += 1;
                return acc + a;
            }, 0, this.locale);
            assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGEIF
    // -----------------------------------------------------------------------------
    const AVERAGEIF = {
        description: _t("Average of values depending on criteria."),
        args: [
            arg("criteria_range (number, range<number>)", _t("The range to check against criterion.")),
            arg("criterion (string)", _t("The pattern or test to apply to criteria_range.")),
            arg("average_range (number, range<number>, default=criteria_range)", _t("The range to average. If not included, criteria_range is used for the average instead.")),
        ],
        returns: ["NUMBER"],
        compute: function (criteriaRange, criterion, averageRange) {
            const _criteriaRange = toMatrix(criteriaRange);
            const _averageRange = averageRange === undefined ? _criteriaRange : toMatrix(averageRange);
            let count = 0;
            let sum = 0;
            visitMatchingRanges([criteriaRange, criterion], (i, j) => {
                const value = _averageRange[i][j];
                if (typeof value === "number") {
                    count += 1;
                    sum += value;
                }
            }, this.locale);
            assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AVERAGEIFS
    // -----------------------------------------------------------------------------
    const AVERAGEIFS = {
        description: _t("Average of values depending on multiple criteria."),
        args: [
            arg("average_range (range)", _t("The range to average.")),
            arg("criteria_range1 (range)", _t("The range to check against criterion1.")),
            arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1.")),
            arg("criteria_range2 (any, range, repeating)", _t("Additional criteria_range and criterion to check.")),
            arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
        ],
        returns: ["NUMBER"],
        compute: function (averageRange, ...values) {
            const _averageRange = toMatrix(averageRange);
            let count = 0;
            let sum = 0;
            visitMatchingRanges(values, (i, j) => {
                const value = _averageRange[i][j];
                if (typeof value === "number") {
                    count += 1;
                    sum += value;
                }
            }, this.locale);
            assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
            return sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNT
    // -----------------------------------------------------------------------------
    const COUNT = {
        description: _t("The number of numeric values in dataset."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range to consider when counting.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when counting.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            let count = 0;
            for (let n of values) {
                if (isMatrix(n)) {
                    for (let i of n) {
                        for (let j of i) {
                            if (typeof j === "number") {
                                count += 1;
                            }
                        }
                    }
                }
                else if (typeof n !== "string" ||
                    isNumber(n, this.locale) ||
                    parseDateTime(n, this.locale)) {
                    count += 1;
                }
            }
            return count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUNTA
    // -----------------------------------------------------------------------------
    const COUNTA = {
        description: _t("The number of values in a dataset."),
        args: [
            arg("value1 (any, range)", _t("The first value or range to consider when counting.")),
            arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when counting.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return reduceAny(values, (acc, a) => (a !== undefined && a !== null ? acc + 1 : acc), 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COVAR
    // -----------------------------------------------------------------------------
    // Note: Unlike the VAR function which corresponds to the variance over a sample (VAR.S),
    // the COVAR function corresponds to the covariance over an entire population (COVAR.P)
    const COVAR = {
        description: _t("The covariance of a dataset."),
        args: [
            arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            return covariance(dataY, dataX, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COVARIANCE.P
    // -----------------------------------------------------------------------------
    const COVARIANCE_P = {
        description: _t("The covariance of a dataset."),
        args: [
            arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            return covariance(dataY, dataX, false);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COVARIANCE.S
    // -----------------------------------------------------------------------------
    const COVARIANCE_S = {
        description: _t("The sample covariance of a dataset."),
        args: [
            arg("data_y (any, range)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (any, range)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            return covariance(dataY, dataX, true);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FORECAST
    // -----------------------------------------------------------------------------
    const FORECAST = {
        description: _t("Calculates the expected y-value for a specified x based on a linear regression of a dataset."),
        args: [
            arg("x (number, range<number>)", _t("The value(s) on the x-axis to forecast.")),
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (x, dataY, dataX) {
            const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
            return predictLinearValues([flatDataY], [flatDataX], matrixMap(toMatrix(x), (value) => toNumber(value, this.locale)), true);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // GROWTH
    // -----------------------------------------------------------------------------
    const GROWTH = {
        description: _t("Fits points to exponential growth trend."),
        args: [
            arg("known_data_y (range<number>)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal exponential growth curve.")),
            arg("known_data_x (range<number>, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")),
            arg("new_data_x (any, range, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")),
            arg("b (boolean, default=TRUE)", _t("Given a general exponential form of y = b*m^x for a curve fit, calculates b if TRUE or forces b to be 1 and only calculates the m values if FALSE.")),
        ],
        returns: ["NUMBER"],
        compute: function (knownDataY, knownDataX = [], newDataX = [], b = true) {
            return expM(predictLinearValues(logM(tryCastAsNumberMatrix(knownDataY, "the first argument (known_data_y)")), tryCastAsNumberMatrix(knownDataX, "the second argument (known_data_x)"), tryCastAsNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b)));
        },
    };
    // -----------------------------------------------------------------------------
    // INTERCEPT
    // -----------------------------------------------------------------------------
    const INTERCEPT = {
        description: _t("Compute the intercept of the linear regression."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
            const [[], [intercept]] = fullLinearRegression([flatDataX], [flatDataY]);
            return intercept;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LARGE
    // -----------------------------------------------------------------------------
    const LARGE = {
        description: _t("Nth largest element from a data set."),
        args: [
            arg("data (any, range)", _t("Array or range containing the dataset to consider.")),
            arg("n (number)", _t("The rank from largest to smallest of the element to return.")),
        ],
        returns: ["NUMBER"],
        computeValueAndFormat: function (data, n) {
            const _n = Math.trunc(toNumber(n?.value, this.locale));
            let largests = [];
            let index;
            let count = 0;
            visitAny([data], (d) => {
                if (typeof d.value === "number") {
                    index = dichotomicSearch(largests, d.value, "nextSmaller", "asc", largests.length, (array, i) => array[i].value);
                    largests.splice(index + 1, 0, d);
                    count++;
                    if (count > _n) {
                        largests.shift();
                        count--;
                    }
                }
            });
            const result = largests.shift();
            assert(() => result !== undefined, _t("[[FUNCTION_NAME]] has no valid input data."));
            assert(() => count >= _n, _t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n));
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LINEST
    // -----------------------------------------------------------------------------
    const LINEST = {
        description: _t("Compute the intercept of the linear regression."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")),
            arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying wheter to compute the slope or not")),
            arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX = [], calculateB = true, verbose = false) {
            return fullLinearRegression(tryCastAsNumberMatrix(dataX, "the first argument (data_y)"), tryCastAsNumberMatrix(dataY, "the second argument (data_x)"), toBoolean(calculateB), toBoolean(verbose));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LOGEST
    // -----------------------------------------------------------------------------
    const LOGEST = {
        description: _t("Compute the intercept of the linear regression."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>, optional, default={1;2;3;...})", _t("The range representing the array or matrix of independent data.")),
            arg("calculate_b (boolean, default=TRUE)", _t("A flag specifying wheter to compute the slope or not")),
            arg("verbose (boolean, default=FALSE)", _t("A flag specifying whether to return additional regression statistics or only the linear coefficients and the y-intercept")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX = [], calculateB = true, verbose = false) {
            const coeffs = fullLinearRegression(tryCastAsNumberMatrix(dataX, "the second argument (data_x)"), logM(tryCastAsNumberMatrix(dataY, "the first argument (data_y)")), toBoolean(calculateB), toBoolean(verbose));
            for (let i = 0; i < coeffs.length; i++) {
                coeffs[i][0] = Math.exp(coeffs[i][0]);
            }
            return coeffs;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MATTHEWS
    // -----------------------------------------------------------------------------
    const MATTHEWS = {
        description: _t("Compute the Matthews correlation coefficient of a dataset."),
        args: [
            arg("data_x (range)", _t("The range representing the array or matrix of observed data.")),
            arg("data_y (range)", _t("The range representing the array or matrix of predicted data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataX, dataY) {
            const flatX = dataX.flat();
            const flatY = dataY.flat();
            assertSameNumberOfElements(flatX, flatY);
            if (flatX.length === 0) {
                throw new Error(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters."));
            }
            const n = flatX.length;
            let trueN = 0, trueP = 0, falseP = 0, falseN = 0;
            for (let i = 0; i < n; ++i) {
                const isTrue1 = toBoolean(flatX[i]);
                const isTrue2 = toBoolean(flatY[i]);
                if (isTrue1 === isTrue2) {
                    if (isTrue1) {
                        trueP++;
                    }
                    else {
                        trueN++;
                    }
                }
                else {
                    if (isTrue1) {
                        falseN++;
                    }
                    else {
                        falseP++;
                    }
                }
            }
            return ((trueP * trueN - falseP * falseN) /
                Math.sqrt((trueP + falseP) * (trueP + falseN) * (trueN + falseP) * (trueN + falseN)));
        },
        isExported: false,
    };
    // -----------------------------------------------------------------------------
    // MAX
    // -----------------------------------------------------------------------------
    const MAX = {
        description: _t("Maximum value in a numeric dataset."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the maximum value.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            const result = reduceNumbers(values, (acc, a) => (acc < a ? a : acc), -Infinity, this.locale);
            return result === -Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MAXA
    // -----------------------------------------------------------------------------
    const MAXA = {
        description: _t("Maximum numeric value in a dataset."),
        args: [
            arg("value1 (any, range)", _t("The first value or range to consider when calculating the maximum value.")),
            arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the maximum value.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            const maxa = reduceNumbersTextAs0(values, (acc, a) => {
                return Math.max(a, acc);
            }, -Infinity, this.locale);
            return maxa === -Infinity ? 0 : maxa;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MAXIFS
    // -----------------------------------------------------------------------------
    const MAXIFS = {
        description: _t("Returns the maximum value in a range of cells, filtered by a set of criteria."),
        args: [
            arg("range (range)", _t("The range of cells from which the maximum will be determined.")),
            arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")),
            arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")),
            arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
            arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
        ],
        returns: ["NUMBER"],
        compute: function (range, ...args) {
            let result = -Infinity;
            const _range = toMatrix(range);
            visitMatchingRanges(args, (i, j) => {
                const value = _range[i][j];
                if (typeof value === "number") {
                    result = result < value ? value : result;
                }
            }, this.locale);
            return result === -Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MEDIAN
    // -----------------------------------------------------------------------------
    const MEDIAN = {
        description: _t("Median value in a numeric dataset."),
        args: [
            arg("value1 (any, range)", _t("The first value or range to consider when calculating the median value.")),
            arg("value2 (any, range, repeating)", _t("Additional values or ranges to consider when calculating the median value.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            let data = [];
            visitNumbers(values, (arg) => {
                data.push(arg);
            }, this.locale);
            return centile(data, 0.5, true, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MIN
    // -----------------------------------------------------------------------------
    const MIN = {
        description: _t("Minimum value in a numeric dataset."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the minimum value.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            const result = reduceNumbers(values, (acc, a) => (a < acc ? a : acc), Infinity, this.locale);
            return result === Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MINA
    // -----------------------------------------------------------------------------
    const MINA = {
        description: _t("Minimum numeric value in a dataset."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range to consider when calculating the minimum value.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to consider when calculating the minimum value.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1) => {
            return isMatrix(value1) ? value1[0][0]?.format : value1?.format;
        },
        compute: function (...values) {
            const mina = reduceNumbersTextAs0(values, (acc, a) => {
                return Math.min(a, acc);
            }, Infinity, this.locale);
            return mina === Infinity ? 0 : mina;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MINIFS
    // -----------------------------------------------------------------------------
    const MINIFS = {
        description: _t("Returns the minimum value in a range of cells, filtered by a set of criteria."),
        args: [
            arg("range (range)", _t("The range of cells from which the minimum will be determined.")),
            arg("criteria_range1 (range)", _t("The range of cells over which to evaluate criterion1.")),
            arg("criterion1 (string)", _t("The pattern or test to apply to criteria_range1, such that each cell that evaluates to TRUE will be included in the filtered set.")),
            arg("criteria_range2 (any, range, repeating)", _t("Additional ranges over which to evaluate the additional criteria. The filtered set will be the intersection of the sets produced by each criterion-range pair.")),
            arg("criterion2 (string, repeating)", _t("The pattern or test to apply to criteria_range2.")),
        ],
        returns: ["NUMBER"],
        compute: function (range, ...args) {
            let result = Infinity;
            const _range = toMatrix(range);
            visitMatchingRanges(args, (i, j) => {
                const value = _range[i][j];
                if (typeof value === "number") {
                    result = result > value ? value : result;
                }
            }, this.locale);
            return result === Infinity ? 0 : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PEARSON
    // -----------------------------------------------------------------------------
    const PEARSON = {
        description: _t("Compute the Pearson product-moment correlation coefficient of a dataset."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            const { flatDataX, flatDataY } = filterAndFlatData(dataX, dataY);
            if (flatDataX.length === 0) {
                throw new Error(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters."));
            }
            if (flatDataX.length < 2) {
                throw new Error(_t("[[FUNCTION_NAME]] needs at least two values for both parameters"));
            }
            const n = flatDataX.length;
            let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0, sumYY = 0;
            for (let i = 0; i < n; i++) {
                const xij = flatDataX[i];
                const yij = flatDataY[i];
                sumX += xij;
                sumY += yij;
                sumXY += xij * yij;
                sumXX += xij * xij;
                sumYY += yij * yij;
            }
            return ((n * sumXY - sumX * sumY) / Math.sqrt((n * sumXX - sumX * sumX) * (n * sumYY - sumY * sumY)));
        },
        isExported: true,
    };
    // In GSheet, CORREL is just an alias to PEARSON
    const CORREL = PEARSON;
    // -----------------------------------------------------------------------------
    // PERCENTILE
    // -----------------------------------------------------------------------------
    const PERCENTILE = {
        description: _t("Value at a given percentile of a dataset."),
        args: [
            arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
            arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (data) => {
            return isMatrix(data) ? data[0][0]?.format : data?.format;
        },
        compute: function (data, percentile) {
            return PERCENTILE_INC.compute.bind(this)(data, percentile);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PERCENTILE.EXC
    // -----------------------------------------------------------------------------
    const PERCENTILE_EXC = {
        description: _t("Value at a given percentile of a dataset exclusive of 0 and 1."),
        args: [
            arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
            arg("percentile (number)", _t("The percentile, exclusive of 0 and 1, whose value within 'data' will be calculated and returned.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (data) => {
            return isMatrix(data) ? data[0][0]?.format : data?.format;
        },
        compute: function (data, percentile) {
            return centile([data], percentile, false, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PERCENTILE.INC
    // -----------------------------------------------------------------------------
    const PERCENTILE_INC = {
        description: _t("Value at a given percentile of a dataset."),
        args: [
            arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
            arg("percentile (number)", _t("The percentile whose value within data will be calculated and returned.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (data) => {
            return isMatrix(data) ? data[0][0]?.format : data?.format;
        },
        compute: function (data, percentile) {
            return centile([data], percentile, true, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // POLYFIT
    // -----------------------------------------------------------------------------
    const POLYFIT_COEFFS = {
        description: _t("Compute the coefficients of polynomial regression of the dataset."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
            arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6.")),
            arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not.")),
        ],
        returns: ["RANGE<NUMBER>"],
        compute: function (dataY, dataX, order, intercept = true) {
            const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
            return polynomialRegression(flatDataY, flatDataX, toNumber(order, this.locale), toBoolean(intercept));
        },
        isExported: false,
    };
    // -----------------------------------------------------------------------------
    // POLYFIT.FORECAST
    // -----------------------------------------------------------------------------
    const POLYFIT_FORECAST = {
        description: _t("Predict value by computing a polynomial regression of the dataset."),
        args: [
            arg("x (number, range<number>)", _t("The value(s) on the x-axis to forecast.")),
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
            arg("order (number)", _t("The order of the polynomial to fit the data, between 1 and 6.")),
            arg("intercept (boolean, default=TRUE)", _t("A flag specifying whether to compute the intercept or not.")),
        ],
        returns: ["NUMBER"],
        compute: function (x, dataY, dataX, order, intercept = true) {
            const _order = toNumber(order, this.locale);
            const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
            const coeffs = polynomialRegression(flatDataY, flatDataX, _order, toBoolean(intercept)).flat();
            return matrixMap(toMatrix(x), (xij) => evaluatePolynomial(coeffs, toNumber(xij, this.locale), _order));
        },
        isExported: false,
    };
    // -----------------------------------------------------------------------------
    // QUARTILE
    // -----------------------------------------------------------------------------
    const QUARTILE = {
        description: _t("Value nearest to a specific quartile of a dataset."),
        args: [
            arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
            arg("quartile_number (number)", _t("Which quartile value to return.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (data) => {
            return isMatrix(data) ? data[0][0]?.format : data?.format;
        },
        compute: function (data, quartileNumber) {
            return QUARTILE_INC.compute.bind(this)(data, quartileNumber);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // QUARTILE.EXC
    // -----------------------------------------------------------------------------
    const QUARTILE_EXC = {
        description: _t("Value nearest to a specific quartile of a dataset exclusive of 0 and 4."),
        args: [
            arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
            arg("quartile_number (number)", _t("Which quartile value, exclusive of 0 and 4, to return.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (data) => {
            return isMatrix(data) ? data[0][0]?.format : data?.format;
        },
        compute: function (data, quartileNumber) {
            const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale));
            return centile([data], 0.25 * _quartileNumber, false, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // QUARTILE.INC
    // -----------------------------------------------------------------------------
    const QUARTILE_INC = {
        description: _t("Value nearest to a specific quartile of a dataset."),
        args: [
            arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
            arg("quartile_number (number)", _t("Which quartile value to return.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (data) => {
            return isMatrix(data) ? data[0][0]?.format : data?.format;
        },
        compute: function (data, quartileNumber) {
            const _quartileNumber = Math.trunc(toNumber(quartileNumber, this.locale));
            return centile([data], 0.25 * _quartileNumber, true, this.locale);
        },
        isExported: true,
    };
    // RANK
    // -----------------------------------------------------------------------------
    const RANK = {
        description: _t("Returns the rank of a specified value in a dataset."),
        args: [
            arg("value (number)", _t("The value whose rank will be determined.")),
            arg("data (range)", _t("The range containing the dataset to consider.")),
            arg("is_ascending (boolean, default=FALSE)", _t("Whether to consider the values in data in descending or ascending order.")),
        ],
        returns: ["ANY"],
        compute: function (value, data, isAscending = false) {
            const _isAscending = toBoolean(isAscending);
            const _value = toNumber(value, this.locale);
            let rank = 1;
            let found = false;
            for (const row of data) {
                for (const cell of row) {
                    if (typeof cell !== "number") {
                        continue;
                    }
                    const _cell = toNumber(cell, this.locale);
                    if (_cell === _value) {
                        found = true;
                    }
                    else if (_cell > _value !== _isAscending) {
                        rank++;
                    }
                }
            }
            if (!found) {
                throw new NotAvailableError(_t("Value not found in the given data."));
            }
            return rank;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RSQ
    // -----------------------------------------------------------------------------
    const RSQ = {
        description: _t("Compute the square of r, the Pearson product-moment correlation coefficient of a dataset."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            const pearson = PEARSON.compute.bind(this);
            return Math.pow(pearson(dataX, dataY), 2.0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SLOPE
    // -----------------------------------------------------------------------------
    const SLOPE = {
        description: _t("Compute the slope of the linear regression."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
            const [[slope]] = fullLinearRegression([flatDataX], [flatDataY]);
            return slope;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SMALL
    // -----------------------------------------------------------------------------
    const SMALL = {
        description: _t("Nth smallest element in a data set."),
        args: [
            arg("data (any, range)", _t("The array or range containing the dataset to consider.")),
            arg("n (number)", _t("The rank from smallest to largest of the element to return.")),
        ],
        returns: ["NUMBER"],
        computeValueAndFormat: function (data, n) {
            const _n = Math.trunc(toNumber(n?.value, this.locale));
            let largests = [];
            let index;
            let count = 0;
            visitAny([data], (d) => {
                if (typeof d.value === "number") {
                    index = dichotomicSearch(largests, d.value, "nextSmaller", "asc", largests.length, (array, i) => array[i].value);
                    largests.splice(index + 1, 0, d);
                    count++;
                    if (count > _n) {
                        largests.pop();
                        count--;
                    }
                }
            });
            const result = largests.pop();
            assert(() => result !== undefined, _t("[[FUNCTION_NAME]] has no valid input data."));
            assert(() => count >= _n, _t("Function [[FUNCTION_NAME]] parameter 2 value (%s) is out of range.", _n));
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SPEARMAN
    // -----------------------------------------------------------------------------
    const SPEARMAN = {
        description: _t("Compute the Spearman rank correlation coefficient of a dataset."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataX, dataY) {
            const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
            const n = flatDataX.length;
            const order = flatDataX.map((e, i) => [e, flatDataY[i]]);
            order.sort((a, b) => a[0] - b[0]);
            for (let i = 0; i < n; ++i) {
                order[i][0] = i;
            }
            order.sort((a, b) => a[1] - b[1]);
            let sum = 0.0;
            for (let i = 0; i < n; ++i) {
                sum += (order[i][0] - i) ** 2;
            }
            return 1 - (6 * sum) / (n ** 3 - n);
        },
        isExported: false,
    };
    // -----------------------------------------------------------------------------
    // STDEV
    // -----------------------------------------------------------------------------
    const STDEV = {
        description: _t("Standard deviation."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VAR.compute.bind(this)(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEV.P
    // -----------------------------------------------------------------------------
    const STDEV_P = {
        description: _t("Standard deviation of entire population."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VAR_P.compute.bind(this)(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEV.S
    // -----------------------------------------------------------------------------
    const STDEV_S = {
        description: _t("Standard deviation."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VAR_S.compute.bind(this)(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEVA
    // -----------------------------------------------------------------------------
    const STDEVA = {
        description: _t("Standard deviation of sample (text as 0)."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VARA.compute.bind(this)(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEVP
    // -----------------------------------------------------------------------------
    const STDEVP = {
        description: _t("Standard deviation of entire population."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VARP.compute.bind(this)(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STDEVPA
    // -----------------------------------------------------------------------------
    const STDEVPA = {
        description: _t("Standard deviation of entire population (text as 0)."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return Math.sqrt(VARPA.compute.bind(this)(...values));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // STEYX
    // -----------------------------------------------------------------------------
    const STEYX = {
        description: _t("Calculates the standard error of the predicted y-value for each x in the regression of a dataset."),
        args: [
            arg("data_y (range<number>)", _t("The range representing the array or matrix of dependent data.")),
            arg("data_x (range<number>)", _t("The range representing the array or matrix of independent data.")),
        ],
        returns: ["NUMBER"],
        compute: function (dataY, dataX) {
            const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
            const data = fullLinearRegression([flatDataX], [flatDataY], true, true);
            return data[1][2];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TREND
    // -----------------------------------------------------------------------------
    const TREND = {
        description: _t("Fits points to linear trend derived via least-squares."),
        args: [
            arg("known_data_y (number, range<number>)", _t("The array or range containing dependent (y) values that are already known, used to curve fit an ideal linear trend.")),
            arg("known_data_x (number, range<number>, optional, default={1;2;3;...})", _t("The values of the independent variable(s) corresponding with known_data_y.")),
            arg("new_data_x (number, range<number>, optional, default=known_data_x)", _t("The data points to return the y values for on the ideal curve fit.")),
            arg("b (boolean, optional, default=TRUE)", _t("Given a general linear form of y = m*x+b for a curve fit, calculates b if TRUE or forces b to be 0 and only calculates the m values if FALSE, i.e. forces the curve fit to pass through the origin.")),
        ],
        returns: ["NUMBER"],
        compute: function (knownDataY, knownDataX = [], newDataX = [], b = true) {
            return predictLinearValues(tryCastAsNumberMatrix(knownDataY, "the first argument (known_data_y)"), tryCastAsNumberMatrix(knownDataX, "the second argument (known_data_x)"), tryCastAsNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b));
        },
    };
    // -----------------------------------------------------------------------------
    // VAR
    // -----------------------------------------------------------------------------
    const VAR = {
        description: _t("Variance."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, true, false, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VAR.P
    // -----------------------------------------------------------------------------
    const VAR_P = {
        description: _t("Variance of entire population."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, false, false, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VAR.S
    // -----------------------------------------------------------------------------
    const VAR_S = {
        description: _t("Variance."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, true, false, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VARA
    // -----------------------------------------------------------------------------
    const VARA = {
        description: _t("Variance of sample (text as 0)."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the sample.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the sample.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, true, true, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VARP
    // -----------------------------------------------------------------------------
    const VARP = {
        description: _t("Variance of entire population."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, false, false, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VARPA
    // -----------------------------------------------------------------------------
    const VARPA = {
        description: _t("Variance of entire population (text as 0)."),
        args: [
            arg("value1 (number, range<number>)", _t("The first value or range of the population.")),
            arg("value2 (number, range<number>, repeating)", _t("Additional values or ranges to include in the population.")),
        ],
        returns: ["NUMBER"],
        compute: function (...values) {
            return variance(values, false, true, this.locale);
        },
        isExported: true,
    };

    var statistical = /*#__PURE__*/Object.freeze({
        __proto__: null,
        AVEDEV: AVEDEV,
        AVERAGE: AVERAGE,
        AVERAGEA: AVERAGEA,
        AVERAGEIF: AVERAGEIF,
        AVERAGEIFS: AVERAGEIFS,
        AVERAGE_WEIGHTED: AVERAGE_WEIGHTED,
        CORREL: CORREL,
        COUNT: COUNT,
        COUNTA: COUNTA,
        COVAR: COVAR,
        COVARIANCE_P: COVARIANCE_P,
        COVARIANCE_S: COVARIANCE_S,
        FORECAST: FORECAST,
        GROWTH: GROWTH,
        INTERCEPT: INTERCEPT,
        LARGE: LARGE,
        LINEST: LINEST,
        LOGEST: LOGEST,
        MATTHEWS: MATTHEWS,
        MAX: MAX,
        MAXA: MAXA,
        MAXIFS: MAXIFS,
        MEDIAN: MEDIAN,
        MIN: MIN,
        MINA: MINA,
        MINIFS: MINIFS,
        PEARSON: PEARSON,
        PERCENTILE: PERCENTILE,
        PERCENTILE_EXC: PERCENTILE_EXC,
        PERCENTILE_INC: PERCENTILE_INC,
        POLYFIT_COEFFS: POLYFIT_COEFFS,
        POLYFIT_FORECAST: POLYFIT_FORECAST,
        QUARTILE: QUARTILE,
        QUARTILE_EXC: QUARTILE_EXC,
        QUARTILE_INC: QUARTILE_INC,
        RANK: RANK,
        RSQ: RSQ,
        SLOPE: SLOPE,
        SMALL: SMALL,
        SPEARMAN: SPEARMAN,
        STDEV: STDEV,
        STDEVA: STDEVA,
        STDEVP: STDEVP,
        STDEVPA: STDEVPA,
        STDEV_P: STDEV_P,
        STDEV_S: STDEV_S,
        STEYX: STEYX,
        TREND: TREND,
        VAR: VAR,
        VARA: VARA,
        VARP: VARP,
        VARPA: VARPA,
        VAR_P: VAR_P,
        VAR_S: VAR_S
    });

    function getMatchingCells(database, field, criteria, locale) {
        // Example
        // # DATABASE             # CRITERIA          # field = "C"
        //
        // | A | B | C |          | A | C |
        // |===========|          |=======|
        // | 1 | x | j |          |<2 | j |
        // | 1 | Z | k |          |   | 7 |
        // | 5 | y | 7 |
        // 1 - Select coordinates of database columns ----------------------------------------------------
        const indexColNameDB = new Map();
        const dimRowDB = database.length;
        for (let indexCol = dimRowDB - 1; indexCol >= 0; indexCol--) {
            indexColNameDB.set(toString(database[indexCol][0]).toUpperCase(), indexCol);
        }
        // Example continuation: indexColNameDB = {"A" => 0, "B" => 1, "C" => 2}
        // 2 - Check if the field parameter exists in the column names of the database -------------------
        // field may either be a text label corresponding to a column header in the
        // first row of database or a numeric index indicating which column to consider,
        // where the first column has the value 1.
        if (typeof field !== "number" && typeof field !== "string") {
            throw new Error(_t("The field must be a number or a string"));
        }
        let index;
        if (typeof field === "number") {
            index = Math.trunc(field) - 1;
            if (index < 0 || dimRowDB - 1 < index) {
                throw new Error(_t("The field (%s) must be one of %s or must be a number between 1 and %s inclusive.", field.toString(), dimRowDB.toString()));
            }
        }
        else {
            const colName = toString(field).toUpperCase();
            index = indexColNameDB.get(colName) ?? -1;
            if (index === -1) {
                throw new Error(_t("The field (%s) must be one of %s.", toString(field), [...indexColNameDB.keys()].toString()));
            }
        }
        // Example continuation: index = 2
        // 3 - For each criteria row, find database row that correspond ----------------------------------
        const dimColCriteria = criteria[0].length;
        if (dimColCriteria < 2) {
            throw new Error(_t("The criteria range contains %s row, it must be at least 2 rows.", dimColCriteria.toString()));
        }
        let matchingRows = new Set();
        const dimColDB = database[0].length;
        for (let indexRow = 1; indexRow < dimColCriteria; indexRow++) {
            let args = [];
            let existColNameDB = true;
            for (let indexCol = 0; indexCol < criteria.length; indexCol++) {
                const currentName = toString(criteria[indexCol][0]).toUpperCase();
                const indexColDB = indexColNameDB.get(currentName);
                const criter = criteria[indexCol][indexRow];
                if (criter !== null) {
                    if (indexColDB !== undefined) {
                        args.push([database[indexColDB].slice(1, dimColDB)]);
                        args.push(criter);
                    }
                    else {
                        existColNameDB = false;
                        break;
                    }
                }
            }
            // Example continuation: args1 = [[1,1,5], "<2", ["j","k",7], "j"]
            // Example continuation: args2 = [["j","k",7], "7"]
            if (existColNameDB) {
                if (args.length > 0) {
                    visitMatchingRanges(args, (i, j) => {
                        matchingRows.add(j);
                    }, locale, true);
                }
                else {
                    // return indices of each database row when a criteria table row is void
                    matchingRows = new Set(Array(dimColDB - 1).keys());
                    break;
                }
            }
        }
        // Example continuation: matchingRows = {0, 2}
        // 4 - return for each database row corresponding, the cells corresponding to the field parameter
        const fieldCol = database[index];
        // Example continuation:: fieldCol = ["C", "j", "k", 7]
        const matchingCells = [...matchingRows].map((x) => fieldCol[x + 1]);
        // Example continuation:: matchingCells = ["j", 7]
        return matchingCells;
    }
    const databaseArgs = [
        arg("database (range)", _t("The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.")),
        arg("field (any)", _t("Indicates which column in database contains the values to be extracted and operated on.")),
        arg("criteria (range)", _t("An array or range containing zero or more criteria to filter the database values by before operating.")),
    ];
    // -----------------------------------------------------------------------------
    // DAVERAGE
    // -----------------------------------------------------------------------------
    const DAVERAGE = {
        description: _t("Average of a set of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return AVERAGE.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DCOUNT
    // -----------------------------------------------------------------------------
    const DCOUNT = {
        description: _t("Counts values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return COUNT.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DCOUNTA
    // -----------------------------------------------------------------------------
    const DCOUNTA = {
        description: _t("Counts values and text from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return COUNTA.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DGET
    // -----------------------------------------------------------------------------
    const DGET = {
        description: _t("Single value from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            assert(() => cells.length === 1, _t("More than one match found in DGET evaluation."));
            return cells[0];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DMAX
    // -----------------------------------------------------------------------------
    const DMAX = {
        description: _t("Maximum of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return MAX.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DMIN
    // -----------------------------------------------------------------------------
    const DMIN = {
        description: _t("Minimum of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return MIN.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DPRODUCT
    // -----------------------------------------------------------------------------
    const DPRODUCT = {
        description: _t("Product of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return PRODUCT.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DSTDEV
    // -----------------------------------------------------------------------------
    const DSTDEV = {
        description: _t("Standard deviation of population sample from table."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return STDEV.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DSTDEVP
    // -----------------------------------------------------------------------------
    const DSTDEVP = {
        description: _t("Standard deviation of entire population from table."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return STDEVP.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DSUM
    // -----------------------------------------------------------------------------
    const DSUM = {
        description: _t("Sum of values from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return SUM.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DVAR
    // -----------------------------------------------------------------------------
    const DVAR = {
        description: _t("Variance of population sample from table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return VAR.compute.bind(this)([cells]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DVARP
    // -----------------------------------------------------------------------------
    const DVARP = {
        description: _t("Variance of a population from a table-like range."),
        args: databaseArgs,
        returns: ["NUMBER"],
        compute: function (database, field, criteria) {
            const cells = getMatchingCells(database, field, criteria, this.locale);
            return VARP.compute.bind(this)([cells]);
        },
        isExported: true,
    };

    var database = /*#__PURE__*/Object.freeze({
        __proto__: null,
        DAVERAGE: DAVERAGE,
        DCOUNT: DCOUNT,
        DCOUNTA: DCOUNTA,
        DGET: DGET,
        DMAX: DMAX,
        DMIN: DMIN,
        DPRODUCT: DPRODUCT,
        DSTDEV: DSTDEV,
        DSTDEVP: DSTDEVP,
        DSUM: DSUM,
        DVAR: DVAR,
        DVARP: DVARP
    });

    const DEFAULT_TYPE = 1;
    const DEFAULT_WEEKEND = 1;
    var TIME_UNIT;
    (function (TIME_UNIT) {
        TIME_UNIT["WHOLE_YEARS"] = "Y";
        TIME_UNIT["WHOLE_MONTHS"] = "M";
        TIME_UNIT["WHOLE_DAYS"] = "D";
        TIME_UNIT["DAYS_WITHOUT_WHOLE_MONTHS"] = "MD";
        TIME_UNIT["MONTH_WITHOUT_WHOLE_YEARS"] = "YM";
        TIME_UNIT["DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR"] = "YD";
    })(TIME_UNIT || (TIME_UNIT = {}));
    // -----------------------------------------------------------------------------
    // DATE
    // -----------------------------------------------------------------------------
    const DATE = {
        description: _t("Converts year/month/day into a date."),
        args: [
            arg("year (number)", _t("The year component of the date.")),
            arg("month (number)", _t("The month component of the date.")),
            arg("day (number)", _t("The day component of the date.")),
        ],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (year, month, day) {
            let _year = Math.trunc(toNumber(year, this.locale));
            const _month = Math.trunc(toNumber(month, this.locale));
            const _day = Math.trunc(toNumber(day, this.locale));
            // For years less than 0 or greater than 10000, return #ERROR.
            assert(() => 0 <= _year && _year <= 9999, _t("The year (%s) must be between 0 and 9999 inclusive.", _year.toString()));
            // Between 0 and 1899, we add that value to 1900 to calculate the year
            if (_year < 1900) {
                _year += 1900;
            }
            const jsDate = new DateTime(_year, _month - 1, _day);
            const result = jsDateToRoundNumber(jsDate);
            assert(() => result >= 0, _t("The function [[FUNCTION_NAME]] result must be greater than or equal 01/01/1900."));
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DATEDIF
    // -----------------------------------------------------------------------------
    const DATEDIF = {
        description: _t("Calculates the number of days, months, or years between two dates."),
        args: [
            arg("start_date (date)", _t("The start date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.")),
            arg("end_date (date)", _t("The end date to consider in the calculation. Must be a reference to a cell containing a DATE, a function returning a DATE type, or a number.")),
            arg("unit (string)", _t('A text abbreviation for unit of time. Accepted values are "Y" (the number of whole years between start_date and end_date), "M" (the number of whole months between start_date and end_date), "D" (the number of days between start_date and end_date), "MD" (the number of days between start_date and end_date after subtracting whole months), "YM" (the number of whole months between start_date and end_date after subtracting whole years), "YD" (the number of days between start_date and end_date, assuming start_date and end_date were no more than one year apart).')),
        ],
        returns: ["NUMBER"],
        compute: function (startDate, endDate, unit) {
            const _unit = toString(unit).toUpperCase();
            assert(() => Object.values(TIME_UNIT).includes(_unit), expectStringSetError(Object.values(TIME_UNIT), toString(unit)));
            const _startDate = Math.trunc(toNumber(startDate, this.locale));
            const _endDate = Math.trunc(toNumber(endDate, this.locale));
            const jsStartDate = numberToJsDate(_startDate);
            const jsEndDate = numberToJsDate(_endDate);
            assert(() => _endDate >= _startDate, _t("start_date (%s) should be on or before end_date (%s).", jsStartDate.toLocaleDateString(), jsEndDate.toLocaleDateString()));
            switch (_unit) {
                case TIME_UNIT.WHOLE_YEARS:
                    return getTimeDifferenceInWholeYears(jsStartDate, jsEndDate);
                case TIME_UNIT.WHOLE_MONTHS:
                    return getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate);
                case TIME_UNIT.WHOLE_DAYS: {
                    return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate);
                }
                case TIME_UNIT.MONTH_WITHOUT_WHOLE_YEARS: {
                    return (getTimeDifferenceInWholeMonths(jsStartDate, jsEndDate) -
                        getTimeDifferenceInWholeYears(jsStartDate, jsEndDate) * 12);
                }
                case TIME_UNIT.DAYS_WITHOUT_WHOLE_MONTHS:
                    // Using "MD" may get incorrect result in Excel
                    // See: https://support.microsoft.com/en-us/office/datedif-function-25dba1a4-2812-480b-84dd-8b32a451b35c
                    let days = jsEndDate.getDate() - jsStartDate.getDate();
                    if (days < 0) {
                        const monthBeforeEndMonth = new DateTime(jsEndDate.getFullYear(), jsEndDate.getMonth() - 1, 1);
                        const daysInMonthBeforeEndMonth = getDaysInMonth(monthBeforeEndMonth);
                        days = daysInMonthBeforeEndMonth - Math.abs(days);
                    }
                    return days;
                case TIME_UNIT.DAYS_BETWEEN_NO_MORE_THAN_ONE_YEAR: {
                    if (areTwoDatesWithinOneYear(_startDate, _endDate)) {
                        return getTimeDifferenceInWholeDays(jsStartDate, jsEndDate);
                    }
                    const endDateWithinOneYear = new DateTime(jsStartDate.getFullYear(), jsEndDate.getMonth(), jsEndDate.getDate());
                    let days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear);
                    if (days < 0) {
                        endDateWithinOneYear.setFullYear(jsStartDate.getFullYear() + 1);
                        days = getTimeDifferenceInWholeDays(jsStartDate, endDateWithinOneYear);
                    }
                    return days;
                }
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DATEVALUE
    // -----------------------------------------------------------------------------
    const DATEVALUE = {
        description: _t("Converts a date string to a date value."),
        args: [arg("date_string (string)", _t("The string representing the date."))],
        returns: ["NUMBER"],
        compute: function (dateString) {
            const _dateString = toString(dateString);
            const internalDate = parseDateTime(_dateString, this.locale);
            assert(() => internalDate !== null, _t("The date_string (%s) cannot be parsed to date/time.", _dateString.toString()));
            return Math.trunc(internalDate.value);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DAY
    // -----------------------------------------------------------------------------
    const DAY = {
        description: _t("Day of the month that a specific date falls on."),
        args: [arg("date (string)", _t("The date from which to extract the day."))],
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date, this.locale).getDate();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DAYS
    // -----------------------------------------------------------------------------
    const DAYS = {
        description: _t("Number of days between two dates."),
        args: [
            arg("end_date (date)", _t("The end of the date range.")),
            arg("start_date (date)", _t("The start of the date range.")),
        ],
        returns: ["NUMBER"],
        compute: function (endDate, startDate) {
            const _endDate = toJsDate(endDate, this.locale);
            const _startDate = toJsDate(startDate, this.locale);
            const dateDif = _endDate.getTime() - _startDate.getTime();
            return Math.round(dateDif / MS_PER_DAY);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DAYS360
    // -----------------------------------------------------------------------------
    const DEFAULT_DAY_COUNT_METHOD = 0;
    const DAYS360 = {
        description: _t("Number of days between two dates on a 360-day year (months of 30 days)."),
        args: [
            arg("start_date (date)", _t("The start date to consider in the calculation.")),
            arg("end_date (date)", _t("The end date to consider in the calculation.")),
            arg(`method (number, default=${DEFAULT_DAY_COUNT_METHOD})`, _t("An indicator of what day count method to use. (0) US NASD method (1) European method")),
        ],
        returns: ["NUMBER"],
        compute: function (startDate, endDate, method = DEFAULT_DAY_COUNT_METHOD) {
            const _startDate = toNumber(startDate, this.locale);
            const _endDate = toNumber(endDate, this.locale);
            const dayCountConvention = toBoolean(method) ? 4 : 0;
            const yearFrac = YEARFRAC.compute.bind(this)(startDate, endDate, dayCountConvention);
            return Math.sign(_endDate - _startDate) * Math.round(yearFrac * 360);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EDATE
    // -----------------------------------------------------------------------------
    const EDATE = {
        description: _t("Date a number of months before/after another date."),
        args: [
            arg("start_date (date)", _t("The date from which to calculate the result.")),
            arg("months (number)", _t("The number of months before (negative) or after (positive) 'start_date' to calculate.")),
        ],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (startDate, months) {
            const _startDate = toJsDate(startDate, this.locale);
            const _months = Math.trunc(toNumber(months, this.locale));
            const jsDate = addMonthsToDate(_startDate, _months, false);
            return jsDateToRoundNumber(jsDate);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EOMONTH
    // -----------------------------------------------------------------------------
    const EOMONTH = {
        description: _t("Last day of a month before or after a date."),
        args: [
            arg("start_date (date)", _t("The date from which to calculate the result.")),
            arg("months (number)", _t("The number of months before (negative) or after (positive) 'start_date' to consider.")),
        ],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (startDate, months) {
            const _startDate = toJsDate(startDate, this.locale);
            const _months = Math.trunc(toNumber(months, this.locale));
            const yStart = _startDate.getFullYear();
            const mStart = _startDate.getMonth();
            const jsDate = new DateTime(yStart, mStart + _months + 1, 0);
            return jsDateToRoundNumber(jsDate);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // HOUR
    // -----------------------------------------------------------------------------
    const HOUR = {
        description: _t("Hour component of a specific time."),
        args: [arg("time (date)", _t("The time from which to calculate the hour component."))],
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date, this.locale).getHours();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISOWEEKNUM
    // -----------------------------------------------------------------------------
    const ISOWEEKNUM = {
        description: _t("ISO week number of the year."),
        args: [
            arg("date (date)", _t("The date for which to determine the ISO week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.")),
        ],
        returns: ["NUMBER"],
        compute: function (date) {
            const _date = toJsDate(date, this.locale);
            const y = _date.getFullYear();
            // 1 - As the 1st week of a year can start the previous year or after the 1st
            // january we first look if the date is in the weeks of the current year, previous
            // year or year after.
            // A - We look for the current year, the first days of the first week
            // and the last days of the last week
            // The first week of the year is the week that contains the first
            // Thursday of the year.
            let firstThursday = 1;
            while (new DateTime(y, 0, firstThursday).getDay() !== 4) {
                firstThursday += 1;
            }
            const firstDayOfFirstWeek = new DateTime(y, 0, firstThursday - 3);
            // The last week of the year is the week that contains the last Thursday of
            // the year.
            let lastThursday = 31;
            while (new DateTime(y, 11, lastThursday).getDay() !== 4) {
                lastThursday -= 1;
            }
            const lastDayOfLastWeek = new DateTime(y, 11, lastThursday + 3);
            // B - If our date > lastDayOfLastWeek then it's in the weeks of the year after
            // If our date < firstDayOfFirstWeek then it's in the weeks of the year before
            let offsetYear;
            if (firstDayOfFirstWeek.getTime() <= _date.getTime()) {
                if (_date.getTime() <= lastDayOfLastWeek.getTime()) {
                    offsetYear = 0;
                }
                else {
                    offsetYear = 1;
                }
            }
            else {
                offsetYear = -1;
            }
            // 2 - now that the year is known, we are looking at the difference between
            // the first day of this year and the date. The difference in days divided by
            // 7 gives us the week number
            let firstDay;
            switch (offsetYear) {
                case 0:
                    firstDay = firstDayOfFirstWeek;
                    break;
                case 1:
                    // firstDay is the 1st day of the 1st week of the year after
                    // firstDay = lastDayOfLastWeek + 1 Day
                    firstDay = new DateTime(y, 11, lastThursday + 3 + 1);
                    break;
                case -1:
                    // firstDay is the 1st day of the 1st week of the previous year.
                    // The first week of the previous year is the week that contains the
                    // first Thursday of the previous year.
                    let firstThursdayPreviousYear = 1;
                    while (new DateTime(y - 1, 0, firstThursdayPreviousYear).getDay() !== 4) {
                        firstThursdayPreviousYear += 1;
                    }
                    firstDay = new DateTime(y - 1, 0, firstThursdayPreviousYear - 3);
                    break;
            }
            const diff = (_date.getTime() - firstDay.getTime()) / MS_PER_DAY;
            return Math.floor(diff / 7) + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MINUTE
    // -----------------------------------------------------------------------------
    const MINUTE = {
        description: _t("Minute component of a specific time."),
        args: [arg("time (date)", _t("The time from which to calculate the minute component."))],
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date, this.locale).getMinutes();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MONTH
    // -----------------------------------------------------------------------------
    const MONTH = {
        description: _t("Month of the year a specific date falls in"),
        args: [arg("date (date)", _t("The date from which to extract the month."))],
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date, this.locale).getMonth() + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NETWORKDAYS
    // -----------------------------------------------------------------------------
    const NETWORKDAYS = {
        description: _t("Net working days between two provided days."),
        args: [
            arg("start_date (date)", _t("The start date of the period from which to calculate the number of net working days.")),
            arg("end_date (date)", _t("The end date of the period from which to calculate the number of net working days.")),
            arg("holidays (date, range<date>, optional)", _t("A range or array constant containing the date serial numbers to consider holidays.")),
        ],
        returns: ["NUMBER"],
        compute: function (startDate, endDate, holidays) {
            return NETWORKDAYS_INTL.compute.bind(this)(startDate, endDate, 1, holidays);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NETWORKDAYS.INTL
    // -----------------------------------------------------------------------------
    /**
     * Transform weekend Spreadsheet information into Date Day JavaScript information.
     * Take string (String method) or number (Number method), return array of numbers.
     *
     * String method: weekends can be specified using seven 0’s and 1’s, where the
     * first number in the set represents Monday and the last number is for Sunday.
     * A zero means that the day is a work day, a 1 means that the day is a weekend.
     * For example, “0000011” would mean Saturday and Sunday are weekends.
     *
     * Number method: instead of using the string method above, a single number can
     * be used. 1 = Saturday/Sunday are weekends, 2 = Sunday/Monday, and this pattern
     * repeats until 7 = Friday/Saturday. 11 = Sunday is the only weekend, 12 = Monday
     * is the only weekend, and this pattern repeats until 17 = Saturday is the only
     * weekend.
     *
     * Example:
     * - 11 return [0] (correspond to Sunday)
     * - 12 return [1] (correspond to Monday)
     * - 3 return [1,2] (correspond to Monday and Tuesday)
     * - "0101010" return [2,4,6] (correspond to Tuesday, Thursday and Saturday)
     */
    function weekendToDayNumber(weekend) {
        // case "string"
        if (typeof weekend === "string") {
            assert(() => {
                if (weekend.length !== 7) {
                    return false;
                }
                for (let day of weekend) {
                    if (day !== "0" && day !== "1") {
                        return false;
                    }
                }
                return true;
            }, _t('When weekend is a string (%s) it must be composed of "0" or "1".', weekend));
            let result = [];
            for (let i = 0; i < 7; i++) {
                if (weekend[i] === "1") {
                    result.push((i + 1) % 7);
                }
            }
            return result;
        }
        //case "number"
        if (typeof weekend === "number") {
            assert(() => (1 <= weekend && weekend <= 7) || (11 <= weekend && weekend <= 17), _t("The weekend (%s) must be a string or a number in the range 1-7 or 11-17.", weekend.toString()));
            // case 1 <= weekend <= 7
            if (weekend <= 7) {
                // 1 = Saturday/Sunday are weekends
                // 2 = Sunday/Monday
                // ...
                // 7 = Friday/Saturday.
                return [weekend - 2 === -1 ? 6 : weekend - 2, weekend - 1];
            }
            // case 11 <= weekend <= 17
            // 11 = Sunday is the only weekend
            // 12 = Monday is the only weekend
            // ...
            // 17 = Saturday is the only weekend.
            return [weekend - 11];
        }
        throw Error(_t("The weekend must be a number or a string."));
    }
    const NETWORKDAYS_INTL = {
        description: _t("Net working days between two dates (specifying weekends)."),
        args: [
            arg("start_date (date)", _t("The start date of the period from which to calculate the number of net working days.")),
            arg("end_date (date)", _t("The end date of the period from which to calculate the number of net working days.")),
            arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t("A number or string representing which days of the week are considered weekends.")),
            arg("holidays (date, range<date>, optional)", _t("A range or array constant containing the dates to consider as holidays.")),
        ],
        returns: ["NUMBER"],
        compute: function (startDate, endDate, weekend = DEFAULT_WEEKEND, holidays) {
            const _startDate = toJsDate(startDate, this.locale);
            const _endDate = toJsDate(endDate, this.locale);
            const daysWeekend = weekendToDayNumber(weekend);
            let timesHoliday = new Set();
            if (holidays !== undefined) {
                visitAny([holidays], (h) => {
                    const holiday = toJsDate(h, this.locale);
                    timesHoliday.add(holiday.getTime());
                });
            }
            const invertDate = _startDate.getTime() > _endDate.getTime();
            const stopDate = DateTime.fromTimestamp((invertDate ? _startDate : _endDate).getTime());
            let stepDate = DateTime.fromTimestamp((invertDate ? _endDate : _startDate).getTime());
            const timeStopDate = stopDate.getTime();
            let timeStepDate = stepDate.getTime();
            let netWorkingDay = 0;
            while (timeStepDate <= timeStopDate) {
                if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) {
                    netWorkingDay += 1;
                }
                stepDate.setDate(stepDate.getDate() + 1);
                timeStepDate = stepDate.getTime();
            }
            return invertDate ? -netWorkingDay : netWorkingDay;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NOW
    // -----------------------------------------------------------------------------
    const NOW = {
        description: _t("Current date and time as a date value."),
        args: [],
        returns: ["DATE"],
        computeFormat: function () {
            return getDateTimeFormat(this.locale);
        },
        compute: function () {
            let today = DateTime.now();
            const delta = today.getTime() - INITIAL_1900_DAY.getTime();
            const time = today.getHours() / 24 + today.getMinutes() / 1440 + today.getSeconds() / 86400;
            return Math.floor(delta / MS_PER_DAY) + time;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SECOND
    // -----------------------------------------------------------------------------
    const SECOND = {
        description: _t("Minute component of a specific time."),
        args: [arg("time (date)", _t("The time from which to calculate the second component."))],
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date, this.locale).getSeconds();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TIME
    // -----------------------------------------------------------------------------
    const TIME = {
        description: _t("Converts hour/minute/second into a time."),
        args: [
            arg("hour (number)", _t("The hour component of the time.")),
            arg("minute (number)", _t("The minute component of the time.")),
            arg("second (number)", _t("The second component of the time.")),
        ],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.timeFormat;
        },
        compute: function (hour, minute, second) {
            let _hour = Math.trunc(toNumber(hour, this.locale));
            let _minute = Math.trunc(toNumber(minute, this.locale));
            let _second = Math.trunc(toNumber(second, this.locale));
            _minute += Math.floor(_second / 60);
            _second = (_second % 60) + (_second < 0 ? 60 : 0);
            _hour += Math.floor(_minute / 60);
            _minute = (_minute % 60) + (_minute < 0 ? 60 : 0);
            _hour %= 24;
            assert(() => _hour >= 0, _t("The function [[FUNCTION_NAME]] result cannot be negative"));
            return _hour / 24 + _minute / (24 * 60) + _second / (24 * 60 * 60);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TIMEVALUE
    // -----------------------------------------------------------------------------
    const TIMEVALUE = {
        description: _t("Converts a time string into its serial number representation."),
        args: [arg("time_string (string)", _t("The string that holds the time representation."))],
        returns: ["NUMBER"],
        compute: function (timeString) {
            const _timeString = toString(timeString);
            const internalDate = parseDateTime(_timeString, this.locale);
            assert(() => internalDate !== null, _t("The time_string (%s) cannot be parsed to date/time.", _timeString));
            const result = internalDate.value - Math.trunc(internalDate.value);
            return result < 0 ? 1 + result : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TODAY
    // -----------------------------------------------------------------------------
    const TODAY = {
        description: _t("Current date as a date value."),
        args: [],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function () {
            const today = DateTime.now();
            const jsDate = new DateTime(today.getFullYear(), today.getMonth(), today.getDate());
            return jsDateToRoundNumber(jsDate);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // WEEKDAY
    // -----------------------------------------------------------------------------
    const WEEKDAY = {
        description: _t("Day of the week of the date provided (as number)."),
        args: [
            arg("date (date)", _t("The date for which to determine the day of the week. Must be a reference to a cell containing a date, a function returning a date type, or a number.")),
            arg(`type (number, default=${DEFAULT_TYPE})`, _t("A number indicating which numbering system to use to represent weekdays. By default, counts starting with Sunday = 1.")),
        ],
        returns: ["NUMBER"],
        compute: function (date, type = DEFAULT_TYPE) {
            const _date = toJsDate(date, this.locale);
            const _type = Math.round(toNumber(type, this.locale));
            const m = _date.getDay();
            assert(() => [1, 2, 3].includes(_type), _t("The type (%s) must be 1, 2 or 3.", _type.toString()));
            if (_type === 1)
                return m + 1;
            if (_type === 2)
                return m === 0 ? 7 : m;
            return m === 0 ? 6 : m - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // WEEKNUM
    // -----------------------------------------------------------------------------
    const WEEKNUM = {
        description: _t("Week number of the year."),
        args: [
            arg("date (date)", _t("The date for which to determine the week number. Must be a reference to a cell containing a date, a function returning a date type, or a number.")),
            arg(`type (number, default=${DEFAULT_TYPE})`, _t("A number representing the day that a week starts on. Sunday = 1.")),
        ],
        returns: ["NUMBER"],
        compute: function (date, type = DEFAULT_TYPE) {
            const _date = toJsDate(date, this.locale);
            const _type = Math.round(toNumber(type, this.locale));
            assert(() => _type === 1 || _type === 2 || (11 <= _type && _type <= 17) || _type === 21, _t("The type (%s) is out of range.", _type.toString()));
            if (_type === 21) {
                return ISOWEEKNUM.compute.bind(this)(date);
            }
            let startDayOfWeek;
            if (_type === 1 || _type === 2) {
                startDayOfWeek = _type - 1;
            }
            else {
                // case 11 <= _type <= 17
                startDayOfWeek = _type - 10 === 7 ? 0 : _type - 10;
            }
            const y = _date.getFullYear();
            let dayStart = 1;
            let startDayOfFirstWeek = new DateTime(y, 0, dayStart);
            while (startDayOfFirstWeek.getDay() !== startDayOfWeek) {
                dayStart += 1;
                startDayOfFirstWeek = new DateTime(y, 0, dayStart);
            }
            const dif = (_date.getTime() - startDayOfFirstWeek.getTime()) / MS_PER_DAY;
            if (dif < 0) {
                return 1;
            }
            return Math.floor(dif / 7) + (dayStart === 1 ? 1 : 2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // WORKDAY
    // -----------------------------------------------------------------------------
    const WORKDAY = {
        description: _t("Date after a number of workdays."),
        args: [
            arg("start_date (date)", _t("The date from which to begin counting.")),
            arg("num_days (number)", _t("The number of working days to advance from start_date. If negative, counts backwards.")),
            arg("holidays (date, range<date>, optional)", _t("A range or array constant containing the dates to consider holidays.")),
        ],
        returns: ["NUMBER"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (startDate, numDays, holidays = undefined) {
            return WORKDAY_INTL.compute.bind(this)(startDate, numDays, 1, holidays ?? null);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // WORKDAY.INTL
    // -----------------------------------------------------------------------------
    const WORKDAY_INTL = {
        description: _t("Date after a number of workdays (specifying weekends)."),
        args: [
            arg("start_date (date)", _t("The date from which to begin counting.")),
            arg("num_days (number)", _t("The number of working days to advance from start_date. If negative, counts backwards.")),
            arg(`weekend (any, default=${DEFAULT_WEEKEND})`, _t("A number or string representing which days of the week are considered weekends.")),
            arg("holidays (date, range<date>, optional)", _t("A range or array constant containing the dates to consider holidays.")),
        ],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (startDate, numDays, weekend = DEFAULT_WEEKEND, holidays) {
            let _startDate = toJsDate(startDate, this.locale);
            let _numDays = Math.trunc(toNumber(numDays, this.locale));
            if (typeof weekend === "string") {
                assert(() => weekend !== "1111111", _t("The weekend (%s) must be different from '1111111'.", weekend));
            }
            const daysWeekend = weekendToDayNumber(weekend);
            let timesHoliday = new Set();
            if (holidays !== undefined) {
                visitAny([holidays], (h) => {
                    const holiday = toJsDate(h, this.locale);
                    timesHoliday.add(holiday.getTime());
                });
            }
            let stepDate = DateTime.fromTimestamp(_startDate.getTime());
            let timeStepDate = stepDate.getTime();
            const unitDay = Math.sign(_numDays);
            let stepDay = Math.abs(_numDays);
            while (stepDay > 0) {
                stepDate.setDate(stepDate.getDate() + unitDay);
                timeStepDate = stepDate.getTime();
                if (!daysWeekend.includes(stepDate.getDay()) && !timesHoliday.has(timeStepDate)) {
                    stepDay -= 1;
                }
            }
            const delta = timeStepDate - INITIAL_1900_DAY.getTime();
            return Math.round(delta / MS_PER_DAY);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // YEAR
    // -----------------------------------------------------------------------------
    const YEAR = {
        description: _t("Year specified by a given date."),
        args: [arg("date (date)", _t("The date from which to extract the year."))],
        returns: ["NUMBER"],
        compute: function (date) {
            return toJsDate(date, this.locale).getFullYear();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // YEARFRAC
    // -----------------------------------------------------------------------------
    const DEFAULT_DAY_COUNT_CONVENTION$1 = 0;
    const YEARFRAC = {
        description: _t("Exact number of years between two dates."),
        args: [
            arg("start_date (date)", _t("The start date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.")),
            arg("end_date (date)", _t("The end date to consider in the calculation. Must be a reference to a cell containing a date, a function returning a date type, or a number.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION$1})`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (startDate, endDate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION$1) {
            let _startDate = Math.trunc(toNumber(startDate, this.locale));
            let _endDate = Math.trunc(toNumber(endDate, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assert(() => _startDate >= 0, _t("The start_date (%s) must be positive or null.", _startDate.toString()));
            assert(() => _endDate >= 0, _t("The end_date (%s) must be positive or null.", _endDate.toString()));
            assert(() => 0 <= _dayCountConvention && _dayCountConvention <= 4, _t("The day_count_convention (%s) must be between 0 and 4 inclusive.", _dayCountConvention.toString()));
            return getYearFrac(_startDate, _endDate, _dayCountConvention);
        },
    };
    // -----------------------------------------------------------------------------
    // MONTH.START
    // -----------------------------------------------------------------------------
    const MONTH_START = {
        description: _t("First day of the month preceding a date."),
        args: [arg("date (date)", _t("The date from which to calculate the result."))],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (date) {
            const _startDate = toJsDate(date, this.locale);
            const yStart = _startDate.getFullYear();
            const mStart = _startDate.getMonth();
            const jsDate = new DateTime(yStart, mStart, 1);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // MONTH.END
    // -----------------------------------------------------------------------------
    const MONTH_END = {
        description: _t("Last day of the month following a date."),
        args: [arg("date (date)", _t("The date from which to calculate the result."))],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (date) {
            return EOMONTH.compute.bind(this)(date, 0);
        },
    };
    // -----------------------------------------------------------------------------
    // QUARTER
    // -----------------------------------------------------------------------------
    const QUARTER = {
        description: _t("Quarter of the year a specific date falls in"),
        args: [arg("date (date)", _t("The date from which to extract the quarter."))],
        returns: ["NUMBER"],
        compute: function (date) {
            return Math.ceil((toJsDate(date, this.locale).getMonth() + 1) / 3);
        },
    };
    // -----------------------------------------------------------------------------
    // QUARTER.START
    // -----------------------------------------------------------------------------
    const QUARTER_START = {
        description: _t("First day of the quarter of the year a specific date falls in."),
        args: [arg("date (date)", _t("The date from which to calculate the start of quarter."))],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (date) {
            const quarter = QUARTER.compute.bind(this)(date);
            const year = YEAR.compute.bind(this)(date);
            const jsDate = new DateTime(year, (quarter - 1) * 3, 1);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // QUARTER.END
    // -----------------------------------------------------------------------------
    const QUARTER_END = {
        description: _t("Last day of the quarter of the year a specific date falls in."),
        args: [arg("date (date)", _t("The date from which to calculate the end of quarter."))],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (date) {
            const quarter = QUARTER.compute.bind(this)(date);
            const year = YEAR.compute.bind(this)(date);
            const jsDate = new DateTime(year, quarter * 3, 0);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // YEAR.START
    // -----------------------------------------------------------------------------
    const YEAR_START = {
        description: _t("First day of the year a specific date falls in."),
        args: [arg("date (date)", _t("The date from which to calculate the start of the year."))],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (date) {
            const year = YEAR.compute.bind(this)(date);
            const jsDate = new DateTime(year, 0, 1);
            return jsDateToRoundNumber(jsDate);
        },
    };
    // -----------------------------------------------------------------------------
    // YEAR.END
    // -----------------------------------------------------------------------------
    const YEAR_END = {
        description: _t("Last day of the year a specific date falls in."),
        args: [arg("date (date)", _t("The date from which to calculate the end of the year."))],
        returns: ["DATE"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (date) {
            const year = YEAR.compute.bind(this)(date);
            const jsDate = new DateTime(year + 1, 0, 0);
            return jsDateToRoundNumber(jsDate);
        },
    };

    var date = /*#__PURE__*/Object.freeze({
        __proto__: null,
        DATE: DATE,
        DATEDIF: DATEDIF,
        DATEVALUE: DATEVALUE,
        DAY: DAY,
        DAYS: DAYS,
        DAYS360: DAYS360,
        EDATE: EDATE,
        EOMONTH: EOMONTH,
        HOUR: HOUR,
        ISOWEEKNUM: ISOWEEKNUM,
        MINUTE: MINUTE,
        MONTH: MONTH,
        MONTH_END: MONTH_END,
        MONTH_START: MONTH_START,
        NETWORKDAYS: NETWORKDAYS,
        NETWORKDAYS_INTL: NETWORKDAYS_INTL,
        NOW: NOW,
        QUARTER: QUARTER,
        QUARTER_END: QUARTER_END,
        QUARTER_START: QUARTER_START,
        SECOND: SECOND,
        TIME: TIME,
        TIMEVALUE: TIMEVALUE,
        TODAY: TODAY,
        WEEKDAY: WEEKDAY,
        WEEKNUM: WEEKNUM,
        WORKDAY: WORKDAY,
        WORKDAY_INTL: WORKDAY_INTL,
        YEAR: YEAR,
        YEARFRAC: YEARFRAC,
        YEAR_END: YEAR_END,
        YEAR_START: YEAR_START
    });

    const DEFAULT_DELTA_ARG = 0;
    // -----------------------------------------------------------------------------
    // DELTA
    // -----------------------------------------------------------------------------
    const DELTA = {
        description: _t("Compare two numeric values, returning 1 if they're equal."),
        args: [
            arg("number1 (number)", _t("The first number to compare.")),
            arg(`number2 (number, default=${DEFAULT_DELTA_ARG})`, _t("The second number to compare.")),
        ],
        returns: ["NUMBER"],
        compute: function (number1, number2 = DEFAULT_DELTA_ARG) {
            const _number1 = toNumber(number1, this.locale);
            const _number2 = toNumber(number2, this.locale);
            return _number1 === _number2 ? 1 : 0;
        },
        isExported: true,
    };

    var engineering = /*#__PURE__*/Object.freeze({
        __proto__: null,
        DELTA: DELTA
    });

    const SORT_TYPES = [
        CellValueType.number,
        CellValueType.error,
        CellValueType.text,
        CellValueType.boolean,
    ];
    function cellsSortingCriterion(sortingOrder) {
        const inverse = sortingOrder === "ascending" ? 1 : -1;
        return (left, right) => {
            if (left.type === CellValueType.empty) {
                return right.type === CellValueType.empty ? 0 : 1;
            }
            else if (right.type === CellValueType.empty) {
                return -1;
            }
            let typeOrder = SORT_TYPES.indexOf(left.type) - SORT_TYPES.indexOf(right.type);
            if (typeOrder === 0) {
                if (left.type === CellValueType.text || left.type === CellValueType.error) {
                    typeOrder = left.value.localeCompare(right.value);
                }
                else {
                    typeOrder = left.value - right.value;
                }
            }
            return inverse * typeOrder;
        };
    }
    function sortCells(cells, sortDirection, emptyCellAsZero) {
        const cellsWithIndex = cells.map((cell, index) => ({
            index,
            type: cell.type,
            value: cell.value,
        }));
        const cellsToSort = emptyCellAsZero
            ? cellsWithIndex.map((cell) => cell.type === CellValueType.empty ? { ...cell, type: CellValueType.number, value: 0 } : cell)
            : cellsWithIndex;
        return cellsToSort.sort(cellsSortingCriterion(sortDirection));
    }
    function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
        let result = DispatchResult.Success;
        //several columns => bypass the contiguity check
        let multiColumns = zone.right > zone.left;
        if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
            multiColumns = false;
            let table;
            for (let row = zone.top; row <= zone.bottom; row++) {
                table = [];
                for (let col = zone.left; col <= zone.right; col++) {
                    let merge = env.model.getters.getMerge({ sheetId, col, row });
                    if (merge && !table.includes(merge.id.toString())) {
                        table.push(merge.id.toString());
                    }
                }
                if (table.length >= 2) {
                    multiColumns = true;
                    break;
                }
            }
        }
        const { col, row } = anchor;
        if (multiColumns) {
            result = env.model.dispatch("SORT_CELLS", { sheetId, col, row, zone, sortDirection });
        }
        else {
            // check contiguity
            const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
            if (isEqual(contiguousZone, zone)) {
                // merge as it is
                result = env.model.dispatch("SORT_CELLS", {
                    sheetId,
                    col,
                    row,
                    zone,
                    sortDirection,
                });
            }
            else {
                env.askConfirmation(_t("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => {
                    zone = contiguousZone;
                    result = env.model.dispatch("SORT_CELLS", {
                        sheetId,
                        col,
                        row,
                        zone,
                        sortDirection,
                    });
                }, () => {
                    result = env.model.dispatch("SORT_CELLS", {
                        sheetId,
                        col,
                        row,
                        zone,
                        sortDirection,
                    });
                });
            }
        }
        if (result.isCancelledBecause("InvalidSortZone" /* CommandResult.InvalidSortZone */)) {
            const { col, row } = anchor;
            env.model.selection.selectZone({ cell: { col, row }, zone });
            env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
        }
    }

    function sortMatrix(matrix, locale, ...criteria) {
        for (const [i, value] of criteria.entries()) {
            assert(() => value !== undefined, _t("Value for parameter %d is missing, while the function [[FUNCTION_NAME]] expect a number or a range.", i + 1));
        }
        const sortingOrders = [];
        const sortColumns = [];
        const nRows = matrix.length;
        for (let i = 0; i < criteria.length; i += 2) {
            sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "ascending" : "descending");
            const sortColumn = criteria[i];
            if (isMatrix(sortColumn) && (sortColumn.length > 1 || sortColumn[0].length > 1)) {
                assert(() => sortColumn.length === 1 && sortColumn[0].length === nRows, _t("Wrong size for %s. Expected a range of size 1x%s. Got %sx%s.", `sort_column${i + 1}`, nRows, sortColumn.length, sortColumn[0].length));
                sortColumns.push(sortColumn.flat().map((c) => c.value));
            }
            else {
                const colIndex = toNumber(toScalar(sortColumn)?.value, locale);
                if (colIndex < 1 || colIndex > matrix[0].length) {
                    return matrix;
                }
                sortColumns.push(matrix.map((row) => row[colIndex - 1].value));
            }
        }
        if (sortColumns.length === 0) {
            for (let i = 0; i < matrix[0].length; i++) {
                sortColumns.push(matrix.map((row) => row[i].value));
                sortingOrders.push("ascending");
            }
        }
        const sortingCriteria = {
            descending: cellsSortingCriterion("descending"),
            ascending: cellsSortingCriterion("ascending"),
        };
        const indexes = range(0, matrix.length);
        indexes.sort((a, b) => {
            for (const [i, sortColumn] of sortColumns.entries()) {
                const left = sortColumn[a];
                const right = sortColumn[b];
                const leftCell = {
                    value: left,
                    type: left === null
                        ? CellValueType.empty
                        : typeof left === "string"
                            ? CellValueType.text
                            : typeof left,
                };
                const rightCell = {
                    value: right,
                    type: right === null
                        ? CellValueType.empty
                        : typeof right === "string"
                            ? CellValueType.text
                            : typeof right,
                };
                const result = sortingCriteria[sortingOrders[i]](leftCell, rightCell);
                if (result !== 0) {
                    return result;
                }
            }
            return 0;
        });
        return indexes.map((i) => matrix[i]);
    }
    // -----------------------------------------------------------------------------
    // FILTER
    // -----------------------------------------------------------------------------
    const FILTER = {
        description: _t("Returns a filtered version of the source range, returning only rows or columns that meet the specified conditions."),
        // TODO modify args description when vectorization on formulas is available
        args: [
            arg("range (any, range<any>)", _t("The data to be filtered.")),
            arg("condition1 (boolean, range<boolean>)", _t("A column or row containing true or false values corresponding to the first column or row of range.")),
            arg("condition2 (boolean, range<boolean>, repeating)", _t("Additional column or row containing true or false values.")),
        ],
        returns: ["RANGE<ANY>"],
        computeValueAndFormat: function (range, ...conditions) {
            let _array = toMatrix(range);
            const _conditionsMatrices = conditions.map((cond) => matrixMap(toMatrix(cond), (data) => data.value));
            _conditionsMatrices.map((c) => assertSingleColOrRow(_t("The arguments condition must be a single column or row."), c));
            assertSameDimensions(_t("The arguments conditions must have the same dimensions."), ..._conditionsMatrices);
            const _conditions = _conditionsMatrices.map((c) => c.flat());
            const mode = _conditionsMatrices[0].length === 1 ? "row" : "col";
            _array = mode === "row" ? transposeMatrix(_array) : _array;
            assert(() => _conditions.every((cond) => cond.length === _array.length), _t("FILTER has mismatched sizes on the range and conditions."));
            const result = [];
            for (let i = 0; i < _array.length; i++) {
                const row = _array[i];
                if (_conditions.every((c) => c[i])) {
                    result.push(row);
                }
            }
            if (!result.length) {
                throw new NotAvailableError(_t("No match found in FILTER evaluation"));
            }
            return mode === "row" ? transposeMatrix(result) : result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SORT
    // -----------------------------------------------------------------------------
    const SORT = {
        description: _t("Sorts the rows of a given array or range by the values in one or more columns."),
        args: [
            arg("range (range)", _t("The data to be sorted.")),
            arg("sort_column (any, range<number>, repeating)", _t("The index of the column in range or a range outside of range containing the values by which to sort.")),
            arg("is_ascending (boolean, repeating)", _t("TRUE or FALSE indicating whether to sort sort_column in ascending order. FALSE sorts in descending order.")),
        ],
        returns: ["RANGE"],
        computeValueAndFormat: function (range, ...sortingCriteria) {
            const _range = transposeMatrix(range);
            return transposeMatrix(sortMatrix(_range, this.locale, ...sortingCriteria));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SORTN
    // -----------------------------------------------------------------------------
    const SORTN = {
        description: _t("Returns the first n items in a data set after performing a sort."),
        args: [
            arg("range (range)", _t("The data to be sorted.")),
            arg("n (number, default=1)", _t("The number of items to return.")),
            arg("display_ties_mode (number, default=0)", _t("A number representing the way to display ties.")),
            arg("sort_column (number, range<number>, repeating)", _t("The index of the column in range or a range outside of range containing the values by which to sort.")),
            arg("is_ascending (boolean, repeating)", _t("TRUE or FALSE indicating whether to sort sort_column in ascending order. FALSE sorts in descending order.")),
        ],
        returns: ["RANGE"],
        computeValueAndFormat: function (range, n, displayTiesMode, ...sortingCriteria) {
            const _n = toNumber(n?.value ?? 1, this.locale);
            assert(() => _n >= 0, _t("Wrong value of 'n'. Expected a positive number. Got %s.", _n));
            const _displayTiesMode = toNumber(displayTiesMode?.value ?? 0, this.locale);
            assert(() => _displayTiesMode >= 0 && _displayTiesMode <= 3, _t("Wrong value of 'display_ties_mode'. Expected a positive number between 0 and 3. Got %s.", _displayTiesMode));
            const sortedData = sortMatrix(transposeMatrix(range), this.locale, ...sortingCriteria);
            const sameRows = (i, j) => JSON.stringify(sortedData[i].map((c) => c.value)) ===
                JSON.stringify(sortedData[j].map((c) => c.value));
            /*
             * displayTiesMode determine how ties (equal values) are dealt with:
             * 0 - ignore ties and show first n rows only
             * 1 - show first n rows plus any additional ties with nth row
             * 2 - show n rows but remove duplicates
             * 3 - show first n unique rows and all duplicates of these rows
             */
            switch (_displayTiesMode) {
                case 0:
                    return transposeMatrix(sortedData.slice(0, _n));
                case 1:
                    for (let i = _n; i < sortedData.length; i++) {
                        if (!sameRows(i, _n - 1)) {
                            return transposeMatrix(sortedData.slice(0, i));
                        }
                    }
                    return transposeMatrix(sortedData);
                case 2: {
                    const uniques = [sortedData[0]];
                    for (let i = 1; i < sortedData.length; i++) {
                        for (let j = 0; j < i; j++) {
                            if (sameRows(i, j)) {
                                break;
                            }
                            if (j === i - 1) {
                                uniques.push(sortedData[i]);
                            }
                        }
                    }
                    return transposeMatrix(uniques.slice(0, _n));
                }
                case 3: {
                    const uniques = [sortedData[0]];
                    let counter = 1;
                    for (let i = 1; i < sortedData.length; i++) {
                        if (!sameRows(i, i - 1)) {
                            counter++;
                        }
                        if (counter > _n) {
                            break;
                        }
                        uniques.push(sortedData[i]);
                    }
                    return transposeMatrix(uniques);
                }
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // UNIQUE
    // -----------------------------------------------------------------------------
    const UNIQUE = {
        description: _t("Unique rows in the provided source range."),
        args: [
            arg("range (any, range<any>)", _t("The data to filter by unique entries.")),
            arg("by_column (boolean, default=FALSE)", _t("Whether to filter the data by columns or by rows.")),
            arg("exactly_once (boolean, default=FALSE)", _t("Whether to return only entries with no duplicates.")),
        ],
        returns: ["RANGE<NUMBER>"],
        computeValueAndFormat: function (range = { value: "" }, byColumn, exactlyOnce) {
            if (!isMatrix(range)) {
                return [[range]];
            }
            const _byColumn = toBoolean(byColumn?.value) || false;
            const _exactlyOnce = toBoolean(exactlyOnce?.value) || false;
            if (!_byColumn) {
                range = transposeMatrix(range);
            }
            const map = new Map();
            for (const data of range) {
                const key = JSON.stringify(data.map((item) => item.value));
                const occurrence = map.get(key);
                if (!occurrence) {
                    map.set(key, { data, count: 1 });
                }
                else {
                    occurrence.count++;
                }
            }
            const result = [];
            for (const row of map.values()) {
                if (_exactlyOnce && row.count > 1) {
                    continue;
                }
                result.push(row.data);
            }
            if (!result.length)
                throw new Error(_t("No unique values found"));
            return _byColumn ? result : transposeMatrix(result);
        },
        isExported: true,
    };

    var filter = /*#__PURE__*/Object.freeze({
        __proto__: null,
        FILTER: FILTER,
        SORT: SORT,
        SORTN: SORTN,
        UNIQUE: UNIQUE
    });

    /** Assert maturity date > settlement date */
    function assertMaturityAndSettlementDatesAreValid(settlement, maturity) {
        assert(() => settlement < maturity, _t("The maturity (%s) must be strictly greater than the settlement (%s).", maturity.toString(), settlement.toString()));
    }
    /** Assert settlement date > issue date */
    function assertSettlementAndIssueDatesAreValid(settlement, issue) {
        assert(() => issue < settlement, _t("The settlement date (%s) must be strictly greater than the issue date (%s).", settlement.toString(), issue.toString()));
    }
    /** Assert coupon frequency is in [1, 2, 4] */
    function assertCouponFrequencyIsValid(frequency) {
        assert(() => [1, 2, 4].includes(frequency), _t("The frequency (%s) must be one of %s", frequency.toString(), [1, 2, 4].toString()));
    }
    /** Assert dayCountConvention is between 0 and 4 */
    function assertDayCountConventionIsValid(dayCountConvention) {
        assert(() => 0 <= dayCountConvention && dayCountConvention <= 4, _t("The day_count_convention (%s) must be between 0 and 4 inclusive.", dayCountConvention.toString()));
    }
    function assertRedemptionStrictlyPositive(redemption) {
        assert(() => redemption > 0, _t("The redemption (%s) must be strictly positive.", redemption.toString()));
    }
    function assertPriceStrictlyPositive(price) {
        assert(() => price > 0, _t("The price (%s) must be strictly positive.", price.toString()));
    }
    function assertNumberOfPeriodsStrictlyPositive(nPeriods) {
        assert(() => nPeriods > 0, _t("The number_of_periods (%s) must be greater than 0.", nPeriods.toString()));
    }
    function assertRateStrictlyPositive(rate) {
        assert(() => rate > 0, _t("The rate (%s) must be strictly positive.", rate.toString()));
    }
    function assertLifeStrictlyPositive(life) {
        assert(() => life > 0, _t("The life (%s) must be strictly positive.", life.toString()));
    }
    function assertCostStrictlyPositive(cost) {
        assert(() => cost > 0, _t("The cost (%s) must be strictly positive.", cost.toString()));
    }
    function assertCostPositiveOrZero(cost) {
        assert(() => cost >= 0, _t("The cost (%s) must be positive or null.", cost.toString()));
    }
    function assertPeriodStrictlyPositive(period) {
        assert(() => period > 0, _t("The period (%s) must be strictly positive.", period.toString()));
    }
    function assertPeriodPositiveOrZero(period) {
        assert(() => period >= 0, _t("The period (%s) must be positive or null.", period.toString()));
    }
    function assertSalvagePositiveOrZero(salvage) {
        assert(() => salvage >= 0, _t("The salvage (%s) must be positive or null.", salvage.toString()));
    }
    function assertSalvageSmallerOrEqualThanCost(salvage, cost) {
        assert(() => salvage <= cost, _t("The salvage (%s) must be smaller or equal than the cost (%s).", salvage.toString(), cost.toString()));
    }
    function assertPresentValueStrictlyPositive(pv) {
        assert(() => pv > 0, _t("The present value (%s) must be strictly positive.", pv.toString()));
    }
    function assertPeriodSmallerOrEqualToLife(period, life) {
        assert(() => period <= life, _t("The period (%s) must be less than or equal life (%s).", period.toString(), life.toString()));
    }
    function assertInvestmentStrictlyPositive(investment) {
        assert(() => investment > 0, _t("The investment (%s) must be strictly positive.", investment.toString()));
    }
    function assertDiscountStrictlyPositive(discount) {
        assert(() => discount > 0, _t("The discount (%s) must be strictly positive.", discount.toString()));
    }
    function assertDiscountStrictlySmallerThanOne(discount) {
        assert(() => discount < 1, _t("The discount (%s) must be smaller than 1.", discount.toString()));
    }
    function assertDeprecationFactorStrictlyPositive(factor) {
        assert(() => factor > 0, _t("The depreciation factor (%s) must be strictly positive.", factor.toString()));
    }
    function assertSettlementLessThanOneYearBeforeMaturity(settlement, maturity, locale) {
        const startDate = toJsDate(settlement, locale);
        const endDate = toJsDate(maturity, locale);
        const startDatePlusOneYear = toJsDate(settlement, locale);
        startDatePlusOneYear.setFullYear(startDate.getFullYear() + 1);
        assert(() => endDate.getTime() <= startDatePlusOneYear.getTime(), _t("The settlement date (%s) must at most one year after the maturity date (%s).", settlement.toString(), maturity.toString()));
    }
    /**
     * Check if the given periods are valid. This will assert :
     *
     * - 0 < numberOfPeriods
     * - 0 < firstPeriod <= lastPeriod
     * - 0 < lastPeriod <= numberOfPeriods
     *
     */
    function assertFirstAndLastPeriodsAreValid(firstPeriod, lastPeriod, numberOfPeriods) {
        assertNumberOfPeriodsStrictlyPositive(numberOfPeriods);
        assert(() => firstPeriod > 0, _t("The first_period (%s) must be strictly positive.", firstPeriod.toString()));
        assert(() => lastPeriod > 0, _t("The last_period (%s) must be strictly positive.", lastPeriod.toString()));
        assert(() => firstPeriod <= lastPeriod, _t("The first_period (%s) must be smaller or equal to the last_period (%s).", firstPeriod.toString(), lastPeriod.toString()));
        assert(() => lastPeriod <= numberOfPeriods, _t("The last_period (%s) must be smaller or equal to the number_of_periods (%s).", firstPeriod.toString(), numberOfPeriods.toString()));
    }
    /**
     * Check if the given periods are valid. This will assert :
     *
     * - 0 < life
     * - 0 <= startPeriod <= endPeriod
     * - 0 <= endPeriod <= life
     *
     */
    function assertStartAndEndPeriodAreValid(startPeriod, endPeriod, life) {
        assertLifeStrictlyPositive(life);
        assert(() => startPeriod >= 0, _t("The start_period (%s) must be greater or equal than 0.", startPeriod.toString()));
        assert(() => endPeriod >= 0, _t("The end_period (%s) must be greater or equal than 0.", endPeriod.toString()));
        assert(() => startPeriod <= endPeriod, _t("The start_period (%s) must be smaller or equal to the end_period (%s).", startPeriod.toString(), endPeriod.toString()));
        assert(() => endPeriod <= life, _t("The end_period (%s) must be smaller or equal to the life (%s).", startPeriod.toString(), life.toString()));
    }
    function assertRateGuessStrictlyGreaterThanMinusOne(guess) {
        assert(() => guess > -1, _t("The rate_guess (%s) must be strictly greater than -1.", guess.toString()));
    }
    function assertCashFlowsAndDatesHaveSameDimension(cashFlows, dates) {
        assert(() => cashFlows.length === dates.length && cashFlows[0].length === dates[0].length, _t("The cashflow_amounts and cashflow_dates ranges must have the same dimensions."));
    }
    function assertCashFlowsHavePositiveAndNegativesValues(cashFlow) {
        assert(() => cashFlow.some((val) => val > 0) && cashFlow.some((val) => val < 0), _t("There must be both positive and negative values in cashflow_amounts."));
    }
    function assertEveryDateGreaterThanFirstDateOfCashFlowDates(dates) {
        assert(() => dates.every((date) => date >= dates[0]), _t("All the dates should be greater or equal to the first date in cashflow_dates (%s).", dates[0].toString()));
    }

    const DEFAULT_DAY_COUNT_CONVENTION = 0;
    const DEFAULT_END_OR_BEGINNING = 0;
    const DEFAULT_FUTURE_VALUE = 0;
    const COUPON_FUNCTION_ARGS = [
        arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
        arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
        arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")),
        arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
    ];
    /**
     * Use the Newton–Raphson method to find a root of the given function in an iterative manner.
     *
     * @param func the function to find a root of
     * @param derivFunc the derivative of the function
     * @param startValue the initial value for the first iteration of the algorithm
     * @param maxIterations the maximum number of iterations
     * @param epsMax the epsilon for the root
     * @param nanFallback a function giving a fallback value to use if func(x) returns NaN. Useful if the
     *                       function is not defined for some range, but we know approximately where the root is when the Newton
     *                       algorithm ends up in this range.
     */
    function newtonMethod(func, derivFunc, startValue, maxIterations, epsMax = 1e-10, nanFallback) {
        let x = startValue;
        let newX;
        let xDelta;
        let y;
        let yEqual0 = false;
        let count = 0;
        let previousFallback = undefined;
        do {
            y = func(x);
            if (isNaN(y)) {
                assert(() => count < maxIterations && nanFallback !== undefined, _t("Function [[FUNCTION_NAME]] didn't find any result."));
                count++;
                x = nanFallback(previousFallback);
                previousFallback = x;
                continue;
            }
            newX = x - y / derivFunc(x);
            xDelta = Math.abs(newX - x);
            x = newX;
            yEqual0 = xDelta < epsMax || Math.abs(y) < epsMax;
            assert(() => count < maxIterations, _t("Function [[FUNCTION_NAME]] didn't find any result."));
            count++;
        } while (!yEqual0);
        return x;
    }
    // -----------------------------------------------------------------------------
    // ACCRINTM
    // -----------------------------------------------------------------------------
    const ACCRINTM = {
        description: _t("Accrued interest of security paying at maturity."),
        args: [
            arg("issue (date)", _t("The date the security was initially issued.")),
            arg("maturity (date)", _t("The maturity date of the security.")),
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (issue, maturity, rate, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(issue, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _redemption = toNumber(redemption, this.locale);
            const _rate = toNumber(rate, this.locale);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertSettlementAndIssueDatesAreValid(end, start);
            assertDayCountConventionIsValid(_dayCountConvention);
            assertRedemptionStrictlyPositive(_redemption);
            assertRateStrictlyPositive(_rate);
            const yearFrac = YEARFRAC.compute.bind(this)(start, end, dayCountConvention);
            return _redemption * _rate * yearFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // AMORLINC
    // -----------------------------------------------------------------------------
    const AMORLINC = {
        description: _t("Depreciation for an accounting period."),
        args: [
            arg("cost (number)", _t("The initial cost of the asset.")),
            arg("purchase_date (date)", _t("The date the asset was purchased.")),
            arg("first_period_end (date)", _t("The date the first period ended.")),
            arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
            arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
            arg("rate (number)", _t("The deprecation rate.")),
            arg("day_count_convention (number, optional)", _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (cost, purchaseDate, firstPeriodEnd, salvage, period, rate, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _cost = toNumber(cost, this.locale);
            const _purchaseDate = Math.trunc(toNumber(purchaseDate, this.locale));
            const _firstPeriodEnd = Math.trunc(toNumber(firstPeriodEnd, this.locale));
            const _salvage = toNumber(salvage, this.locale);
            const _period = toNumber(period, this.locale);
            const _rate = toNumber(rate, this.locale);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertCostStrictlyPositive(_cost);
            assertSalvagePositiveOrZero(_salvage);
            assertSalvageSmallerOrEqualThanCost(_salvage, _cost);
            assertPeriodPositiveOrZero(_period);
            assertRateStrictlyPositive(_rate);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _purchaseDate <= _firstPeriodEnd, _t("The purchase_date (%s) must be before the first_period_end (%s).", _purchaseDate.toString(), _firstPeriodEnd.toString()));
            /**
             * https://wiki.documentfoundation.org/Documentation/Calc_Functions/AMORLINC
             *
             * AMORLINC period 0 = cost * rate * YEARFRAC(purchase date, first period end)
             * AMORLINC period n = cost * rate
             * AMORLINC at the last period is such that the remaining deprecated cost is equal to the salvage value.
             *
             * The period is and rounded to 1 if < 1 truncated if > 1,
             *
             * Compatibility note :
             * If (purchase date) === (first period end), on GSheet the deprecation at the first period is 0, and on Excel
             * it is a full period deprecation. We choose to use the Excel behaviour.
             */
            const roundedPeriod = _period < 1 && _period > 0 ? 1 : Math.trunc(_period);
            const deprec = _cost * _rate;
            const yearFrac = YEARFRAC.compute.bind(this)(_purchaseDate, _firstPeriodEnd, _dayCountConvention);
            const firstDeprec = _purchaseDate === _firstPeriodEnd ? deprec : deprec * yearFrac;
            const valueAtPeriod = _cost - firstDeprec - deprec * roundedPeriod;
            if (valueAtPeriod >= _salvage) {
                return roundedPeriod === 0 ? firstDeprec : deprec;
            }
            return _salvage - valueAtPeriod < deprec ? deprec - (_salvage - valueAtPeriod) : 0;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPDAYS
    // -----------------------------------------------------------------------------
    const COUPDAYS = {
        description: _t("Days in coupon period containing settlement date."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            // https://wiki.documentfoundation.org/Documentation/Calc_Functions/COUPDAYS
            if (_dayCountConvention === 1) {
                const before = COUPPCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
                const after = COUPNCD.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
                return after - before;
            }
            const daysInYear = _dayCountConvention === 3 ? 365 : 360;
            return daysInYear / _frequency;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPDAYBS
    // -----------------------------------------------------------------------------
    const COUPDAYBS = {
        description: _t("Days from settlement until next coupon."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const couponBeforeStart = COUPPCD.compute.bind(this)(start, end, frequency, dayCountConvention);
            if ([1, 2, 3].includes(_dayCountConvention)) {
                return start - couponBeforeStart;
            }
            if (_dayCountConvention === 4) {
                const yearFrac = getYearFrac(couponBeforeStart, start, _dayCountConvention);
                return Math.round(yearFrac * 360);
            }
            const startDate = toJsDate(start, this.locale);
            const dateCouponBeforeStart = toJsDate(couponBeforeStart, this.locale);
            const y1 = dateCouponBeforeStart.getFullYear();
            const y2 = startDate.getFullYear();
            const m1 = dateCouponBeforeStart.getMonth() + 1; // +1 because months in js start at 0 and it's confusing
            const m2 = startDate.getMonth() + 1;
            let d1 = dateCouponBeforeStart.getDate();
            let d2 = startDate.getDate();
            /**
             * Rules based on https://en.wikipedia.org/wiki/Day_count_convention#30/360_US
             *
             * These are slightly modified (no mention of if investment is EOM and rules order is modified),
             * but from my testing this seems the rules used by Excel/GSheet.
             */
            if (m1 === 2 &&
                m2 === 2 &&
                isLastDayOfMonth(dateCouponBeforeStart) &&
                isLastDayOfMonth(startDate)) {
                d2 = 30;
            }
            if (d2 === 31 && (d1 === 30 || d1 === 31)) {
                d2 = 30;
            }
            if (m1 === 2 && isLastDayOfMonth(dateCouponBeforeStart)) {
                d1 = 30;
            }
            if (d1 === 31) {
                d1 = 30;
            }
            return (y2 - y1) * 360 + (m2 - m1) * 30 + (d2 - d1);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPDAYSNC
    // -----------------------------------------------------------------------------
    const COUPDAYSNC = {
        description: _t("Days from settlement until next coupon."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const couponAfterStart = COUPNCD.compute.bind(this)(start, end, frequency, dayCountConvention);
            if ([1, 2, 3].includes(_dayCountConvention)) {
                return couponAfterStart - start;
            }
            if (_dayCountConvention === 4) {
                const yearFrac = getYearFrac(start, couponAfterStart, _dayCountConvention);
                return Math.round(yearFrac * 360);
            }
            const coupDayBs = COUPDAYBS.compute.bind(this)(settlement, maturity, frequency, _dayCountConvention);
            const coupDays = COUPDAYS.compute.bind(this)(settlement, maturity, frequency, _dayCountConvention);
            return coupDays - coupDayBs;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPNCD
    // -----------------------------------------------------------------------------
    const COUPNCD = {
        description: _t("Next coupon date after the settlement date."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const monthsPerPeriod = 12 / _frequency;
            const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
            const date = addMonthsToDate(toJsDate(end, this.locale), -(coupNum - 1) * monthsPerPeriod, true);
            return jsDateToRoundNumber(date);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPNUM
    // -----------------------------------------------------------------------------
    const COUPNUM = {
        description: _t("Number of coupons between settlement and maturity."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            let num = 1;
            let currentDate = end;
            const monthsPerPeriod = 12 / _frequency;
            while (currentDate > start) {
                currentDate = jsDateToRoundNumber(addMonthsToDate(toJsDate(currentDate, this.locale), -monthsPerPeriod, false));
                num++;
            }
            return num - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COUPPCD
    // -----------------------------------------------------------------------------
    const COUPPCD = {
        description: _t("Last coupon date prior to or on the settlement date."),
        args: COUPON_FUNCTION_ARGS,
        returns: ["NUMBER"],
        computeFormat: function () {
            return this.locale.dateFormat;
        },
        compute: function (settlement, maturity, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            const monthsPerPeriod = 12 / _frequency;
            const coupNum = COUPNUM.compute.bind(this)(settlement, maturity, frequency, dayCountConvention);
            const date = addMonthsToDate(toJsDate(end, this.locale), -coupNum * monthsPerPeriod, true);
            return jsDateToRoundNumber(date);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CUMIPMT
    // -----------------------------------------------------------------------------
    const CUMIPMT = {
        description: _t("Cumulative interest paid over a set of periods."),
        args: [
            arg("rate (number)", _t("The interest rate.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
            arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")),
            arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            const first = toNumber(firstPeriod, this.locale);
            const last = toNumber(lastPeriod, this.locale);
            const _rate = toNumber(rate, this.locale);
            const pv = toNumber(presentValue, this.locale);
            const nOfPeriods = toNumber(numberOfPeriods, this.locale);
            assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods);
            assertRateStrictlyPositive(_rate);
            assertPresentValueStrictlyPositive(pv);
            let cumSum = 0;
            for (let i = first; i <= last; i++) {
                const impt = IPMT.compute.bind(this)(rate, i, nOfPeriods, presentValue, 0, endOrBeginning);
                cumSum += impt;
            }
            return cumSum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CUMPRINC
    // -----------------------------------------------------------------------------
    const CUMPRINC = {
        description: _t("Cumulative principal paid over a set of periods."),
        args: [
            arg("rate (number)", _t("The interest rate.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
            arg("first_period (number)", _t("The number of the payment period to begin the cumulative calculation.")),
            arg("last_period (number)", _t("The number of the payment period to end the cumulative calculation.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        compute: function (rate, numberOfPeriods, presentValue, firstPeriod, lastPeriod, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            const first = toNumber(firstPeriod, this.locale);
            const last = toNumber(lastPeriod, this.locale);
            const _rate = toNumber(rate, this.locale);
            const pv = toNumber(presentValue, this.locale);
            const nOfPeriods = toNumber(numberOfPeriods, this.locale);
            assertFirstAndLastPeriodsAreValid(first, last, nOfPeriods);
            assertRateStrictlyPositive(_rate);
            assertPresentValueStrictlyPositive(pv);
            let cumSum = 0;
            for (let i = first; i <= last; i++) {
                const ppmt = PPMT.compute.bind(this)(rate, i, nOfPeriods, presentValue, 0, endOrBeginning);
                cumSum += ppmt;
            }
            return cumSum;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DB
    // -----------------------------------------------------------------------------
    const DB = {
        description: _t("Depreciation via declining balance method."),
        args: [
            arg("cost (number)", _t("The initial cost of the asset.")),
            arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
            arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
            arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
            arg("month (number, optional)", _t("The number of months in the first year of depreciation.")),
        ],
        returns: ["NUMBER"],
        // to do: replace by dollar format
        computeFormat: () => "#,##0.00",
        compute: function (cost, salvage, life, period, ...args) {
            const _cost = toNumber(cost, this.locale);
            const _salvage = toNumber(salvage, this.locale);
            const _life = toNumber(life, this.locale);
            const _period = Math.trunc(toNumber(period, this.locale));
            const _month = args.length ? Math.trunc(toNumber(args[0], this.locale)) : 12;
            const lifeLimit = _life + (_month === 12 ? 0 : 1);
            assertCostPositiveOrZero(_cost);
            assertSalvagePositiveOrZero(_salvage);
            assertPeriodStrictlyPositive(_period);
            assertLifeStrictlyPositive(_life);
            assert(() => 1 <= _month && _month <= 12, _t("The month (%s) must be between 1 and 12 inclusive.", _month.toString()));
            assert(() => _period <= lifeLimit, _t("The period (%s) must be less than or equal to %s.", _period.toString(), lifeLimit.toString()));
            const monthPart = _month / 12;
            let rate = 1 - Math.pow(_salvage / _cost, 1 / _life);
            // round to 3 decimal places
            rate = Math.round(rate * 1000) / 1000;
            let before = _cost;
            let after = _cost * (1 - rate * monthPart);
            for (let i = 1; i < _period; i++) {
                before = after;
                after = before * (1 - rate);
                if (i === _life) {
                    after = before * (1 - rate * (1 - monthPart));
                }
            }
            return before - after;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DDB
    // -----------------------------------------------------------------------------
    const DEFAULT_DDB_DEPRECIATION_FACTOR = 2;
    const DDB = {
        description: _t("Depreciation via double-declining balance method."),
        args: [
            arg("cost (number)", _t("The initial cost of the asset.")),
            arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
            arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
            arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
            arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The factor by which depreciation decreases.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "#,##0.00",
        compute: function (cost, salvage, life, period, factor = DEFAULT_DDB_DEPRECIATION_FACTOR) {
            factor = factor || 0;
            const _cost = toNumber(cost, this.locale);
            const _salvage = toNumber(salvage, this.locale);
            const _life = toNumber(life, this.locale);
            const _period = toNumber(period, this.locale);
            const _factor = toNumber(factor, this.locale);
            assertCostPositiveOrZero(_cost);
            assertSalvagePositiveOrZero(_salvage);
            assertPeriodStrictlyPositive(_period);
            assertLifeStrictlyPositive(_life);
            assertPeriodSmallerOrEqualToLife(_period, _life);
            assertDeprecationFactorStrictlyPositive(_factor);
            if (_cost === 0 || _salvage >= _cost)
                return 0;
            const deprecFactor = _factor / _life;
            if (deprecFactor > 1) {
                return period === 1 ? _cost - _salvage : 0;
            }
            if (_period <= 1) {
                return _cost * deprecFactor;
            }
            const previousCost = _cost * Math.pow(1 - deprecFactor, _period - 1);
            const nextCost = _cost * Math.pow(1 - deprecFactor, _period);
            const deprec = nextCost < _salvage ? previousCost - _salvage : previousCost - nextCost;
            return Math.max(deprec, 0);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DISC
    // -----------------------------------------------------------------------------
    const DISC = {
        description: _t("Discount rate of a security based on price."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
            arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, price, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _price = toNumber(price, this.locale);
            const _redemption = toNumber(redemption, this.locale);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCountConvention);
            assertPriceStrictlyPositive(_price);
            assertRedemptionStrictlyPositive(_redemption);
            /**
             * https://support.microsoft.com/en-us/office/disc-function-71fce9f3-3f05-4acf-a5a3-eac6ef4daa53
             *
             * B = number of days in year, depending on year basis
             * DSM = number of days from settlement to maturity
             *
             *        redemption - price          B
             * DISC = ____________________  *    ____
             *            redemption             DSM
             */
            const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention);
            return (_redemption - _price) / _redemption / yearsFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DOLLARDE
    // -----------------------------------------------------------------------------
    const DOLLARDE = {
        description: _t("Convert a decimal fraction to decimal value."),
        args: [
            arg("fractional_price (number)", _t("The price quotation given using fractional decimal conventions.")),
            arg("unit (number)", _t("The units of the fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")),
        ],
        returns: ["NUMBER"],
        compute: function (fractionalPrice, unit) {
            const price = toNumber(fractionalPrice, this.locale);
            const _unit = Math.trunc(toNumber(unit, this.locale));
            assert(() => _unit > 0, _t("The unit (%s) must be strictly positive.", _unit.toString()));
            const truncatedPrice = Math.trunc(price);
            const priceFractionalPart = price - truncatedPrice;
            const frac = 10 ** Math.ceil(Math.log10(_unit)) / _unit;
            return truncatedPrice + priceFractionalPart * frac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DOLLARFR
    // -----------------------------------------------------------------------------
    const DOLLARFR = {
        description: _t("Convert a decimal value to decimal fraction."),
        args: [
            arg("decimal_price (number)", _t("The price quotation given as a decimal value.")),
            arg("unit (number)", _t("The units of the desired fraction, e.g. 8 for 1/8ths or 32 for 1/32nds.")),
        ],
        returns: ["NUMBER"],
        compute: function (decimalPrice, unit) {
            const price = toNumber(decimalPrice, this.locale);
            const _unit = Math.trunc(toNumber(unit, this.locale));
            assert(() => _unit > 0, _t("The unit (%s) must be strictly positive.", _unit.toString()));
            const truncatedPrice = Math.trunc(price);
            const priceFractionalPart = price - truncatedPrice;
            const frac = _unit / 10 ** Math.ceil(Math.log10(_unit));
            return truncatedPrice + priceFractionalPart * frac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DURATION
    // -----------------------------------------------------------------------------
    const DURATION = {
        description: _t("Number of periods for an investment to reach a value."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("yield (number)", _t("The expected annual yield of the security.")),
            arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const _rate = toNumber(rate, this.locale);
            const _yield = toNumber(securityYield, this.locale);
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString()));
            assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString()));
            const years = YEARFRAC.compute.bind(this)(start, end, _dayCountConvention);
            const timeFirstYear = years - Math.trunc(years) || 1 / _frequency;
            const nbrCoupons = Math.ceil(years * _frequency);
            // The DURATION function return the Macaulay duration
            // See example: https://en.wikipedia.org/wiki/Bond_duration#Formulas
            const cashFlowFromCoupon = _rate / _frequency;
            const yieldPerPeriod = _yield / _frequency;
            let count = 0;
            let sum = 0;
            for (let i = 1; i <= nbrCoupons; i++) {
                const cashFlowPerPeriod = cashFlowFromCoupon + (i === nbrCoupons ? 1 : 0);
                const presentValuePerPeriod = cashFlowPerPeriod / (1 + yieldPerPeriod) ** i;
                sum += (timeFirstYear + (i - 1) / _frequency) * presentValuePerPeriod;
                count += presentValuePerPeriod;
            }
            return count === 0 ? 0 : sum / count;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EFFECT
    // -----------------------------------------------------------------------------
    const EFFECT = {
        description: _t("Annual effective interest rate."),
        args: [
            arg("nominal_rate (number)", _t("The nominal interest rate per year.")),
            arg("periods_per_year (number)", _t("The number of compounding periods per year.")),
        ],
        returns: ["NUMBER"],
        compute: function (nominal_rate, periods_per_year) {
            const nominal = toNumber(nominal_rate, this.locale);
            const periods = Math.trunc(toNumber(periods_per_year, this.locale));
            assert(() => nominal > 0, _t("The nominal rate (%s) must be strictly greater than 0.", nominal.toString()));
            assert(() => periods > 0, _t("The number of periods by year (%s) must strictly greater than 0.", periods.toString()));
            // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate
            return Math.pow(1 + nominal / periods, periods) - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FV
    // -----------------------------------------------------------------------------
    const DEFAULT_PRESENT_VALUE = 0;
    const FV = {
        description: _t("Future value of an annuity investment."),
        args: [
            arg("rate (number)", _t("The interest rate.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("payment_amount (number)", _t("The amount per period to be paid.")),
            arg(`present_value (number, default=${DEFAULT_PRESENT_VALUE})`, _t("The current value of the annuity.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        // to do: replace by dollar format
        computeFormat: () => "#,##0.00",
        compute: function (rate, numberOfPeriods, paymentAmount, presentValue = DEFAULT_PRESENT_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            presentValue = presentValue || 0;
            endOrBeginning = endOrBeginning || 0;
            const r = toNumber(rate, this.locale);
            const n = toNumber(numberOfPeriods, this.locale);
            const p = toNumber(paymentAmount, this.locale);
            const pv = toNumber(presentValue, this.locale);
            const type = toBoolean(endOrBeginning) ? 1 : 0;
            return r ? -pv * (1 + r) ** n - (p * (1 + r * type) * ((1 + r) ** n - 1)) / r : -(pv + p * n);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FVSCHEDULE
    // -----------------------------------------------------------------------------
    const FVSCHEDULE = {
        description: _t("Future value of principal from series of rates."),
        args: [
            arg("principal (number)", _t("The amount of initial capital or value to compound against.")),
            arg("rate_schedule (number, range<number>)", _t("A series of interest rates to compound against the principal.")),
        ],
        returns: ["NUMBER"],
        compute: function (principalAmount, rateSchedule) {
            const principal = toNumber(principalAmount, this.locale);
            return reduceAny([rateSchedule], (acc, rate) => acc * (1 + toNumber(rate, this.locale)), principal);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // INTRATE
    // -----------------------------------------------------------------------------
    const INTRATE = {
        description: _t("Calculates effective interest rate."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("investment (number)", _t("The amount invested in the security.")),
            arg("redemption (number)", _t("The amount to be received at maturity.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, investment, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _redemption = toNumber(redemption, this.locale);
            const _investment = toNumber(investment, this.locale);
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertInvestmentStrictlyPositive(_investment);
            assertRedemptionStrictlyPositive(_redemption);
            /**
             * https://wiki.documentfoundation.org/Documentation/Calc_Functions/INTRATE
             *
             *             (Redemption  - Investment) / Investment
             * INTRATE =  _________________________________________
             *              YEARFRAC(settlement, maturity, basis)
             */
            const yearFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, dayCountConvention);
            return (_redemption - _investment) / _investment / yearFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IPMT
    // -----------------------------------------------------------------------------
    const IPMT = {
        description: _t("Payment on the principal of an investment."),
        args: [
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("period (number)", _t("The amortization period, in terms of number of periods.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
            arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "#,##0.00",
        compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            const payment = PMT.compute.bind(this)(rate, numberOfPeriods, presentValue, futureValue, endOrBeginning);
            const ppmt = PPMT.compute.bind(this)(rate, currentPeriod, numberOfPeriods, presentValue, futureValue, endOrBeginning);
            return payment - ppmt;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IRR
    // -----------------------------------------------------------------------------
    const DEFAULT_RATE_GUESS = 0.1;
    const IRR = {
        description: _t("Internal rate of return given periodic cashflows."),
        args: [
            arg("cashflow_amounts (number, range<number>)", _t("An array or range containing the income or payments associated with the investment.")),
            arg(`rate_guess (number, default=${DEFAULT_RATE_GUESS})`, _t("An estimate for what the internal rate of return will be.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "0%",
        compute: function (cashFlowAmounts, rateGuess = DEFAULT_RATE_GUESS) {
            const _rateGuess = toNumber(rateGuess, this.locale);
            assertRateGuessStrictlyGreaterThanMinusOne(_rateGuess);
            // check that values contains at least one positive value and one negative value
            // and extract number present in the cashFlowAmount argument
            let positive = false;
            let negative = false;
            let amounts = [];
            visitNumbers([cashFlowAmounts], (amount) => {
                if (amount > 0)
                    positive = true;
                if (amount < 0)
                    negative = true;
                amounts.push(amount);
            }, this.locale);
            assert(() => positive && negative, _t("The cashflow_amounts must include negative and positive values."));
            const firstAmount = amounts.shift();
            // The result of IRR is the rate at which the NPV() function will return zero with the given values.
            // This algorithm uses the Newton's method on the NPV function to determine the result
            // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method
            // As the NPV function isn't continuous, we apply the Newton's method on the numerator of the NPV formula.
            function npvNumerator(rate, startValue, values) {
                const nbrValue = values.length;
                let i = 0;
                return values.reduce((acc, v) => {
                    i++;
                    return acc + v * rate ** (nbrValue - i);
                }, startValue * rate ** nbrValue);
            }
            function npvNumeratorDeriv(rate, startValue, values) {
                const nbrValue = values.length;
                let i = 0;
                return values.reduce((acc, v) => {
                    i++;
                    return acc + v * (nbrValue - i) * rate ** (nbrValue - i - 1);
                }, startValue * nbrValue * rate ** (nbrValue - 1));
            }
            function func(x) {
                return npvNumerator(x, firstAmount, amounts);
            }
            function derivFunc(x) {
                return npvNumeratorDeriv(x, firstAmount, amounts);
            }
            return newtonMethod(func, derivFunc, _rateGuess + 1, 20, 1e-5) - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISPMT
    // -----------------------------------------------------------------------------
    const ISPMT = {
        description: _t("Returns the interest paid at a particular period of an investment."),
        args: [
            arg("rate (number)", _t("The interest rate.")),
            arg("period (number)", _t("The period for which you want to view the interest payment.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
        ],
        returns: ["NUMBER"],
        compute: function (rate, currentPeriod, numberOfPeriods, presentValue) {
            const interestRate = toNumber(rate, this.locale);
            const period = toNumber(currentPeriod, this.locale);
            const nOfPeriods = toNumber(numberOfPeriods, this.locale);
            const investment = toNumber(presentValue, this.locale);
            assert(() => nOfPeriods !== 0, _t("The number of periods must be different than 0.", nOfPeriods.toString()));
            const currentInvestment = investment - investment * (period / nOfPeriods);
            return -1 * currentInvestment * interestRate;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MDURATION
    // -----------------------------------------------------------------------------
    const MDURATION = {
        description: _t("Modified Macaulay duration."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("yield (number)", _t("The expected annual yield of the security.")),
            arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, rate, securityYield, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            const duration = DURATION.compute.bind(this)(settlement, maturity, rate, securityYield, frequency, dayCountConvention);
            const y = toNumber(securityYield, this.locale);
            const k = Math.trunc(toNumber(frequency, this.locale));
            return duration / (1 + y / k);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MIRR
    // -----------------------------------------------------------------------------
    const MIRR = {
        description: _t("Modified internal rate of return."),
        args: [
            arg("cashflow_amounts (range<number>)", _t("A range containing the income or payments associated with the investment. The array should contain bot payments and incomes.")),
            arg("financing_rate (number)", _t("The interest rate paid on funds invested.")),
            arg("reinvestment_return_rate (number)", _t("The return (as a percentage) earned on reinvestment of income received from the investment.")),
        ],
        returns: ["NUMBER"],
        compute: function (cashflowAmount, financingRate, reinvestmentRate) {
            const fRate = toNumber(financingRate, this.locale);
            const rRate = toNumber(reinvestmentRate, this.locale);
            const cashFlow = transposeMatrix(cashflowAmount)
                .flat()
                .filter((t) => t !== null)
                .map((val) => toNumber(val, this.locale));
            const n = cashFlow.length;
            /**
             * https://en.wikipedia.org/wiki/Modified_internal_rate_of_return
             *
             *         /  FV(positive cash flows, reinvestment rate) \  ^ (1 / (n - 1))
             * MIRR = |  ___________________________________________  |                 - 1
             *         \   - PV(negative cash flows, finance rate)   /
             *
             * with n the number of cash flows.
             *
             * You can compute FV and PV as :
             *
             * FV =    SUM      [ (cashFlow[i]>0 ? cashFlow[i] : 0) * (1 + rRate)**(n - i-1) ]
             *       i= 0 => n
             *
             * PV =    SUM      [ (cashFlow[i]<0 ? cashFlow[i] : 0) / (1 + fRate)**i ]
             *       i= 0 => n
             */
            let fv = 0;
            let pv = 0;
            for (const i of range(0, n)) {
                const amount = cashFlow[i];
                if (amount >= 0) {
                    fv += amount * (rRate + 1) ** (n - i - 1);
                }
                else {
                    pv += amount / (fRate + 1) ** i;
                }
            }
            assert(() => pv !== 0 && fv !== 0, _t("There must be both positive and negative values in cashflow_amounts."));
            const exponent = 1 / (n - 1);
            return (-fv / pv) ** exponent - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NOMINAL
    // -----------------------------------------------------------------------------
    const NOMINAL = {
        description: _t("Annual nominal interest rate."),
        args: [
            arg("effective_rate (number)", _t("The effective interest rate per year.")),
            arg("periods_per_year (number)", _t("The number of compounding periods per year.")),
        ],
        returns: ["NUMBER"],
        compute: function (effective_rate, periods_per_year) {
            const effective = toNumber(effective_rate, this.locale);
            const periods = Math.trunc(toNumber(periods_per_year, this.locale));
            assert(() => effective > 0, _t("The effective rate (%s) must must strictly greater than 0.", effective.toString()));
            assert(() => periods > 0, _t("The number of periods by year (%s) must strictly greater than 0.", periods.toString()));
            // https://en.wikipedia.org/wiki/Nominal_interest_rate#Nominal_versus_effective_interest_rate
            return (Math.pow(effective + 1, 1 / periods) - 1) * periods;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NPER
    // -----------------------------------------------------------------------------
    const NPER = {
        description: _t("Number of payment periods for an investment."),
        args: [
            arg("rate (number)", _t("The interest rate.")),
            arg("payment_amount (number)", _t("The amount of each payment made.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
            arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        compute: function (rate, paymentAmount, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            futureValue = futureValue || 0;
            endOrBeginning = endOrBeginning || 0;
            const r = toNumber(rate, this.locale);
            const p = toNumber(paymentAmount, this.locale);
            const pv = toNumber(presentValue, this.locale);
            const fv = toNumber(futureValue, this.locale);
            const t = toBoolean(endOrBeginning) ? 1 : 0;
            /**
             * https://wiki.documentfoundation.org/Documentation/Calc_Functions/NPER
             *
             * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r
             *
             * We solve the equation for N:
             *
             * with C = [ p * (1 + r * t)] / r and
             *      R = 1 + r
             *
             * => 0 = pv * R^N + C * R^N - C + fv
             * <=> (C - fv) = R^N * (pv + C)
             * <=> log[(C - fv) / (pv + C)] = N * log(R)
             */
            if (r === 0) {
                return -(fv + pv) / p;
            }
            const c = (p * (1 + r * t)) / r;
            return Math.log((c - fv) / (pv + c)) / Math.log(1 + r);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NPV
    // -----------------------------------------------------------------------------
    function npvResult(r, startValue, values, locale) {
        let i = 0;
        return reduceNumbers(values, (acc, v) => {
            i++;
            return acc + v / (1 + r) ** i;
        }, startValue, locale);
    }
    const NPV = {
        description: _t("The net present value of an investment based on a series of periodic cash flows and a discount rate."),
        args: [
            arg("discount (number)", _t("The discount rate of the investment over one period.")),
            arg("cashflow1 (number, range<number>)", _t("The first future cash flow.")),
            arg("cashflow2 (number, range<number>, repeating)", _t("Additional future cash flows.")),
        ],
        returns: ["NUMBER"],
        // to do: replace by dollar format
        computeFormat: () => "#,##0.00",
        compute: function (discount, ...values) {
            const _discount = toNumber(discount, this.locale);
            assert(() => _discount !== -1, _t("The discount (%s) must be different from -1.", _discount.toString()));
            return npvResult(_discount, 0, values, this.locale);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PDURATION
    // -----------------------------------------------------------------------------
    const PDURATION = {
        description: _t("Computes the number of periods needed for an investment to reach a value."),
        args: [
            arg("rate (number)", _t("The rate at which the investment grows each period.")),
            arg("present_value (number)", _t("The investment's current value.")),
            arg("future_value (number)", _t("The investment's desired future value.")),
        ],
        returns: ["NUMBER"],
        compute: function (rate, presentValue, futureValue) {
            const _rate = toNumber(rate, this.locale);
            const _presentValue = toNumber(presentValue, this.locale);
            const _futureValue = toNumber(futureValue, this.locale);
            assertRateStrictlyPositive(_rate);
            assert(() => _presentValue > 0, _t("The present_value (%s) must be strictly positive.", _presentValue.toString()));
            assert(() => _futureValue > 0, _t("The future_value (%s) must be strictly positive.", _futureValue.toString()));
            return (Math.log(_futureValue) - Math.log(_presentValue)) / Math.log(1 + _rate);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PMT
    // -----------------------------------------------------------------------------
    const PMT = {
        description: _t("Periodic payment for an annuity investment."),
        args: [
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
            arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "#,##0.00",
        compute: function (rate, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            futureValue = futureValue || 0;
            endOrBeginning = endOrBeginning || 0;
            const n = toNumber(numberOfPeriods, this.locale);
            const r = toNumber(rate, this.locale);
            const t = toBoolean(endOrBeginning) ? 1 : 0;
            let fv = toNumber(futureValue, this.locale);
            let pv = toNumber(presentValue, this.locale);
            assertNumberOfPeriodsStrictlyPositive(n);
            /**
             * https://wiki.documentfoundation.org/Documentation/Calc_Functions/PMT
             *
             * 0 = pv * (1 + r)^N + fv + [ p * (1 + r * t) * ((1 + r)^N - 1) ] / r
             *
             * We simply the equation for p
             */
            if (r === 0) {
                return -(fv + pv) / n;
            }
            let payment = -(pv * (1 + r) ** n + fv);
            payment = (payment * r) / ((1 + r * t) * ((1 + r) ** n - 1));
            return payment;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PPMT
    // -----------------------------------------------------------------------------
    const PPMT = {
        description: _t("Payment on the principal of an investment."),
        args: [
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("period (number)", _t("The amortization period, in terms of number of periods.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
            arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "#,##0.00",
        compute: function (rate, currentPeriod, numberOfPeriods, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            futureValue = futureValue || 0;
            endOrBeginning = endOrBeginning || 0;
            const n = toNumber(numberOfPeriods, this.locale);
            const r = toNumber(rate, this.locale);
            const period = toNumber(currentPeriod, this.locale);
            const type = toBoolean(endOrBeginning) ? 1 : 0;
            const fv = toNumber(futureValue, this.locale);
            const pv = toNumber(presentValue, this.locale);
            assertNumberOfPeriodsStrictlyPositive(n);
            assert(() => period > 0 && period <= n, _t("The period must be between 1 and number_of_periods", n.toString()));
            const payment = PMT.compute.bind(this)(r, n, pv, fv, endOrBeginning);
            if (type === 1 && period === 1)
                return payment;
            const eqPeriod = type === 0 ? period - 1 : period - 2;
            const eqPv = pv + payment * type;
            const capitalAtPeriod = -FV.compute.bind(this)(r, eqPeriod, payment, eqPv, 0);
            const currentInterest = capitalAtPeriod * r;
            return payment + currentInterest;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PV
    // -----------------------------------------------------------------------------
    const PV = {
        description: _t("Present value of an annuity investment."),
        args: [
            arg("rate (number)", _t("The interest rate.")),
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("payment_amount (number)", _t("The amount per period to be paid.")),
            arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
        ],
        returns: ["NUMBER"],
        // to do: replace by dollar format
        computeFormat: () => "#,##0.00",
        compute: function (rate, numberOfPeriods, paymentAmount, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING) {
            futureValue = futureValue || 0;
            endOrBeginning = endOrBeginning || 0;
            const r = toNumber(rate, this.locale);
            const n = toNumber(numberOfPeriods, this.locale);
            const p = toNumber(paymentAmount, this.locale);
            const fv = toNumber(futureValue, this.locale);
            const type = toBoolean(endOrBeginning) ? 1 : 0;
            // https://wiki.documentfoundation.org/Documentation/Calc_Functions/PV
            return r ? -((p * (1 + r * type) * ((1 + r) ** n - 1)) / r + fv) / (1 + r) ** n : -(fv + p * n);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PRICE
    // -----------------------------------------------------------------------------
    const PRICE = {
        description: _t("Price of a security paying periodic interest."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("yield (number)", _t("The expected annual yield of the security.")),
            arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
            arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, rate, securityYield, redemption, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _rate = toNumber(rate, this.locale);
            const _yield = toNumber(securityYield, this.locale);
            const _redemption = toNumber(redemption, this.locale);
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString()));
            assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString()));
            assertRedemptionStrictlyPositive(_redemption);
            const years = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention);
            const nbrRealCoupons = years * _frequency;
            const nbrFullCoupons = Math.ceil(nbrRealCoupons);
            const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1;
            const yieldFactorPerPeriod = 1 + _yield / _frequency;
            const cashFlowFromCoupon = (100 * _rate) / _frequency;
            if (nbrFullCoupons === 1) {
                return ((cashFlowFromCoupon + _redemption) / ((timeFirstCoupon * _yield) / _frequency + 1) -
                    cashFlowFromCoupon * (1 - timeFirstCoupon));
            }
            let cashFlowsPresentValue = 0;
            for (let i = 1; i <= nbrFullCoupons; i++) {
                cashFlowsPresentValue +=
                    cashFlowFromCoupon / yieldFactorPerPeriod ** (i - 1 + timeFirstCoupon);
            }
            const redemptionPresentValue = _redemption / yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon);
            return (redemptionPresentValue + cashFlowsPresentValue - cashFlowFromCoupon * (1 - timeFirstCoupon));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PRICEDISC
    // -----------------------------------------------------------------------------
    const PRICEDISC = {
        description: _t("Price of a discount security."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("discount (number)", _t("The discount rate of the security at time of purchase.")),
            arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, discount, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _discount = toNumber(discount, this.locale);
            const _redemption = toNumber(redemption, this.locale);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCountConvention);
            assertDiscountStrictlyPositive(_discount);
            assertRedemptionStrictlyPositive(_redemption);
            /**
             * https://support.microsoft.com/en-us/office/pricedisc-function-d06ad7c1-380e-4be7-9fd9-75e3079acfd3
             *
             * B = number of days in year, depending on year basis
             * DSM = number of days from settlement to maturity
             *
             * PRICEDISC = redemption - discount * redemption * (DSM/B)
             */
            const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention);
            return _redemption - _discount * _redemption * yearsFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PRICEMAT
    // -----------------------------------------------------------------------------
    const PRICEMAT = {
        description: _t("Calculates the price of a security paying interest at maturity, based on expected yield."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("issue (date)", _t("The date the security was initially issued.")),
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("yield (number)", _t("The expected annual yield of the security.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, issue, rate, securityYield, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _issue = Math.trunc(toNumber(issue, this.locale));
            const _rate = toNumber(rate, this.locale);
            const _yield = toNumber(securityYield, this.locale);
            const _dayCount = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertSettlementAndIssueDatesAreValid(_settlement, _issue);
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCount);
            assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString()));
            assert(() => _yield >= 0, _t("The yield (%s) must be positive or null.", _yield.toString()));
            /**
             * https://support.microsoft.com/en-us/office/pricemat-function-52c3b4da-bc7e-476a-989f-a95f675cae77
             *
             * B = number of days in year, depending on year basis
             * DSM = number of days from settlement to maturity
             * DIM = number of days from issue to maturity
             * DIS = number of days from issue to settlement
             *
             *             100 + (DIM/B * rate * 100)
             *  PRICEMAT =  __________________________   - (DIS/B * rate * 100)
             *              1 + (DSM/B * yield)
             *
             * The ratios number_of_days / days_in_year are computed using the YEARFRAC function, that handle
             * differences due to day count conventions.
             *
             * Compatibility note :
             *
             * Contrary to GSheet and OpenOffice, Excel doesn't seems to always use its own YEARFRAC function
             * to compute PRICEMAT, and give different values for some combinations of dates and day count
             * conventions ( notably for leap years and dayCountConvention = 1 (Actual/Actual)).
             *
             * Our function PRICEMAT give us the same results as LibreOffice Calc.
             * Google Sheet use the formula with YEARFRAC, but its YEARFRAC function results are different
             * from the results of Excel/LibreOffice, thus we get different values with PRICEMAT.
             *
             */
            const settlementToMaturity = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCount);
            const issueToSettlement = YEARFRAC.compute.bind(this)(_settlement, _issue, _dayCount);
            const issueToMaturity = YEARFRAC.compute.bind(this)(_issue, _maturity, _dayCount);
            const numerator = 100 + issueToMaturity * _rate * 100;
            const denominator = 1 + settlementToMaturity * _yield;
            const term2 = issueToSettlement * _rate * 100;
            return numerator / denominator - term2;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RATE
    // -----------------------------------------------------------------------------
    const RATE_GUESS_DEFAULT = 0.1;
    const RATE = {
        description: _t("Interest rate of an annuity investment."),
        args: [
            arg("number_of_periods (number)", _t("The number of payments to be made.")),
            arg("payment_per_period (number)", _t("The amount per period to be paid.")),
            arg("present_value (number)", _t("The current value of the annuity.")),
            arg(`future_value (number, default=${DEFAULT_FUTURE_VALUE})`, _t("The future value remaining after the final payment has been made.")),
            arg(`end_or_beginning (number, default=${DEFAULT_END_OR_BEGINNING})`, _t("Whether payments are due at the end (0) or beginning (1) of each period.")),
            arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the interest rate will be.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "0%",
        compute: function (numberOfPeriods, paymentPerPeriod, presentValue, futureValue = DEFAULT_FUTURE_VALUE, endOrBeginning = DEFAULT_END_OR_BEGINNING, rateGuess = RATE_GUESS_DEFAULT) {
            futureValue = futureValue || 0;
            endOrBeginning = endOrBeginning || 0;
            rateGuess = rateGuess || RATE_GUESS_DEFAULT;
            const n = toNumber(numberOfPeriods, this.locale);
            const payment = toNumber(paymentPerPeriod, this.locale);
            const type = toBoolean(endOrBeginning) ? 1 : 0;
            const guess = toNumber(rateGuess, this.locale);
            let fv = toNumber(futureValue, this.locale);
            let pv = toNumber(presentValue, this.locale);
            assertNumberOfPeriodsStrictlyPositive(n);
            assert(() => [payment, pv, fv].some((val) => val > 0) && [payment, pv, fv].some((val) => val < 0), _t("There must be both positive and negative values in [payment_amount, present_value, future_value].", n.toString()));
            assertRateGuessStrictlyGreaterThanMinusOne(guess);
            fv -= payment * type;
            pv += payment * type;
            // https://github.com/apache/openoffice/blob/trunk/main/sc/source/core/tool/interpr2.cxx
            const func = (rate) => {
                const powN = Math.pow(1 + rate, n);
                const intResult = (powN - 1) / rate;
                return fv + pv * powN + payment * intResult;
            };
            const derivFunc = (rate) => {
                const powNMinus1 = Math.pow(1 + rate, n - 1);
                const powN = Math.pow(1 + rate, n);
                const intResult = (powN - 1) / rate;
                const intResultDeriv = (n * powNMinus1) / rate - intResult / rate;
                const fTermDerivation = pv * n * powNMinus1 + payment * intResultDeriv;
                return fTermDerivation;
            };
            return newtonMethod(func, derivFunc, guess, 40, 1e-5);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RECEIVED
    // -----------------------------------------------------------------------------
    const RECEIVED = {
        description: _t("Amount received at maturity for a security."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("investment (number)", _t("The amount invested (irrespective of face value of each security).")),
            arg("discount (number)", _t("The discount rate of the security invested in.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, investment, discount, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _investment = toNumber(investment, this.locale);
            const _discount = toNumber(discount, this.locale);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCountConvention);
            assertInvestmentStrictlyPositive(_investment);
            assertDiscountStrictlyPositive(_discount);
            /**
             * https://support.microsoft.com/en-us/office/received-function-7a3f8b93-6611-4f81-8576-828312c9b5e5
             *
             *                    investment
             * RECEIVED = _________________________
             *              1 - discount * DSM / B
             *
             * with DSM = number of days from settlement to maturity and B = number of days in a year
             *
             * The ratio DSM/B can be computed with the YEARFRAC function to take the dayCountConvention into account.
             */
            const yearsFrac = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention);
            return _investment / (1 - _discount * yearsFrac);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RRI
    // -----------------------------------------------------------------------------
    const RRI = {
        description: _t("Computes the rate needed for an investment to reach a specific value within a specific number of periods."),
        args: [
            arg("number_of_periods (number)", _t("The number of periods.")),
            arg("present_value (number)", _t("The present value of the investment.")),
            arg("future_value (number)", _t("The future value of the investment.")),
        ],
        returns: ["NUMBER"],
        compute: function (numberOfPeriods, presentValue, futureValue) {
            const n = toNumber(numberOfPeriods, this.locale);
            const pv = toNumber(presentValue, this.locale);
            const fv = toNumber(futureValue, this.locale);
            assertNumberOfPeriodsStrictlyPositive(n);
            /**
             * https://support.microsoft.com/en-us/office/rri-function-6f5822d8-7ef1-4233-944c-79e8172930f4
             *
             * RRI = (future value / present value) ^ (1 / number of periods) - 1
             */
            return (fv / pv) ** (1 / n) - 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SLN
    // -----------------------------------------------------------------------------
    const SLN = {
        description: _t("Depreciation of an asset using the straight-line method."),
        args: [
            arg("cost (number)", _t("The initial cost of the asset.")),
            arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
            arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "#,##0.00",
        compute: function (cost, salvage, life) {
            const _cost = toNumber(cost, this.locale);
            const _salvage = toNumber(salvage, this.locale);
            const _life = toNumber(life, this.locale);
            // No assertion is done on the values of the arguments to be compatible with Excel/Gsheet that don't check the values.
            // It's up to the user to make sure the arguments make sense, which is good design because the user is smart.
            return (_cost - _salvage) / _life;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SYD
    // -----------------------------------------------------------------------------
    const SYD = {
        description: _t("Depreciation via sum of years digit method."),
        args: [
            arg("cost (number)", _t("The initial cost of the asset.")),
            arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
            arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
            arg("period (number)", _t("The single period within life for which to calculate depreciation.")),
        ],
        returns: ["NUMBER"],
        computeFormat: () => "#,##0.00",
        compute: function (cost, salvage, life, period) {
            const _cost = toNumber(cost, this.locale);
            const _salvage = toNumber(salvage, this.locale);
            const _life = toNumber(life, this.locale);
            const _period = toNumber(period, this.locale);
            assertPeriodStrictlyPositive(_period);
            assertLifeStrictlyPositive(_life);
            assertPeriodSmallerOrEqualToLife(_period, _life);
            /**
             * This deprecation method use the sum of digits of the periods of the life as the deprecation factor.
             * For example for a life = 5, we have a deprecation factor or 1 + 2 + 3 + 4 + 5 = 15 = life * (life + 1) / 2 = F.
             *
             * The deprecation for a period p is then computed based on F and the remaining lifetime at the period P.
             *
             * deprecation = (cost - salvage) * (number of remaining periods / F)
             */
            const deprecFactor = (_life * (_life + 1)) / 2;
            const remainingPeriods = _life - _period + 1;
            return (_cost - _salvage) * (remainingPeriods / deprecFactor);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TBILLPRICE
    // -----------------------------------------------------------------------------
    const TBILLPRICE = {
        description: _t("Price of a US Treasury bill."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("discount (number)", _t("The discount rate of the bill at time of purchase.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, discount) {
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const disc = toNumber(discount, this.locale);
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale);
            assertDiscountStrictlyPositive(disc);
            assertDiscountStrictlySmallerThanOne(disc);
            /**
             * https://support.microsoft.com/en-us/office/tbillprice-function-eacca992-c29d-425a-9eb8-0513fe6035a2
             *
             * TBILLPRICE = 100 * (1 - discount * DSM / 360)
             *
             * with DSM = number of days from settlement to maturity
             *
             * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360).
             */
            const yearFrac = YEARFRAC.compute.bind(this)(start, end, 2);
            return 100 * (1 - disc * yearFrac);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TBILLEQ
    // -----------------------------------------------------------------------------
    const TBILLEQ = {
        description: _t("Equivalent rate of return for a US Treasury bill."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("discount (number)", _t("The discount rate of the bill at time of purchase.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, discount) {
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const disc = toNumber(discount, this.locale);
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale);
            assertDiscountStrictlyPositive(disc);
            assertDiscountStrictlySmallerThanOne(disc);
            /**
             * https://support.microsoft.com/en-us/office/tbilleq-function-2ab72d90-9b4d-4efe-9fc2-0f81f2c19c8c
             *
             *               365 * discount
             * TBILLEQ = ________________________
             *            360 - discount * DSM
             *
             * with DSM = number of days from settlement to maturity
             *
             * What is not indicated in the Excel documentation is that this formula only works for duration between settlement
             * and maturity that are less than 6 months (182 days). This is because US Treasury bills use semi-annual interest,
             * and thus we have to take into account the compound interest for the calculation.
             *
             * For this case, the formula becomes (Treasury Securities and Derivatives, by Frank J. Fabozzi, page 49)
             *
             *            -2X + 2* SQRT[ X² - (2X - 1) * (1 - 100/p) ]
             * TBILLEQ = ________________________________________________
             *                            2X - 1
             *
             * with X = DSM / (number of days in a year),
             *  and p is the price, computed with TBILLPRICE
             *
             * Note that from my tests in Excel, we take (number of days in a year) = 366 ONLY if DSM is 366, not if
             * the settlement year is a leap year.
             *
             */
            const nDays = DAYS.compute.bind(this)(end, start);
            if (nDays <= 182) {
                return (365 * disc) / (360 - disc * nDays);
            }
            const p = TBILLPRICE.compute.bind(this)(start, end, disc) / 100;
            const daysInYear = nDays === 366 ? 366 : 365;
            const x = nDays / daysInYear;
            const num = -2 * x + 2 * Math.sqrt(x ** 2 - (2 * x - 1) * (1 - 1 / p));
            const denom = 2 * x - 1;
            return num / denom;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TBILLYIELD
    // -----------------------------------------------------------------------------
    const TBILLYIELD = {
        description: _t("The yield of a US Treasury bill based on price."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, price) {
            const start = Math.trunc(toNumber(settlement, this.locale));
            const end = Math.trunc(toNumber(maturity, this.locale));
            const p = toNumber(price, this.locale);
            assertMaturityAndSettlementDatesAreValid(start, end);
            assertSettlementLessThanOneYearBeforeMaturity(start, end, this.locale);
            assertPriceStrictlyPositive(p);
            /**
             * https://support.microsoft.com/en-us/office/tbillyield-function-6d381232-f4b0-4cd5-8e97-45b9c03468ba
             *
             *              100 - price     360
             * TBILLYIELD = ____________ * _____
             *                 price        DSM
             *
             * with DSM = number of days from settlement to maturity
             *
             * The ratio DSM/360 can be computed with the YEARFRAC function with dayCountConvention = 2 (actual/360).
             *
             */
            const yearFrac = YEARFRAC.compute.bind(this)(start, end, 2);
            return ((100 - p) / p) * (1 / yearFrac);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VDB
    // -----------------------------------------------------------------------------
    const DEFAULT_VDB_NO_SWITCH = false;
    const VDB = {
        description: _t("Variable declining balance. WARNING : does not handle decimal periods."),
        args: [
            arg("cost (number)", _t("The initial cost of the asset.")),
            arg("salvage (number)", _t("The value of the asset at the end of depreciation.")),
            arg("life (number)", _t("The number of periods over which the asset is depreciated.")),
            arg("start (number)", _t("Starting period to calculate depreciation.")),
            arg("end (number)", _t("Ending period to calculate depreciation.")),
            arg(`factor (number, default=${DEFAULT_DDB_DEPRECIATION_FACTOR})`, _t("The number of months in the first year of depreciation.")),
            arg(`no_switch (number, default=${DEFAULT_VDB_NO_SWITCH})`, _t("Whether to switch to straight-line depreciation when the depreciation is greater than the declining balance calculation.")),
        ],
        returns: ["NUMBER"],
        compute: function (cost, salvage, life, startPeriod, endPeriod, factor = DEFAULT_DDB_DEPRECIATION_FACTOR, noSwitch = DEFAULT_VDB_NO_SWITCH) {
            factor = factor || 0;
            const _cost = toNumber(cost, this.locale);
            const _salvage = toNumber(salvage, this.locale);
            const _life = toNumber(life, this.locale);
            /* TODO : handle decimal periods
             * on end_period it looks like it is a simple linear function, but I cannot understand exactly how
             * decimals periods are handled with start_period.
             */
            const _startPeriod = Math.trunc(toNumber(startPeriod, this.locale));
            const _endPeriod = Math.trunc(toNumber(endPeriod, this.locale));
            const _factor = toNumber(factor, this.locale);
            const _noSwitch = toBoolean(noSwitch);
            assertCostPositiveOrZero(_cost);
            assertSalvagePositiveOrZero(_salvage);
            assertStartAndEndPeriodAreValid(_startPeriod, _endPeriod, _life);
            assertDeprecationFactorStrictlyPositive(_factor);
            if (_cost === 0)
                return 0;
            if (_salvage >= _cost) {
                return _startPeriod < 1 ? _cost - _salvage : 0;
            }
            const doubleDeprecFactor = _factor / _life;
            if (doubleDeprecFactor >= 1) {
                return _startPeriod < 1 ? _cost - _salvage : 0;
            }
            let previousCost = _cost;
            let currentDeprec = 0;
            let resultDeprec = 0;
            let isLinearDeprec = false;
            for (let i = 0; i < _endPeriod; i++) {
                // compute the current deprecation, or keep the last one if we reached a stage of linear deprecation
                if (!isLinearDeprec || _noSwitch) {
                    const doubleDeprec = previousCost * doubleDeprecFactor;
                    const remainingPeriods = _life - i;
                    const linearDeprec = (previousCost - _salvage) / remainingPeriods;
                    if (!_noSwitch && linearDeprec > doubleDeprec) {
                        isLinearDeprec = true;
                        currentDeprec = linearDeprec;
                    }
                    else {
                        currentDeprec = doubleDeprec;
                    }
                }
                const nextCost = Math.max(previousCost - currentDeprec, _salvage);
                if (i >= _startPeriod) {
                    resultDeprec += previousCost - nextCost;
                }
                previousCost = nextCost;
            }
            return resultDeprec;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // XIRR
    // -----------------------------------------------------------------------------
    const XIRR = {
        description: _t("Internal rate of return given non-periodic cash flows."),
        args: [
            arg("cashflow_amounts (range<number>)", _t("An range containing the income or payments associated with the investment.")),
            arg("cashflow_dates (range<number>)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")),
            arg(`rate_guess (number, default=${RATE_GUESS_DEFAULT})`, _t("An estimate for what the internal rate of return will be.")),
        ],
        returns: ["NUMBER"],
        compute: function (cashflowAmounts, cashflowDates, rateGuess = RATE_GUESS_DEFAULT) {
            rateGuess = rateGuess || 0;
            const guess = toNumber(rateGuess, this.locale);
            const _cashFlows = cashflowAmounts.flat().map((val) => toNumber(val, this.locale));
            const _dates = cashflowDates.flat().map((val) => toNumber(val, this.locale));
            assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates);
            assertCashFlowsHavePositiveAndNegativesValues(_cashFlows);
            assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates);
            assertRateGuessStrictlyGreaterThanMinusOne(guess);
            const map = new Map();
            for (const i of range(0, _dates.length)) {
                const date = _dates[i];
                if (map.has(date))
                    map.set(date, map.get(date) + _cashFlows[i]);
                else
                    map.set(date, _cashFlows[i]);
            }
            const dates = Array.from(map.keys());
            const values = dates.map((date) => map.get(date));
            /**
             * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d
             *
             * The rate is computed iteratively by trying to solve the equation
             *
             *
             * 0 =    SUM     [ P_i * (1 + rate) ^((d_0 - d_i) / 365) ]  + P_0
             *     i = 1 => n
             *
             * with P_i = price number i
             *      d_i = date number i
             *
             * This function is not defined for rate < -1. For the case where we get rates < -1 in the Newton method, add
             * a fallback for a number very close to -1 to continue the Newton method.
             *
             */
            const func = (rate) => {
                let value = values[0];
                for (const i of range(1, values.length)) {
                    const dateDiff = (dates[0] - dates[i]) / 365;
                    value += values[i] * (1 + rate) ** dateDiff;
                }
                return value;
            };
            const derivFunc = (rate) => {
                let deriv = 0;
                for (const i of range(1, values.length)) {
                    const dateDiff = (dates[0] - dates[i]) / 365;
                    deriv += dateDiff * values[i] * (1 + rate) ** (dateDiff - 1);
                }
                return deriv;
            };
            const nanFallback = (previousFallback) => {
                // -0.9 => -0.99 => -0.999 => ...
                if (!previousFallback)
                    return -0.9;
                return previousFallback / 10 - 0.9;
            };
            return newtonMethod(func, derivFunc, guess, 40, 1e-5, nanFallback);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // XNPV
    // -----------------------------------------------------------------------------
    const XNPV = {
        description: _t("Net present value given to non-periodic cash flows.."),
        args: [
            arg("discount (number)", _t("The discount rate of the investment over one period.")),
            arg("cashflow_amounts (number, range<number>)", _t("An range containing the income or payments associated with the investment.")),
            arg("cashflow_dates (number, range<number>)", _t("An range with dates corresponding to the cash flows in cashflow_amounts.")),
        ],
        returns: ["NUMBER"],
        compute: function (discount, cashflowAmounts, cashflowDates) {
            const rate = toNumber(discount, this.locale);
            const _cashFlows = isMatrix(cashflowAmounts)
                ? cashflowAmounts.flat().map((val) => strictToNumber(val, this.locale))
                : [strictToNumber(cashflowAmounts, this.locale)];
            const _dates = isMatrix(cashflowDates)
                ? cashflowDates.flat().map((val) => strictToNumber(val, this.locale))
                : [strictToNumber(cashflowDates, this.locale)];
            if (isMatrix(cashflowDates) && isMatrix(cashflowAmounts)) {
                assertCashFlowsAndDatesHaveSameDimension(cashflowAmounts, cashflowDates);
            }
            else {
                assert(() => _cashFlows.length === _dates.length, _t("There must be the same number of values in cashflow_amounts and cashflow_dates."));
            }
            assertEveryDateGreaterThanFirstDateOfCashFlowDates(_dates);
            assertRateStrictlyPositive(rate);
            if (_cashFlows.length === 1)
                return _cashFlows[0];
            // aggregate values of the same date
            const map = new Map();
            for (const i of range(0, _dates.length)) {
                const date = _dates[i];
                if (map.has(date))
                    map.set(date, map.get(date) + _cashFlows[i]);
                else
                    map.set(date, _cashFlows[i]);
            }
            const dates = Array.from(map.keys());
            const values = dates.map((date) => map.get(date));
            /**
             * https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d
             *
             * The present value is computed using
             *
             *
             * NPV =    SUM     [ P_i *(1 + rate) ^((d_0 - d_i) / 365) ]  + P_0
             *       i = 1 => n
             *
             * with P_i = price number i
             *      d_i = date number i
             *
             *
             */
            let pv = values[0];
            for (const i of range(1, values.length)) {
                const dateDiff = (dates[0] - dates[i]) / 365;
                pv += values[i] * (1 + rate) ** dateDiff;
            }
            return pv;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // YIELD
    // -----------------------------------------------------------------------------
    const YIELD = {
        description: _t("Annual yield of a security paying periodic interest."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
            arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
            arg("frequency (number)", _t("The number of interest or coupon payments per year (1, 2, or 4).")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, rate, price, redemption, frequency, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _rate = toNumber(rate, this.locale);
            const _price = toNumber(price, this.locale);
            const _redemption = toNumber(redemption, this.locale);
            const _frequency = Math.trunc(toNumber(frequency, this.locale));
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertCouponFrequencyIsValid(_frequency);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString()));
            assertPriceStrictlyPositive(_price);
            assertRedemptionStrictlyPositive(_redemption);
            const years = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention);
            const nbrRealCoupons = years * _frequency;
            const nbrFullCoupons = Math.ceil(nbrRealCoupons);
            const timeFirstCoupon = nbrRealCoupons - Math.floor(nbrRealCoupons) || 1;
            const cashFlowFromCoupon = (100 * _rate) / _frequency;
            if (nbrFullCoupons === 1) {
                const subPart = _price + cashFlowFromCoupon * (1 - timeFirstCoupon);
                return (((_redemption + cashFlowFromCoupon - subPart) * _frequency * (1 / timeFirstCoupon)) /
                    subPart);
            }
            // The result of YIELD function is the yield at which the PRICE function will return the given price.
            // This algorithm uses the Newton's method on the PRICE function to determine the result.
            // Newton's method: https://en.wikipedia.org/wiki/Newton%27s_method
            // As the PRICE function isn't continuous, we apply the Newton's method on the numerator of the PRICE formula.
            // For simplicity, it is not yield but yieldFactorPerPeriod (= 1 + yield / frequency) which will be calibrated in Newton's method.
            // yield can be deduced from yieldFactorPerPeriod in sequence.
            function priceNumerator(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon, redemption) {
                let result = redemption -
                    (price + cashFlowFromCoupon * (1 - timeFirstCoupon)) *
                        yieldFactorPerPeriod ** (nbrFullCoupons - 1 + timeFirstCoupon);
                for (let i = 1; i <= nbrFullCoupons; i++) {
                    result += cashFlowFromCoupon * yieldFactorPerPeriod ** (i - 1);
                }
                return result;
            }
            function priceNumeratorDeriv(price, timeFirstCoupon, nbrFullCoupons, yieldFactorPerPeriod, cashFlowFromCoupon) {
                let result = -(price + cashFlowFromCoupon * (1 - timeFirstCoupon)) *
                    (nbrFullCoupons - 1 + timeFirstCoupon) *
                    yieldFactorPerPeriod ** (nbrFullCoupons - 2 + timeFirstCoupon);
                for (let i = 1; i <= nbrFullCoupons; i++) {
                    result += cashFlowFromCoupon * (i - 1) * yieldFactorPerPeriod ** (i - 2);
                }
                return result;
            }
            function func(x) {
                return priceNumerator(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon, _redemption);
            }
            function derivFunc(x) {
                return priceNumeratorDeriv(_price, timeFirstCoupon, nbrFullCoupons, x, cashFlowFromCoupon);
            }
            const initYield = _rate + 1;
            const initYieldFactorPerPeriod = 1 + initYield / _frequency;
            const methodResult = newtonMethod(func, derivFunc, initYieldFactorPerPeriod, 100, 1e-5);
            return (methodResult - 1) * _frequency;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // YIELDDISC
    // -----------------------------------------------------------------------------
    const YIELDDISC = {
        description: _t("Annual yield of a discount security."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("price (number)", _t("The price at which the security is bought per 100 face value.")),
            arg("redemption (number)", _t("The redemption amount per 100 face value, or par.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, price, redemption, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _price = toNumber(price, this.locale);
            const _redemption = toNumber(redemption, this.locale);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCountConvention);
            assertPriceStrictlyPositive(_price);
            assertRedemptionStrictlyPositive(_redemption);
            /**
             * https://wiki.documentfoundation.org/Documentation/Calc_Functions/YIELDDISC
             *
             *                    (redemption / price) - 1
             * YIELDDISC = _____________________________________
             *             YEARFRAC(settlement, maturity, basis)
             */
            const yearFrac = YEARFRAC.compute.bind(this)(settlement, maturity, dayCountConvention);
            return (_redemption / _price - 1) / yearFrac;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // YIELDMAT
    // -----------------------------------------------------------------------------
    const YIELDMAT = {
        description: _t("Annual yield of a security paying interest at maturity."),
        args: [
            arg("settlement (date)", _t("The settlement date of the security, the date after issuance when the security is delivered to the buyer.")),
            arg("maturity (date)", _t("The maturity or end date of the security, when it can be redeemed at face, or par value.")),
            arg("issue (date)", _t("The date the security was initially issued.")),
            arg("rate (number)", _t("The annualized rate of interest.")),
            arg("price (number)", _t("The price at which the security is bought.")),
            arg(`day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} )`, _t("An indicator of what day count method to use.")),
        ],
        returns: ["NUMBER"],
        compute: function (settlement, maturity, issue, rate, price, dayCountConvention = DEFAULT_DAY_COUNT_CONVENTION) {
            dayCountConvention = dayCountConvention || 0;
            const _settlement = Math.trunc(toNumber(settlement, this.locale));
            const _maturity = Math.trunc(toNumber(maturity, this.locale));
            const _issue = Math.trunc(toNumber(issue, this.locale));
            const _rate = toNumber(rate, this.locale);
            const _price = toNumber(price, this.locale);
            const _dayCountConvention = Math.trunc(toNumber(dayCountConvention, this.locale));
            assertMaturityAndSettlementDatesAreValid(_settlement, _maturity);
            assertDayCountConventionIsValid(_dayCountConvention);
            assert(() => _settlement >= _issue, _t("The settlement (%s) must be greater than or equal to the issue (%s).", _settlement.toString(), _issue.toString()));
            assert(() => _rate >= 0, _t("The rate (%s) must be positive or null.", _rate.toString()));
            assertPriceStrictlyPositive(_price);
            const issueToMaturity = YEARFRAC.compute.bind(this)(_issue, _maturity, _dayCountConvention);
            const issueToSettlement = YEARFRAC.compute.bind(this)(_issue, _settlement, _dayCountConvention);
            const settlementToMaturity = YEARFRAC.compute.bind(this)(_settlement, _maturity, _dayCountConvention);
            const numerator = (100 * (1 + _rate * issueToMaturity)) / (_price + 100 * _rate * issueToSettlement) - 1;
            return numerator / settlementToMaturity;
        },
        isExported: true,
    };

    var financial = /*#__PURE__*/Object.freeze({
        __proto__: null,
        ACCRINTM: ACCRINTM,
        AMORLINC: AMORLINC,
        COUPDAYBS: COUPDAYBS,
        COUPDAYS: COUPDAYS,
        COUPDAYSNC: COUPDAYSNC,
        COUPNCD: COUPNCD,
        COUPNUM: COUPNUM,
        COUPPCD: COUPPCD,
        CUMIPMT: CUMIPMT,
        CUMPRINC: CUMPRINC,
        DB: DB,
        DDB: DDB,
        DISC: DISC,
        DOLLARDE: DOLLARDE,
        DOLLARFR: DOLLARFR,
        DURATION: DURATION,
        EFFECT: EFFECT,
        FV: FV,
        FVSCHEDULE: FVSCHEDULE,
        INTRATE: INTRATE,
        IPMT: IPMT,
        IRR: IRR,
        ISPMT: ISPMT,
        MDURATION: MDURATION,
        MIRR: MIRR,
        NOMINAL: NOMINAL,
        NPER: NPER,
        NPV: NPV,
        PDURATION: PDURATION,
        PMT: PMT,
        PPMT: PPMT,
        PRICE: PRICE,
        PRICEDISC: PRICEDISC,
        PRICEMAT: PRICEMAT,
        PV: PV,
        RATE: RATE,
        RECEIVED: RECEIVED,
        RRI: RRI,
        SLN: SLN,
        SYD: SYD,
        TBILLEQ: TBILLEQ,
        TBILLPRICE: TBILLPRICE,
        TBILLYIELD: TBILLYIELD,
        VDB: VDB,
        XIRR: XIRR,
        XNPV: XNPV,
        YIELD: YIELD,
        YIELDDISC: YIELDDISC,
        YIELDMAT: YIELDMAT
    });

    // -----------------------------------------------------------------------------
    // ISERR
    // -----------------------------------------------------------------------------
    const ISERR = {
        description: _t("Whether a value is an error other than #N/A."),
        args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                value();
                return false;
            }
            catch (e) {
                return e?.errorType != CellErrorType.NotAvailable;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISERROR
    // -----------------------------------------------------------------------------
    const ISERROR = {
        description: _t("Whether a value is an error."),
        args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                value();
                return false;
            }
            catch (e) {
                return true;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISLOGICAL
    // -----------------------------------------------------------------------------
    const ISLOGICAL = {
        description: _t("Whether a value is `true` or `false`."),
        args: [arg("value (any, lazy)", _t("The value to be verified as a logical TRUE or FALSE."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                return typeof value() === "boolean";
            }
            catch (e) {
                return false;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISNA
    // -----------------------------------------------------------------------------
    const ISNA = {
        description: _t("Whether a value is the error #N/A."),
        args: [arg("value (any, lazy)", _t("The value to be verified as an error type."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                value();
                return false;
            }
            catch (e) {
                return e?.errorType === CellErrorType.NotAvailable;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISNONTEXT
    // -----------------------------------------------------------------------------
    const ISNONTEXT = {
        description: _t("Whether a value is non-textual."),
        args: [arg("value (any, lazy)", _t("The value to be checked."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                return typeof value() !== "string";
            }
            catch (e) {
                return true;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISNUMBER
    // -----------------------------------------------------------------------------
    const ISNUMBER = {
        description: _t("Whether a value is a number."),
        args: [arg("value (any, lazy)", _t("The value to be verified as a number."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                return typeof value() === "number";
            }
            catch (e) {
                return false;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISTEXT
    // -----------------------------------------------------------------------------
    const ISTEXT = {
        description: _t("Whether a value is text."),
        args: [arg("value (any, lazy)", _t("The value to be verified as text."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                return typeof value() === "string";
            }
            catch (e) {
                return false;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ISBLANK
    // -----------------------------------------------------------------------------
    const ISBLANK = {
        description: _t("Whether the referenced cell is empty"),
        args: [arg("value (any, lazy)", _t("Reference to the cell that will be checked for emptiness."))],
        returns: ["BOOLEAN"],
        compute: function (value) {
            try {
                const val = value();
                return val === null;
            }
            catch (e) {
                return false;
            }
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NA
    // -----------------------------------------------------------------------------
    const NA = {
        description: _t("Returns the error value #N/A."),
        args: [],
        returns: ["BOOLEAN"],
        compute: function (value) {
            throw new NotAvailableError();
        },
        isExported: true,
    };

    var info = /*#__PURE__*/Object.freeze({
        __proto__: null,
        ISBLANK: ISBLANK,
        ISERR: ISERR,
        ISERROR: ISERROR,
        ISLOGICAL: ISLOGICAL,
        ISNA: ISNA,
        ISNONTEXT: ISNONTEXT,
        ISNUMBER: ISNUMBER,
        ISTEXT: ISTEXT,
        NA: NA
    });

    // -----------------------------------------------------------------------------
    // AND
    // -----------------------------------------------------------------------------
    const AND = {
        description: _t("Logical `and` operator."),
        args: [
            arg("logical_expression1 (boolean, range<boolean>)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")),
            arg("logical_expression2 (boolean, range<boolean>, repeating)", _t("More expressions that represent logical values.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (...logicalExpressions) {
            let foundBoolean = false;
            let acc = true;
            conditionalVisitBoolean(logicalExpressions, (arg) => {
                foundBoolean = true;
                acc = acc && arg;
                return acc;
            });
            assert(() => foundBoolean, _t("[[FUNCTION_NAME]] has no valid input data."));
            return acc;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FALSE
    // -----------------------------------------------------------------------------
    const FALSE = {
        description: _t("Logical value `false`."),
        args: [],
        returns: ["BOOLEAN"],
        compute: function () {
            return false;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IF
    // -----------------------------------------------------------------------------
    const IF = {
        description: _t("Returns value depending on logical expression."),
        args: [
            arg("logical_expression (boolean)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE.")),
            arg("value_if_true (any, lazy)", _t("The value the function returns if logical_expression is TRUE.")),
            arg("value_if_false (any, lazy, default=FALSE)", _t("The value the function returns if logical_expression is FALSE.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (logicalExpression, valueIfTrue, valueIfFalse = () => ({ value: false })) {
            const result = toBoolean(logicalExpression?.value) ? valueIfTrue() : valueIfFalse();
            if (result === undefined) {
                return { value: "" };
            }
            if (result.value === null) {
                result.value = "";
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IFERROR
    // -----------------------------------------------------------------------------
    const IFERROR = {
        description: _t("Value if it is not an error, otherwise 2nd argument."),
        args: [
            arg("value (any, lazy)", _t("The value to return if value itself is not an error.")),
            arg(`value_if_error (any, lazy, default="empty")`, _t("The value the function returns if value is an error.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (value, valueIfError = () => ({ value: "" })) {
            let result;
            try {
                result = value();
            }
            catch (e) {
                result = valueIfError();
            }
            if (result === undefined) {
                return { value: "" };
            }
            if (result.value === null) {
                result.value = "";
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IFNA
    // -----------------------------------------------------------------------------
    const IFNA = {
        description: _t("Value if it is not an #N/A error, otherwise 2nd argument."),
        args: [
            arg("value (any, lazy)", _t("The value to return if value itself is not #N/A an error.")),
            arg(`value_if_error (any, lazy, default="empty")`, _t("The value the function returns if value is an #N/A error.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (value, valueIfError = () => ({ value: "" })) {
            let result;
            try {
                result = value();
            }
            catch (e) {
                if (e.errorType === CellErrorType.NotAvailable) {
                    result = valueIfError();
                }
                else {
                    result = value();
                }
            }
            if (result === undefined) {
                return { value: "" };
            }
            if (result.value === null) {
                result.value = "";
            }
            return result;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // IFS
    // -----------------------------------------------------------------------------
    const IFS = {
        description: _t("Returns a value depending on multiple logical expressions."),
        args: [
            arg("condition1 (boolean, lazy)", _t("The first condition to be evaluated. This can be a boolean, a number, an array, or a reference to any of those.")),
            arg("value1 (any, lazy)", _t("The returned value if condition1 is TRUE.")),
            arg("condition2 (boolean, lazy, repeating)", _t("Additional conditions to be evaluated if the previous ones are FALSE.")),
            arg("value2 (any, lazy, repeating)", _t("Additional values to be returned if their corresponding conditions are TRUE.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (...values) {
            assert(() => values.length % 2 === 0, _t("Wrong number of arguments. Expected an even number of arguments."));
            for (let n = 0; n < values.length - 1; n += 2) {
                if (toBoolean(values[n]()?.value)) {
                    const result = values[n + 1]();
                    if (result === undefined) {
                        return { value: "" };
                    }
                    if (result.value === null) {
                        result.value = "";
                    }
                    return result;
                }
            }
            throw new Error(_t("No match."));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // NOT
    // -----------------------------------------------------------------------------
    const NOT = {
        description: _t("Returns opposite of provided logical value."),
        args: [
            arg("logical_expression (boolean)", _t("An expression or reference to a cell holding an expression that represents some logical value.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (logicalExpression) {
            return !toBoolean(logicalExpression);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // OR
    // -----------------------------------------------------------------------------
    const OR = {
        description: _t("Logical `or` operator."),
        args: [
            arg("logical_expression1 (boolean, range<boolean>)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")),
            arg("logical_expression2 (boolean, range<boolean>, repeating)", _t("More expressions that evaluate to logical values.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (...logicalExpressions) {
            let foundBoolean = false;
            let acc = false;
            conditionalVisitBoolean(logicalExpressions, (arg) => {
                foundBoolean = true;
                acc = acc || arg;
                return !acc;
            });
            assert(() => foundBoolean, _t("[[FUNCTION_NAME]] has no valid input data."));
            return acc;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TRUE
    // -----------------------------------------------------------------------------
    const TRUE = {
        description: _t("Logical value `true`."),
        args: [],
        returns: ["BOOLEAN"],
        compute: function () {
            return true;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // XOR
    // -----------------------------------------------------------------------------
    const XOR = {
        description: _t("Logical `xor` operator."),
        args: [
            arg("logical_expression1 (boolean, range<boolean>)", _t("An expression or reference to a cell containing an expression that represents some logical value, i.e. TRUE or FALSE, or an expression that can be coerced to a logical value.")),
            arg("logical_expression2 (boolean, range<boolean>, repeating)", _t("More expressions that evaluate to logical values.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (...logicalExpressions) {
            let foundBoolean = false;
            let acc = false;
            conditionalVisitBoolean(logicalExpressions, (arg) => {
                foundBoolean = true;
                acc = acc ? !arg : arg;
                return true; // no stop condition
            });
            assert(() => foundBoolean, _t("[[FUNCTION_NAME]] has no valid input data."));
            return acc;
        },
        isExported: true,
    };

    var logical = /*#__PURE__*/Object.freeze({
        __proto__: null,
        AND: AND,
        FALSE: FALSE,
        IF: IF,
        IFERROR: IFERROR,
        IFNA: IFNA,
        IFS: IFS,
        NOT: NOT,
        OR: OR,
        TRUE: TRUE,
        XOR: XOR
    });

    const DEFAULT_IS_SORTED = true;
    const DEFAULT_MATCH_MODE = 0;
    const DEFAULT_SEARCH_MODE = 1;
    const DEFAULT_ABSOLUTE_RELATIVE_MODE = 1;
    function assertAvailable(variable, searchKey) {
        if (variable === undefined) {
            throw new NotAvailableError(_t("Did not find value '%s' in [[FUNCTION_NAME]] evaluation.", toString(searchKey)));
        }
    }
    // -----------------------------------------------------------------------------
    // ADDRESS
    // -----------------------------------------------------------------------------
    const ADDRESS = {
        description: _t("Returns a cell reference as a string. "),
        args: [
            arg("row (number)", _t("The row number of the cell reference. ")),
            arg("column (number)", _t("The column number (not name) of the cell reference. A is column number 1. ")),
            arg(`absolute_relative_mode (number, default=${DEFAULT_ABSOLUTE_RELATIVE_MODE})`, _t("An indicator of whether the reference is row/column absolute. 1 is row and column absolute (e.g. $A$1), 2 is row absolute and column relative (e.g. A$1), 3 is row relative and column absolute (e.g. $A1), and 4 is row and column relative (e.g. A1).")),
            arg("use_a1_notation (boolean, default=TRUE)", _t("A boolean indicating whether to use A1 style notation (TRUE) or R1C1 style notation (FALSE).")),
            arg("sheet (string, optional)", _t("A string indicating the name of the sheet into which the address points.")),
        ],
        returns: ["STRING"],
        compute: function (row, column, absoluteRelativeMode = DEFAULT_ABSOLUTE_RELATIVE_MODE, useA1Notation = true, sheet) {
            const rowNumber = strictToInteger(row, this.locale);
            const colNumber = strictToInteger(column, this.locale);
            assertNumberGreaterThanOrEqualToOne(rowNumber);
            assertNumberGreaterThanOrEqualToOne(colNumber);
            const _absoluteRelativeMode = strictToInteger(absoluteRelativeMode, this.locale);
            assert(() => [1, 2, 3, 4].includes(_absoluteRelativeMode), expectNumberRangeError(1, 4, _absoluteRelativeMode));
            const _useA1Notation = toBoolean(useA1Notation);
            let cellReference;
            if (_useA1Notation) {
                const rangePart = {
                    rowFixed: [1, 2].includes(_absoluteRelativeMode) ? true : false,
                    colFixed: [1, 3].includes(_absoluteRelativeMode) ? true : false,
                };
                cellReference = toXC(colNumber - 1, rowNumber - 1, rangePart);
            }
            else {
                const rowPart = [1, 2].includes(_absoluteRelativeMode) ? `R${rowNumber}` : `R[${rowNumber}]`;
                const colPart = [1, 3].includes(_absoluteRelativeMode) ? `C${colNumber}` : `C[${colNumber}]`;
                cellReference = rowPart + colPart;
            }
            if (sheet !== undefined) {
                return `${getCanonicalSheetName(toString(sheet))}!${cellReference}`;
            }
            return cellReference;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COLUMN
    // -----------------------------------------------------------------------------
    const COLUMN = {
        description: _t("Column number of a specified cell."),
        args: [
            arg("cell_reference (meta, default='this cell')", _t("The cell whose column number will be returned. Column A corresponds to 1. By default, the function use the cell in which the formula is entered.")),
        ],
        returns: ["NUMBER"],
        compute: function (cellReference) {
            const _cellReference = cellReference || this.__originCellXC?.();
            assert(() => !!_cellReference, "In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter.");
            const zone = toZone(_cellReference);
            return zone.left + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // COLUMNS
    // -----------------------------------------------------------------------------
    const COLUMNS = {
        description: _t("Number of columns in a specified array or range."),
        args: [arg("range (meta)", _t("The range whose column count will be returned."))],
        returns: ["NUMBER"],
        compute: function (range) {
            const zone = toZone(range);
            return zone.right - zone.left + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // HLOOKUP
    // -----------------------------------------------------------------------------
    const HLOOKUP = {
        description: _t("Horizontal lookup"),
        args: [
            arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
            arg("range (range)", _t("The range to consider for the search. The first row in the range is searched for the key specified in search_key.")),
            arg("index (number)", _t("The row index of the value to be returned, where the first row in range is numbered 1.")),
            arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the row to be searched (the first row of the specified range) is sorted, in which case the closest match for search_key will be returned.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
            const _index = Math.trunc(toNumber(index?.value, this.locale));
            assert(() => 1 <= _index && _index <= range[0].length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
            const getValueFromRange = (range, index) => range[index][0].value;
            const _isSorted = toBoolean(isSorted.value);
            const colIndex = _isSorted
                ? dichotomicSearch(range, searchKey?.value, "nextSmaller", "asc", range.length, getValueFromRange)
                : linearSearch(range, searchKey?.value, "strict", range.length, getValueFromRange);
            const col = range[colIndex];
            assertAvailable(col, searchKey?.value);
            return col[_index - 1];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // INDEX
    // -----------------------------------------------------------------------------
    const INDEX = {
        description: _t("Returns the content of a cell, specified by row and column offset."),
        args: [
            arg("reference (any, range)", _t("The range of cells from which the values are returned.")),
            arg("row (number, default=0)", _t("The index of the row to be returned from within the reference range of cells.")),
            arg("column (number, default=0)", _t("The index of the column to be returned from within the reference range of cells.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (reference, row = { value: 0 }, column = { value: 0 }) {
            const _reference = isMatrix(reference) ? reference : [[reference]];
            const _row = toNumber(row.value, this.locale);
            const _column = toNumber(column.value, this.locale);
            assert(() => _column >= 0 &&
                _column - 1 < _reference.length &&
                _row >= 0 &&
                _row - 1 < _reference[0].length, _t("Index out of range."));
            if (_row === 0 && _column === 0) {
                return _reference;
            }
            if (_row === 0) {
                return [_reference[_column - 1]];
            }
            if (_column === 0) {
                return _reference.map((col) => [col[_row - 1]]);
            }
            return _reference[_column - 1][_row - 1];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LOOKUP
    // -----------------------------------------------------------------------------
    const LOOKUP = {
        description: _t("Look up a value."),
        args: [
            arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
            arg("search_array (range)", _t("One method of using this function is to provide a single sorted row or column search_array to look through for the search_key with a second argument result_range. The other way is to combine these two arguments into one search_array where the first row or column is searched and a value is returned from the last row or column in the array. If search_key is not found, a non-exact match may be returned.")),
            arg("result_range (range, optional)", _t("The range from which to return a result. The value returned corresponds to the location where search_key is found in search_range. This range must be only a single row or column and should not be used if using the search_result_array method.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (searchKey, searchArray, resultRange) {
            let nbCol = searchArray.length;
            let nbRow = searchArray[0].length;
            const verticalSearch = nbRow >= nbCol;
            const getElement = verticalSearch
                ? (range, index) => range[0][index].value
                : (range, index) => range[index][0].value;
            const rangeLength = verticalSearch ? nbRow : nbCol;
            const index = dichotomicSearch(searchArray, searchKey?.value, "nextSmaller", "asc", rangeLength, getElement);
            if (index === -1)
                assertAvailable(undefined, searchKey?.value);
            verticalSearch
                ? assertAvailable(searchArray[0][index], searchKey?.value)
                : assertAvailable(searchArray[index][nbRow - 1], searchKey?.value);
            if (resultRange === undefined) {
                return verticalSearch ? searchArray[nbCol - 1][index] : searchArray[index][nbRow - 1];
            }
            nbCol = resultRange.length;
            nbRow = resultRange[0].length;
            assert(() => nbCol === 1 || nbRow === 1, _t("The result_range must be a single row or a single column."));
            if (nbCol > 1) {
                assert(() => index <= nbCol - 1, _t("[[FUNCTION_NAME]] evaluates to an out of range row value %s.", (index + 1).toString()));
                return resultRange[index][0];
            }
            assert(() => index <= nbRow - 1, _t("[[FUNCTION_NAME]] evaluates to an out of range column value %s.", (index + 1).toString()));
            return resultRange[0][index];
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MATCH
    // -----------------------------------------------------------------------------
    const DEFAULT_SEARCH_TYPE = 1;
    const MATCH = {
        description: _t("Position of item in range that matches value."),
        args: [
            arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
            arg("range (any, range)", _t("The one-dimensional array to be searched.")),
            arg(`search_type (number, default=${DEFAULT_SEARCH_TYPE})`, _t("The search method. 1 (default) finds the largest value less than or equal to search_key when range is sorted in ascending order. 0 finds the exact value when range is unsorted. -1 finds the smallest value greater than or equal to search_key when range is sorted in descending order.")),
        ],
        returns: ["NUMBER"],
        compute: function (searchKey, range, searchType = DEFAULT_SEARCH_TYPE) {
            let _searchType = toNumber(searchType, this.locale);
            const nbCol = range.length;
            const nbRow = range[0].length;
            assert(() => nbCol === 1 || nbRow === 1, _t("The range must be a single row or a single column."));
            let index = -1;
            const getElement = nbCol === 1
                ? (range, index) => range[0][index]
                : (range, index) => range[index][0];
            const rangeLen = nbCol === 1 ? range[0].length : range.length;
            _searchType = Math.sign(_searchType);
            switch (_searchType) {
                case 1:
                    index = dichotomicSearch(range, searchKey, "nextSmaller", "asc", rangeLen, getElement);
                    break;
                case 0:
                    index = linearSearch(range, searchKey, "strict", rangeLen, getElement);
                    break;
                case -1:
                    index = dichotomicSearch(range, searchKey, "nextGreater", "desc", rangeLen, getElement);
                    break;
            }
            assertAvailable(nbCol === 1 ? range[0][index] : range[index], searchKey);
            return index + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ROW
    // -----------------------------------------------------------------------------
    const ROW = {
        description: _t("Row number of a specified cell."),
        args: [
            arg("cell_reference (meta, default='this cell')", _t("The cell whose row number will be returned. By default, this function uses the cell in which the formula is entered.")),
        ],
        returns: ["NUMBER"],
        compute: function (cellReference) {
            cellReference = cellReference || this.__originCellXC?.();
            assert(() => !!cellReference, "In this context, the function [[FUNCTION_NAME]] needs to have a cell or range in parameter.");
            const zone = toZone(cellReference);
            return zone.top + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // ROWS
    // -----------------------------------------------------------------------------
    const ROWS = {
        description: _t("Number of rows in a specified array or range."),
        args: [arg("range (meta)", _t("The range whose row count will be returned."))],
        returns: ["NUMBER"],
        compute: function (range) {
            const zone = toZone(range);
            return zone.bottom - zone.top + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // VLOOKUP
    // -----------------------------------------------------------------------------
    const VLOOKUP = {
        description: _t("Vertical lookup."),
        args: [
            arg("search_key (any)", _t("The value to search for. For example, 42, 'Cats', or I24.")),
            arg("range (any, range)", _t("The range to consider for the search. The first column in the range is searched for the key specified in search_key.")),
            arg("index (number)", _t("The column index of the value to be returned, where the first column in range is numbered 1.")),
            arg(`is_sorted (boolean, default=${DEFAULT_IS_SORTED})`, _t("Indicates whether the column to be searched (the first column of the specified range) is sorted, in which case the closest match for search_key will be returned.")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (searchKey, range, index, isSorted = { value: DEFAULT_IS_SORTED }) {
            const _index = Math.trunc(toNumber(index?.value, this.locale));
            assert(() => 1 <= _index && _index <= range.length, _t("[[FUNCTION_NAME]] evaluates to an out of bounds range."));
            const getValueFromRange = (range, index) => range[0][index].value;
            const _isSorted = toBoolean(isSorted.value);
            const rowIndex = _isSorted
                ? dichotomicSearch(range, searchKey?.value, "nextSmaller", "asc", range[0].length, getValueFromRange)
                : linearSearch(range, searchKey?.value, "strict", range[0].length, getValueFromRange);
            const value = range[_index - 1][rowIndex];
            assertAvailable(value, searchKey);
            return value;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // XLOOKUP
    // -----------------------------------------------------------------------------
    const XLOOKUP = {
        description: _t("Search a range for a match and return the corresponding item from a second range."),
        args: [
            arg("search_key (any)", _t("The value to search for.")),
            arg("lookup_range (any, range)", _t("The range to consider for the search. Should be a single column or a single row.")),
            arg("return_range (any, range)", _t("The range containing the return value. Should have the same dimensions as lookup_range.")),
            arg("if_not_found (any, lazy, optional)", _t("If a valid match is not found, return this value.")),
            arg(`match_mode (any, default=${DEFAULT_MATCH_MODE})`, _t("(0) Exact match. (-1) Return next smaller item if no match. (1) Return next greater item if no match.")),
            arg(`search_mode (any, default=${DEFAULT_SEARCH_MODE})`, _t("(1) Search starting at first item. \
      (-1) Search starting at last item. \
      (2) Perform a binary search that relies on lookup_array being sorted in ascending order. If not sorted, invalid results will be returned. \
      (-2) Perform a binary search that relies on lookup_array being sorted in descending order. If not sorted, invalid results will be returned.\
      ")),
        ],
        returns: ["ANY"],
        computeValueAndFormat: function (searchKey, lookupRange, returnRange, defaultValue, matchMode = { value: DEFAULT_MATCH_MODE }, searchMode = { value: DEFAULT_SEARCH_MODE }) {
            const _matchMode = Math.trunc(toNumber(matchMode.value, this.locale));
            const _searchMode = Math.trunc(toNumber(searchMode.value, this.locale));
            assert(() => lookupRange.length === 1 || lookupRange[0].length === 1, _t("lookup_range should be either a single row or single column."));
            assert(() => [-1, 1, -2, 2].includes(_searchMode), _t("searchMode should be a value in [-1, 1, -2, 2]."));
            assert(() => [-1, 0, 1].includes(_matchMode), _t("matchMode should be a value in [-1, 0, 1]."));
            const lookupDirection = lookupRange.length === 1 ? "col" : "row";
            assert(() => lookupDirection === "col"
                ? returnRange[0].length === lookupRange[0].length
                : returnRange.length === lookupRange.length, _t("return_range should have the same dimensions as lookup_range."));
            const getElement = lookupDirection === "col"
                ? (range, index) => range[0][index].value
                : (range, index) => range[index][0].value;
            const rangeLen = lookupDirection === "col" ? lookupRange[0].length : lookupRange.length;
            const mode = _matchMode === 0 ? "strict" : _matchMode === 1 ? "nextGreater" : "nextSmaller";
            const reverseSearch = _searchMode === -1;
            const index = _searchMode === 2 || _searchMode === -2
                ? dichotomicSearch(lookupRange, searchKey?.value, mode, _searchMode === 2 ? "asc" : "desc", rangeLen, getElement)
                : linearSearch(lookupRange, searchKey?.value, mode, rangeLen, getElement, reverseSearch);
            if (index !== -1) {
                return lookupDirection === "col"
                    ? returnRange.map((col) => [col[index]])
                    : [returnRange[index]];
            }
            const _defaultValue = defaultValue?.();
            assertAvailable(_defaultValue, searchKey);
            return [[_defaultValue]];
        },
        isExported: true,
    };

    var lookup = /*#__PURE__*/Object.freeze({
        __proto__: null,
        ADDRESS: ADDRESS,
        COLUMN: COLUMN,
        COLUMNS: COLUMNS,
        HLOOKUP: HLOOKUP,
        INDEX: INDEX,
        LOOKUP: LOOKUP,
        MATCH: MATCH,
        ROW: ROW,
        ROWS: ROWS,
        VLOOKUP: VLOOKUP,
        XLOOKUP: XLOOKUP
    });

    // -----------------------------------------------------------------------------
    // ADD
    // -----------------------------------------------------------------------------
    const ADD = {
        description: _t("Sum of two numbers."),
        args: [
            arg("value1 (number)", _t("The first addend.")),
            arg("value2 (number)", _t("The second addend.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1, value2) => value1?.format || value2?.format,
        compute: function (value1, value2) {
            return toNumber(value1, this.locale) + toNumber(value2, this.locale);
        },
    };
    // -----------------------------------------------------------------------------
    // CONCAT
    // -----------------------------------------------------------------------------
    const CONCAT = {
        description: _t("Concatenation of two values."),
        args: [
            arg("value1 (string)", _t("The value to which value2 will be appended.")),
            arg("value2 (string)", _t("The value to append to value1.")),
        ],
        returns: ["STRING"],
        compute: function (value1, value2) {
            return toString(value1) + toString(value2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // DIVIDE
    // -----------------------------------------------------------------------------
    const DIVIDE = {
        description: _t("One number divided by another."),
        args: [
            arg("dividend (number)", _t("The number to be divided.")),
            arg("divisor (number)", _t("The number to divide by.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (dividend, divisor) => dividend?.format || divisor?.format,
        compute: function (dividend, divisor) {
            const _divisor = toNumber(divisor, this.locale);
            assert(() => _divisor !== 0, _t("The divisor must be different from zero."));
            return toNumber(dividend, this.locale) / _divisor;
        },
    };
    // -----------------------------------------------------------------------------
    // EQ
    // -----------------------------------------------------------------------------
    function isEmpty(value) {
        return value === null || value === undefined;
    }
    const getNeutral = { number: 0, string: "", boolean: false };
    const EQ = {
        description: _t("Equal."),
        args: [
            arg("value1 (any)", _t("The first value.")),
            arg("value2 (any)", _t("The value to test against value1 for equality.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            value1 = isEmpty(value1) ? getNeutral[typeof value2] : value1;
            value2 = isEmpty(value2) ? getNeutral[typeof value1] : value2;
            if (typeof value1 === "string") {
                value1 = value1.toUpperCase();
            }
            if (typeof value2 === "string") {
                value2 = value2.toUpperCase();
            }
            return value1 === value2;
        },
    };
    // -----------------------------------------------------------------------------
    // GT
    // -----------------------------------------------------------------------------
    function applyRelationalOperator(value1, value2, cb) {
        value1 = isEmpty(value1) ? getNeutral[typeof value2] : value1;
        value2 = isEmpty(value2) ? getNeutral[typeof value1] : value2;
        if (typeof value1 !== "number") {
            value1 = toString(value1).toUpperCase();
        }
        if (typeof value2 !== "number") {
            value2 = toString(value2).toUpperCase();
        }
        const tV1 = typeof value1;
        const tV2 = typeof value2;
        if (tV1 === "string" && tV2 === "number") {
            return true;
        }
        if (tV2 === "string" && tV1 === "number") {
            return false;
        }
        return cb(value1, value2);
    }
    const GT = {
        description: _t("Strictly greater than."),
        args: [
            arg("value1 (any)", _t("The value to test as being greater than value2.")),
            arg("value2 (any)", _t("The second value.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return applyRelationalOperator(value1, value2, (v1, v2) => {
                return v1 > v2;
            });
        },
    };
    // -----------------------------------------------------------------------------
    // GTE
    // -----------------------------------------------------------------------------
    const GTE = {
        description: _t("Greater than or equal to."),
        args: [
            arg("value1 (any)", _t("The value to test as being greater than or equal to value2.")),
            arg("value2 (any)", _t("The second value.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return applyRelationalOperator(value1, value2, (v1, v2) => {
                return v1 >= v2;
            });
        },
    };
    // -----------------------------------------------------------------------------
    // LT
    // -----------------------------------------------------------------------------
    const LT = {
        description: _t("Less than."),
        args: [
            arg("value1 (any)", _t("The value to test as being less than value2.")),
            arg("value2 (any)", _t("The second value.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return !GTE.compute.bind(this)(value1, value2);
        },
    };
    // -----------------------------------------------------------------------------
    // LTE
    // -----------------------------------------------------------------------------
    const LTE = {
        description: _t("Less than or equal to."),
        args: [
            arg("value1 (any)", _t("The value to test as being less than or equal to value2.")),
            arg("value2 (any)", _t("The second value.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return !GT.compute.bind(this)(value1, value2);
        },
    };
    // -----------------------------------------------------------------------------
    // MINUS
    // -----------------------------------------------------------------------------
    const MINUS = {
        description: _t("Difference of two numbers."),
        args: [
            arg("value1 (number)", _t("The minuend, or number to be subtracted from.")),
            arg("value2 (number)", _t("The subtrahend, or number to subtract from value1.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (value1, value2) => value1?.format || value2?.format,
        compute: function (value1, value2) {
            return toNumber(value1, this.locale) - toNumber(value2, this.locale);
        },
    };
    // -----------------------------------------------------------------------------
    // MULTIPLY
    // -----------------------------------------------------------------------------
    const MULTIPLY = {
        description: _t("Product of two numbers"),
        args: [
            arg("factor1 (number)", _t("The first multiplicand.")),
            arg("factor2 (number)", _t("The second multiplicand.")),
        ],
        returns: ["NUMBER"],
        computeFormat: (factor1, factor2) => factor1?.format || factor2?.format,
        compute: function (factor1, factor2) {
            return toNumber(factor1, this.locale) * toNumber(factor2, this.locale);
        },
    };
    // -----------------------------------------------------------------------------
    // NE
    // -----------------------------------------------------------------------------
    const NE = {
        description: _t("Not equal."),
        args: [
            arg("value1 (any)", _t("The first value.")),
            arg("value2 (any)", _t("The value to test against value1 for inequality.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (value1, value2) {
            return !EQ.compute.bind(this)(value1, value2);
        },
    };
    // -----------------------------------------------------------------------------
    // POW
    // -----------------------------------------------------------------------------
    const POW = {
        description: _t("A number raised to a power."),
        args: [
            arg("base (number)", _t("The number to raise to the exponent power.")),
            arg("exponent (number)", _t("The exponent to raise base to.")),
        ],
        returns: ["NUMBER"],
        compute: function (base, exponent) {
            return POWER.compute.bind(this)(base, exponent);
        },
    };
    // -----------------------------------------------------------------------------
    // UMINUS
    // -----------------------------------------------------------------------------
    const UMINUS = {
        description: _t("A number with the sign reversed."),
        args: [
            arg("value (number)", _t("The number to have its sign reversed. Equivalently, the number to multiply by -1.")),
        ],
        computeFormat: (value) => value?.format,
        returns: ["NUMBER"],
        compute: function (value) {
            return -toNumber(value, this.locale);
        },
    };
    // -----------------------------------------------------------------------------
    // UNARY_PERCENT
    // -----------------------------------------------------------------------------
    const UNARY_PERCENT = {
        description: _t("Value interpreted as a percentage."),
        args: [arg("percentage (number)", _t("The value to interpret as a percentage."))],
        returns: ["NUMBER"],
        compute: function (percentage) {
            return toNumber(percentage, this.locale) / 100;
        },
    };
    // -----------------------------------------------------------------------------
    // UPLUS
    // -----------------------------------------------------------------------------
    const UPLUS = {
        description: _t("A specified number, unchanged."),
        args: [arg("value (any)", _t("The number to return."))],
        returns: ["ANY"],
        computeFormat: (value) => value?.format,
        compute: function (value = "") {
            return value === null ? "" : value;
        },
    };

    var operators = /*#__PURE__*/Object.freeze({
        __proto__: null,
        ADD: ADD,
        CONCAT: CONCAT,
        DIVIDE: DIVIDE,
        EQ: EQ,
        GT: GT,
        GTE: GTE,
        LT: LT,
        LTE: LTE,
        MINUS: MINUS,
        MULTIPLY: MULTIPLY,
        NE: NE,
        POW: POW,
        UMINUS: UMINUS,
        UNARY_PERCENT: UNARY_PERCENT,
        UPLUS: UPLUS
    });

    const DEFAULT_STARTING_AT = 1;
    /** Regex matching all the words in a string */
    const wordRegex = /[A-Za-zÀ-ÖØ-öø-ÿ]+/g;
    // -----------------------------------------------------------------------------
    // CHAR
    // -----------------------------------------------------------------------------
    const CHAR = {
        description: _t("Gets character associated with number."),
        args: [
            arg("table_number (number)", _t("The number of the character to look up from the current Unicode table in decimal format.")),
        ],
        returns: ["STRING"],
        compute: function (tableNumber) {
            const _tableNumber = Math.trunc(toNumber(tableNumber, this.locale));
            assert(() => _tableNumber >= 1, _t("The table_number (%s) is out of range.", _tableNumber.toString()));
            return String.fromCharCode(_tableNumber);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CLEAN
    // -----------------------------------------------------------------------------
    const CLEAN = {
        description: _t("Remove non-printable characters from a piece of text."),
        args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
        returns: ["STRING"],
        compute: function (text) {
            const _text = toString(text);
            let cleanedStr = "";
            for (const char of _text) {
                if (char && char.charCodeAt(0) > 31) {
                    cleanedStr += char;
                }
            }
            return cleanedStr;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // CONCATENATE
    // -----------------------------------------------------------------------------
    const CONCATENATE = {
        description: _t("Appends strings to one another."),
        args: [
            arg("string1 (string, range<string>)", _t("The initial string.")),
            arg("string2 (string, range<string>, repeating)", _t("More strings to append in sequence.")),
        ],
        returns: ["STRING"],
        compute: function (...values) {
            return reduceAny(values, (acc, a) => acc + toString(a), "");
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // EXACT
    // -----------------------------------------------------------------------------
    const EXACT = {
        description: _t("Tests whether two strings are identical."),
        args: [
            arg("string1 (string)", _t("The first string to compare.")),
            arg("string2 (string)", _t("The second string to compare.")),
        ],
        returns: ["BOOLEAN"],
        compute: function (string1, string2) {
            return toString(string1) === toString(string2);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // FIND
    // -----------------------------------------------------------------------------
    const FIND = {
        description: _t("First position of string found in text, case-sensitive."),
        args: [
            arg("search_for (string)", _t("The string to look for within text_to_search.")),
            arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")),
            arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")),
        ],
        returns: ["NUMBER"],
        compute: function (searchFor, textToSearch, startingAt = DEFAULT_STARTING_AT) {
            const _searchFor = toString(searchFor);
            const _textToSearch = toString(textToSearch);
            const _startingAt = toNumber(startingAt, this.locale);
            assert(() => _textToSearch !== "", _t("The text_to_search must be non-empty."));
            assert(() => _startingAt >= 1, _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString()));
            const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
            assert(() => result >= 0, _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor.toString(), _textToSearch));
            return result + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // JOIN
    // -----------------------------------------------------------------------------
    const JOIN = {
        description: _t("Concatenates elements of arrays with delimiter."),
        args: [
            arg("delimiter (string)", _t("The character or string to place between each concatenated value.")),
            arg("value_or_array1 (string, range<string>)", _t("The value or values to be appended using delimiter.")),
            arg("value_or_array2 (string, range<string>, repeating)", _t("More values to be appended using delimiter.")),
        ],
        returns: ["STRING"],
        compute: function (delimiter, ...valuesOrArrays) {
            const _delimiter = toString(delimiter);
            return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "");
        },
    };
    // -----------------------------------------------------------------------------
    // LEFT
    // -----------------------------------------------------------------------------
    const LEFT = {
        description: _t("Substring from beginning of specified string."),
        args: [
            arg("text (string)", _t("The string from which the left portion will be returned.")),
            arg("number_of_characters (number, optional)", _t("The number of characters to return from the left side of string.")),
        ],
        returns: ["STRING"],
        compute: function (text, ...args) {
            const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
            assert(() => _numberOfCharacters >= 0, _t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters.toString()));
            return toString(text).substring(0, _numberOfCharacters);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LEN
    // -----------------------------------------------------------------------------
    const LEN = {
        description: _t("Length of a string."),
        args: [arg("text (string)", _t("The string whose length will be returned."))],
        returns: ["NUMBER"],
        compute: function (text) {
            return toString(text).length;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // LOWER
    // -----------------------------------------------------------------------------
    const LOWER = {
        description: _t("Converts a specified string to lowercase."),
        args: [arg("text (string)", _t("The string to convert to lowercase."))],
        returns: ["STRING"],
        compute: function (text) {
            return toString(text).toLowerCase();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // MID
    // -----------------------------------------------------------------------------
    const MID = {
        description: _t("A segment of a string."),
        args: [
            arg("text (string)", _t("The string to extract a segment from.")),
            arg("starting_at (number)", _t("The index from the left of string from which to begin extracting. The first character in string has the index 1.")),
            arg("extract_length (number)", _t("The length of the segment to extract.")),
        ],
        returns: ["STRING"],
        compute: function (text, starting_at, extract_length) {
            const _text = toString(text);
            const _starting_at = toNumber(starting_at, this.locale);
            const _extract_length = toNumber(extract_length, this.locale);
            assert(() => _starting_at >= 1, _t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
            assert(() => _extract_length >= 0, _t("The extract_length argument (%s) must be positive or null.", _extract_length.toString()));
            return _text.slice(_starting_at - 1, _starting_at + _extract_length - 1);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // PROPER
    // -----------------------------------------------------------------------------
    const PROPER = {
        description: _t("Capitalizes each word in a specified string."),
        args: [
            arg("text_to_capitalize (string)", _t("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase.")),
        ],
        returns: ["STRING"],
        compute: function (text) {
            const _text = toString(text);
            return _text.replace(wordRegex, (word) => {
                return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
            });
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // REPLACE
    // -----------------------------------------------------------------------------
    const REPLACE = {
        description: _t("Replaces part of a text string with different text."),
        args: [
            arg("text (string)", _t("The text, a part of which will be replaced.")),
            arg("position (number)", _t("The position where the replacement will begin (starting from 1).")),
            arg("length (number)", _t("The number of characters in the text to be replaced.")),
            arg("new_text (string)", _t("The text which will be inserted into the original text.")),
        ],
        returns: ["STRING"],
        compute: function (text, position, length, newText) {
            const _position = toNumber(position, this.locale);
            assert(() => _position >= 1, _t("The position (%s) must be greater than or equal to 1.", _position.toString()));
            const _text = toString(text);
            const _length = toNumber(length, this.locale);
            const _newText = toString(newText);
            return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // RIGHT
    // -----------------------------------------------------------------------------
    const RIGHT = {
        description: _t("A substring from the end of a specified string."),
        args: [
            arg("text (string)", _t("The string from which the right portion will be returned.")),
            arg("number_of_characters (number, optional)", _t("The number of characters to return from the right side of string.")),
        ],
        returns: ["STRING"],
        compute: function (text, ...args) {
            const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
            assert(() => _numberOfCharacters >= 0, _t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters.toString()));
            const _text = toString(text);
            const stringLength = _text.length;
            return _text.substring(stringLength - _numberOfCharacters, stringLength);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SEARCH
    // -----------------------------------------------------------------------------
    const SEARCH = {
        description: _t("First position of string found in text, ignoring case."),
        args: [
            arg("search_for (string)", _t("The string to look for within text_to_search.")),
            arg("text_to_search (string)", _t("The text to search for the first occurrence of search_for.")),
            arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search.")),
        ],
        returns: ["NUMBER"],
        compute: function (searchFor, textToSearch, startingAt = DEFAULT_STARTING_AT) {
            const _searchFor = toString(searchFor).toLowerCase();
            const _textToSearch = toString(textToSearch).toLowerCase();
            const _startingAt = toNumber(startingAt, this.locale);
            assert(() => _textToSearch !== "", _t("The text_to_search must be non-empty."));
            assert(() => _startingAt >= 1, _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString()));
            const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
            assert(() => result >= 0, _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch));
            return result + 1;
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SPLIT
    // -----------------------------------------------------------------------------
    const SPLIT_DEFAULT_SPLIT_BY_EACH = true;
    const SPLIT_DEFAULT_REMOVE_EMPTY_TEXT = true;
    const SPLIT = {
        description: _t("Split text by specific character delimiter(s)."),
        args: [
            arg("text (string)", _t("The text to divide.")),
            arg("delimiter (string)", _t("The character or characters to use to split text.")),
            arg(`split_by_each (boolean, default=${SPLIT_DEFAULT_SPLIT_BY_EACH}})`, _t("Whether or not to divide text around each character contained in delimiter.")),
            arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t("Whether or not to remove empty text messages from the split results. The default behavior is to treat \
        consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters.")),
        ],
        returns: ["RANGE<STRING>"],
        compute: function (text, delimiter, splitByEach = SPLIT_DEFAULT_SPLIT_BY_EACH, removeEmptyText = SPLIT_DEFAULT_REMOVE_EMPTY_TEXT) {
            const _text = toString(text);
            const _delimiter = escapeRegExp(toString(delimiter));
            const _splitByEach = toBoolean(splitByEach);
            const _removeEmptyText = toBoolean(removeEmptyText);
            assert(() => _delimiter.length > 0, _t("The _delimiter (%s) must be not be empty.", _delimiter));
            const regex = _splitByEach ? new RegExp(`[${_delimiter}]`, "g") : new RegExp(_delimiter, "g");
            let result = _text.split(regex);
            if (_removeEmptyText) {
                result = result.filter((text) => text !== "");
            }
            return transposeMatrix([result]);
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // SUBSTITUTE
    // -----------------------------------------------------------------------------
    const SUBSTITUTE = {
        description: _t("Replaces existing text with new text in a string."),
        args: [
            arg("text_to_search (string)", _t("The text within which to search and replace.")),
            arg("search_for (string)", _t("The string to search for within text_to_search.")),
            arg("replace_with (string)", _t("The string that will replace search_for.")),
            arg("occurrence_number (number, optional)", _t("The instance of search_for within text_to_search to replace with replace_with. By default, all occurrences of search_for are replaced; however, if occurrence_number is specified, only the indicated instance of search_for is replaced.")),
        ],
        returns: ["NUMBER"],
        compute: function (textToSearch, searchFor, replaceWith, occurrenceNumber) {
            const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
            assert(() => _occurrenceNumber >= 0, _t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber.toString()));
            const _textToSearch = toString(textToSearch);
            const _searchFor = toString(searchFor);
            if (_searchFor === "") {
                return _textToSearch;
            }
            const _replaceWith = toString(replaceWith);
            const reg = new RegExp(escapeRegExp(_searchFor), "g");
            if (_occurrenceNumber === 0) {
                return _textToSearch.replace(reg, _replaceWith);
            }
            let n = 0;
            return _textToSearch.replace(reg, (text) => (++n === _occurrenceNumber ? _replaceWith : text));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TEXTJOIN
    // -----------------------------------------------------------------------------
    const TEXTJOIN = {
        description: _t("Combines text from multiple strings and/or arrays."),
        args: [
            arg("delimiter (string)", _t(" A string, possible empty, or a reference to a valid string. If empty, the text will be simply concatenated.")),
            arg("ignore_empty (boolean)", _t("A boolean; if TRUE, empty cells selected in the text arguments won't be included in the result.")),
            arg("text1 (string, range<string>)", _t("Any text item. This could be a string, or an array of strings in a range.")),
            arg("text2 (string, range<string>, repeating)", _t("Additional text item(s).")),
        ],
        returns: ["STRING"],
        compute: function (delimiter, ignoreEmpty, ...textsOrArrays) {
            const _delimiter = toString(delimiter);
            const _ignoreEmpty = toBoolean(ignoreEmpty);
            let n = 0;
            return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "");
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TRIM
    // -----------------------------------------------------------------------------
    const TRIM = {
        description: _t("Removes space characters."),
        args: [
            arg("text (string)", _t("The text or reference to a cell containing text to be trimmed.")),
        ],
        returns: ["STRING"],
        compute: function (text) {
            return trimContent(toString(text));
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // UPPER
    // -----------------------------------------------------------------------------
    const UPPER = {
        description: _t("Converts a specified string to uppercase."),
        args: [arg("text (string)", _t("The string to convert to uppercase."))],
        returns: ["STRING"],
        compute: function (text) {
            return toString(text).toUpperCase();
        },
        isExported: true,
    };
    // -----------------------------------------------------------------------------
    // TEXT
    // -----------------------------------------------------------------------------
    const TEXT = {
        description: _t("Converts a number to text according to a specified format."),
        args: [
            arg("number (number)", _t("The number, date or time to format.")),
            arg("format (string)", _t("The pattern by which to format the number, enclosed in quotation marks.")),
        ],
        returns: ["STRING"],
        compute: function (number, format) {
            const _number = toNumber(number, this.locale);
            return formatValue(_number, { format: toString(format), locale: this.locale });
        },
        isExported: true,
    };

    var text = /*#__PURE__*/Object.freeze({
        __proto__: null,
        CHAR: CHAR,
        CLEAN: CLEAN,
        CONCATENATE: CONCATENATE,
        EXACT: EXACT,
        FIND: FIND,
        JOIN: JOIN,
        LEFT: LEFT,
        LEN: LEN,
        LOWER: LOWER,
        MID: MID,
        PROPER: PROPER,
        REPLACE: REPLACE,
        RIGHT: RIGHT,
        SEARCH: SEARCH,
        SPLIT: SPLIT,
        SUBSTITUTE: SUBSTITUTE,
        TEXT: TEXT,
        TEXTJOIN: TEXTJOIN,
        TRIM: TRIM,
        UPPER: UPPER
    });

    // -----------------------------------------------------------------------------
    // HYPERLINK
    // -----------------------------------------------------------------------------
    const HYPERLINK = {
        description: _t("Creates a hyperlink in a cell."),
        args: [
            arg("url (string)", _t("The full URL of the link enclosed in quotation marks.")),
            arg("link_label (string, optional)", _t("The text to display in the cell, enclosed in quotation marks.")),
        ],
        returns: ["STRING"],
        compute: function (url, linkLabel) {
            const processedUrl = toString(url).trim();
            const processedLabel = toString(linkLabel) || processedUrl;
            if (processedUrl === "")
                return processedLabel;
            return markdownLink(processedLabel, processedUrl);
        },
        isExported: true,
    };

    var web = /*#__PURE__*/Object.freeze({
        __proto__: null,
        HYPERLINK: HYPERLINK
    });

    const categories = [
        { name: _t("Array"), functions: array },
        { name: _t("Database"), functions: database },
        { name: _t("Date"), functions: date },
        { name: _t("Filter"), functions: filter },
        { name: _t("Financial"), functions: financial },
        { name: _t("Info"), functions: info },
        { name: _t("Lookup"), functions: lookup },
        { name: _t("Logical"), functions: logical },
        { name: _t("Math"), functions: math },
        { name: _t("Misc"), functions: misc },
        { name: _t("Operator"), functions: operators },
        { name: _t("Statistical"), functions: statistical },
        { name: _t("Text"), functions: text },
        { name: _t("Engineering"), functions: engineering },
        { name: _t("Web"), functions: web },
    ];
    const functionNameRegex = /^[A-Z0-9\_\.]+$/;
    //------------------------------------------------------------------------------
    // Function registry
    //------------------------------------------------------------------------------
    class FunctionRegistry extends Registry {
        mapping = {};
        add(name, addDescr) {
            name = name.toUpperCase();
            if (!functionNameRegex.test(name)) {
                throw new Error(_t("Invalid function name %s. Function names can exclusively contain alphanumerical values separated by dots (.) or underscore (_)", name));
            }
            const descr = addMetaInfoFromArg(addDescr);
            validateArguments(descr.args);
            this.mapping[name] = createComputeFunctionFromDescription(descr);
            super.add(name, descr);
            return this;
        }
    }
    function createComputeFunctionFromDescription(descr) {
        const computeValueAndFormat = "computeValueAndFormat" in descr;
        const computeValue = "compute" in descr;
        const computeFormat = "computeFormat" in descr;
        if (!computeValueAndFormat && !computeValue) {
            throw new Error("Invalid function description, need at least one 'compute' or 'computeValueAndFormat' function");
        }
        if (computeValueAndFormat && (computeValue || computeFormat)) {
            throw new Error("Invalid function description, cannot have both 'computeValueAndFormat' and 'compute'/'computeFormat' functions");
        }
        if (computeValueAndFormat) {
            return descr.computeValueAndFormat;
        }
        // case computeValue
        return buildComputeFunctionFromDescription(descr);
    }
    function buildComputeFunctionFromDescription(descr) {
        return function (...args) {
            const computeValue = descr.compute.bind(this);
            const computeFormat = descr.computeFormat?.bind(this) || (() => undefined);
            const value = computeValue(...extractArgValuesFromArgs(args));
            const format = computeFormat(...args);
            if (isMatrix(value)) {
                if (format === undefined || isMatrix(format)) {
                    return value.map((col, i) => col.map((row, j) => ({ value: row, format: format?.[i]?.[j] })));
                }
            }
            else {
                if (format === undefined || !isMatrix(format)) {
                    return { value, format };
                }
            }
            throw new Error("A format matrix should never be associated with a scalar value");
        };
    }
    function extractArgValuesFromArgs(args) {
        return args.map((arg) => {
            if (arg === undefined) {
                return undefined;
            }
            if (typeof arg === "function") {
                return () => extractArgValueFromArg(arg());
            }
            return extractArgValueFromArg(arg);
        });
    }
    function extractArgValueFromArg(arg) {
        if (isMatrix(arg)) {
            return matrixMap(arg, (data) => data.value);
        }
        return arg?.value;
    }
    const functionRegistry = new FunctionRegistry();
    for (let category of categories) {
        const fns = category.functions;
        for (let name in fns) {
            const addDescr = fns[name];
            addDescr.category = addDescr.category || category.name;
            name = name.replace(/_/g, ".");
            functionRegistry.add(name, { isExported: false, ...addDescr });
        }
    }

    const insertRow = {
        name: (env) => {
            const number = getRowsNumber(env);
            return number === 1 ? _t("Insert row") : _t("Insert %s rows", number.toString());
        },
        isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
        icon: "o-spreadsheet-Icon.INSERT_ROW",
    };
    const rowInsertRowBefore = {
        name: (env) => {
            const number = getRowsNumber(env);
            return number === 1 ? _t("Insert row above") : _t("Insert %s rows above", number.toString());
        },
        execute: INSERT_ROWS_BEFORE_ACTION,
        isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
        icon: "o-spreadsheet-Icon.INSERT_ROW_BEFORE",
    };
    const topBarInsertRowsBefore = {
        ...rowInsertRowBefore,
        name: (env) => {
            const number = getRowsNumber(env);
            if (number === 1) {
                return _t("Row above");
            }
            return _t("%s Rows above", number.toString());
        },
    };
    const cellInsertRowsBefore = {
        ...rowInsertRowBefore,
        name: (env) => {
            const number = getRowsNumber(env);
            if (number === 1) {
                return _t("Insert row");
            }
            return _t("Insert %s rows", number.toString());
        },
        isVisible: IS_ONLY_ONE_RANGE,
        icon: "o-spreadsheet-Icon.INSERT_ROW_BEFORE",
    };
    const rowInsertRowsAfter = {
        execute: INSERT_ROWS_AFTER_ACTION,
        name: (env) => {
            const number = getRowsNumber(env);
            return number === 1 ? _t("Insert row below") : _t("Insert %s rows below", number.toString());
        },
        isVisible: (env) => CAN_INSERT_HEADER(env, "ROW"),
        icon: "o-spreadsheet-Icon.INSERT_ROW_AFTER",
    };
    const topBarInsertRowsAfter = {
        ...rowInsertRowsAfter,
        name: (env) => {
            const number = getRowsNumber(env);
            if (number === 1) {
                return _t("Row below");
            }
            return _t("%s Rows below", number.toString());
        },
    };
    const insertCol = {
        name: (env) => {
            const number = getColumnsNumber(env);
            return number === 1 ? _t("Insert column") : _t("Insert %s columns", number.toString());
        },
        isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
        icon: "o-spreadsheet-Icon.INSERT_COL",
    };
    const colInsertColsBefore = {
        name: (env) => {
            const number = getColumnsNumber(env);
            return number === 1
                ? _t("Insert column left")
                : _t("Insert %s columns left", number.toString());
        },
        execute: INSERT_COLUMNS_BEFORE_ACTION,
        isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
        icon: "o-spreadsheet-Icon.INSERT_COL_BEFORE",
    };
    const topBarInsertColsBefore = {
        ...colInsertColsBefore,
        name: (env) => {
            const number = getColumnsNumber(env);
            if (number === 1) {
                return _t("Column left");
            }
            return _t("%s Columns left", number.toString());
        },
    };
    const cellInsertColsBefore = {
        ...colInsertColsBefore,
        name: (env) => {
            const number = getColumnsNumber(env);
            if (number === 1) {
                return _t("Insert column");
            }
            return _t("Insert %s columns", number.toString());
        },
        isVisible: IS_ONLY_ONE_RANGE,
        icon: "o-spreadsheet-Icon.INSERT_COL_BEFORE",
    };
    const colInsertColsAfter = {
        name: (env) => {
            const number = getColumnsNumber(env);
            return number === 1
                ? _t("Insert column right")
                : _t("Insert %s columns right", number.toString());
        },
        execute: INSERT_COLUMNS_AFTER_ACTION,
        isVisible: (env) => CAN_INSERT_HEADER(env, "COL"),
        icon: "o-spreadsheet-Icon.INSERT_COL_AFTER",
    };
    const topBarInsertColsAfter = {
        ...colInsertColsAfter,
        name: (env) => {
            const number = getColumnsNumber(env);
            if (number === 1) {
                return _t("Column right");
            }
            return _t("%s Columns right", number.toString());
        },
        execute: INSERT_COLUMNS_AFTER_ACTION,
    };
    const insertCell = {
        name: _t("Insert cells"),
        isVisible: (env) => IS_ONLY_ONE_RANGE(env) &&
            env.model.getters.getActiveCols().size === 0 &&
            env.model.getters.getActiveRows().size === 0,
        icon: "o-spreadsheet-Icon.INSERT_CELL",
    };
    const insertCellShiftDown = {
        name: _t("Insert cells and shift down"),
        execute: (env) => {
            const zone = env.model.getters.getSelectedZone();
            const result = env.model.dispatch("INSERT_CELL", { zone, shiftDimension: "ROW" });
            handlePasteResult(env, result);
        },
        isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0,
        icon: "o-spreadsheet-Icon.INSERT_CELL_SHIFT_DOWN",
    };
    const insertCellShiftRight = {
        name: _t("Insert cells and shift right"),
        execute: (env) => {
            const zone = env.model.getters.getSelectedZone();
            const result = env.model.dispatch("INSERT_CELL", { zone, shiftDimension: "COL" });
            handlePasteResult(env, result);
        },
        isVisible: (env) => env.model.getters.getActiveRows().size === 0 && env.model.getters.getActiveCols().size === 0,
        icon: "o-spreadsheet-Icon.INSERT_CELL_SHIFT_RIGHT",
    };
    const insertChart = {
        name: _t("Chart"),
        execute: CREATE_CHART,
        icon: "o-spreadsheet-Icon.INSERT_CHART",
    };
    const insertImage = {
        name: _t("Image"),
        description: "Ctrl+O",
        execute: CREATE_IMAGE,
        isVisible: (env) => env.imageProvider !== undefined,
        icon: "o-spreadsheet-Icon.INSERT_IMAGE",
    };
    const insertFunction = {
        name: _t("Function"),
        icon: "o-spreadsheet-Icon.SHOW_HIDE_FORMULA",
    };
    const insertFunctionSum = {
        name: _t("SUM"),
        execute: (env) => env.startCellEdition(`=SUM(`),
    };
    const insertFunctionAverage = {
        name: _t("AVERAGE"),
        execute: (env) => env.startCellEdition(`=AVERAGE(`),
    };
    const insertFunctionCount = {
        name: _t("COUNT"),
        execute: (env) => env.startCellEdition(`=COUNT(`),
    };
    const insertFunctionMax = {
        name: _t("MAX"),
        execute: (env) => env.startCellEdition(`=MAX(`),
    };
    const insertFunctionMin = {
        name: _t("MIN"),
        execute: (env) => env.startCellEdition(`=MIN(`),
    };
    const categorieFunctionAll = {
        name: _t("All"),
        children: [allFunctionListMenuBuilder],
    };
    function allFunctionListMenuBuilder() {
        const fnNames = functionRegistry.getKeys();
        return createFormulaFunctions(fnNames);
    }
    const categoriesFunctionListMenuBuilder = () => {
        const functions = functionRegistry.content;
        const categories = [...new Set(functionRegistry.getAll().map((fn) => fn.category))].filter(isDefined$1);
        return categories.sort().map((category, i) => {
            const functionsInCategory = Object.keys(functions).filter((key) => functions[key].category === category);
            return {
                name: category,
                children: createFormulaFunctions(functionsInCategory),
            };
        });
    };
    const insertLink = {
        name: _t("Link"),
        execute: INSERT_LINK,
        icon: "o-spreadsheet-Icon.INSERT_LINK",
    };
    const insertSheet = {
        name: _t("Insert sheet"),
        execute: (env) => {
            const activeSheetId = env.model.getters.getActiveSheetId();
            const position = env.model.getters.getSheetIds().indexOf(activeSheetId) + 1;
            const sheetId = env.model.uuidGenerator.uuidv4();
            env.model.dispatch("CREATE_SHEET", { sheetId, position });
            env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: sheetId });
        },
        icon: "o-spreadsheet-Icon.INSERT_SHEET",
    };
    function createFormulaFunctions(fnNames) {
        return fnNames.sort().map((fnName, i) => {
            return {
                name: fnName,
                sequence: i * 10,
                execute: (env) => env.startCellEdition(`=${fnName}(`),
            };
        });
    }
    function getRowsNumber(env) {
        const activeRows = env.model.getters.getActiveRows();
        if (activeRows.size) {
            return activeRows.size;
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            return zone.bottom - zone.top + 1;
        }
    }
    function getColumnsNumber(env) {
        const activeCols = env.model.getters.getActiveCols();
        if (activeCols.size) {
            return activeCols.size;
        }
        else {
            const zone = env.model.getters.getSelectedZones()[0];
            return zone.right - zone.left + 1;
        }
    }

    //------------------------------------------------------------------------------
    // Context Menu Registry
    //------------------------------------------------------------------------------
    const cellMenuRegistry = new MenuItemRegistry();
    cellMenuRegistry
        .add("cut", {
        ...cut,
        sequence: 10,
    })
        .add("copy", {
        ...copy,
        sequence: 20,
    })
        .add("paste", {
        ...paste,
        sequence: 30,
    })
        .add("paste_special", {
        ...pasteSpecial,
        sequence: 40,
        separator: true,
    })
        .addChild("paste_value_only", ["paste_special"], {
        ...pasteSpecialValue,
        sequence: 10,
    })
        .addChild("paste_format_only", ["paste_special"], {
        ...pasteSpecialFormat,
        sequence: 20,
    })
        .add("add_row_before", {
        ...cellInsertRowsBefore,
        sequence: 70,
    })
        .add("add_column_before", {
        ...cellInsertColsBefore,
        sequence: 90,
    })
        .add("insert_cell", {
        ...insertCell,
        sequence: 100,
        separator: true,
    })
        .addChild("insert_cell_down", ["insert_cell"], {
        ...insertCellShiftDown,
        name: _t("Shift down"),
        sequence: 10,
    })
        .addChild("insert_cell_right", ["insert_cell"], {
        ...insertCellShiftRight,
        name: _t("Shift right"),
        sequence: 20,
    })
        .add("delete_row", {
        ...deleteRow,
        sequence: 110,
        icon: "o-spreadsheet-Icon.DELETE",
    })
        .add("delete_column", {
        ...deleteCol,
        sequence: 120,
        icon: "o-spreadsheet-Icon.DELETE",
    })
        .add("delete_cell", {
        ...deleteCells,
        sequence: 130,
        separator: true,
        icon: "o-spreadsheet-Icon.DELETE",
    })
        .addChild("delete_cell_up", ["delete_cell"], {
        ...deleteCellShiftUp,
        name: _t("Shift up"),
        sequence: 10,
        icon: "o-spreadsheet-Icon.DELETE_CELL_SHIFT_UP",
    })
        .addChild("delete_cell_left", ["delete_cell"], {
        ...deleteCellShiftLeft,
        name: _t("Shift left"),
        sequence: 20,
        icon: "o-spreadsheet-Icon.DELETE_CELL_SHIFT_LEFT",
    })
        .add("insert_link", {
        ...insertLink,
        name: INSERT_LINK_NAME,
        sequence: 150,
        separator: true,
    });

    const AddFilterInteractiveContent = {
        filterOverlap: _t("You cannot create overlapping filters."),
        nonContinuousTargets: _t("A filter can only be created on a continuous selection."),
        mergeInFilter: _t("You can't create a filter over a range that contains a merge."),
    };
    function interactiveAddFilter(env, sheetId, target) {
        const result = env.model.dispatch("CREATE_FILTER_TABLE", { target, sheetId });
        if (result.isCancelledBecause("FilterOverlap" /* CommandResult.FilterOverlap */)) {
            env.raiseError(AddFilterInteractiveContent.filterOverlap);
        }
        else if (result.isCancelledBecause("MergeInFilter" /* CommandResult.MergeInFilter */)) {
            env.raiseError(AddFilterInteractiveContent.mergeInFilter);
        }
        else if (result.isCancelledBecause("NonContinuousTargets" /* CommandResult.NonContinuousTargets */)) {
            env.raiseError(AddFilterInteractiveContent.nonContinuousTargets);
        }
    }

    const sortRange = {
        name: _t("Sort range"),
        isVisible: IS_ONLY_ONE_RANGE,
        icon: "o-spreadsheet-Icon.SORT_RANGE",
    };
    const sortAscending = {
        name: _t("Ascending (A ⟶ Z)"),
        execute: (env) => {
            const { anchor, zones } = env.model.getters.getSelection();
            const sheetId = env.model.getters.getActiveSheetId();
            interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "ascending");
        },
        icon: "o-spreadsheet-Icon.SORT_ASCENDING",
    };
    const dataCleanup = {
        name: _t("Data cleanup"),
        icon: "o-spreadsheet-Icon.DATA_CLEANUP",
    };
    const removeDuplicates = {
        name: _t("Remove duplicates"),
        execute: (env) => {
            if (getZoneArea(env.model.getters.getSelectedZone()) === 1) {
                env.model.selection.selectTableAroundSelection();
            }
            env.openSidePanel("RemoveDuplicates", {});
        },
    };
    const trimWhitespace = {
        name: _t("Trim whitespace"),
        execute: (env) => {
            env.model.dispatch("TRIM_WHITESPACE");
        },
    };
    const sortDescending = {
        name: _t("Descending (Z ⟶ A)"),
        execute: (env) => {
            const { anchor, zones } = env.model.getters.getSelection();
            const sheetId = env.model.getters.getActiveSheetId();
            interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "descending");
        },
        icon: "o-spreadsheet-Icon.SORT_DESCENDING",
    };
    const addDataFilter = {
        name: _t("Create filter"),
        execute: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            const selection = env.model.getters.getSelection().zones;
            interactiveAddFilter(env, sheetId, selection);
        },
        isVisible: (env) => !SELECTION_CONTAINS_FILTER(env),
        isEnabled: (env) => {
            const selectedZones = env.model.getters.getSelectedZones();
            return areZonesContinuous(...selectedZones);
        },
        icon: "o-spreadsheet-Icon.MENU_FILTER_ICON",
    };
    const removeDataFilter = {
        name: _t("Remove filter"),
        execute: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            env.model.dispatch("REMOVE_FILTER_TABLE", {
                sheetId,
                target: env.model.getters.getSelectedZones(),
            });
        },
        isVisible: SELECTION_CONTAINS_FILTER,
        icon: "o-spreadsheet-Icon.MENU_FILTER_ICON",
    };
    const splitToColumns = {
        name: _t("Split text to columns"),
        sequence: 1,
        execute: (env) => env.openSidePanel("SplitToColumns", {}),
        isEnabled: (env) => env.model.getters.isSingleColSelected(),
        icon: "o-spreadsheet-Icon.SPLIT_TEXT",
    };

    /**
     * Create a format action specification for a given format.
     * The format can be dynamically computed from the environment.
     */
    function createFormatActionSpec({ name, format, descriptionValue, }) {
        const formatCallback = typeof format === "function" ? format : () => format;
        return {
            name,
            description: (env) => formatValue(descriptionValue, {
                format: formatCallback(env),
                locale: env.model.getters.getLocale(),
            }),
            execute: (env) => setFormatter(env, formatCallback(env)),
            isActive: (env) => isFormatSelected(env, formatCallback(env)),
        };
    }
    const formatNumberAutomatic = {
        name: _t("Automatic"),
        execute: (env) => setFormatter(env, ""),
        isActive: (env) => isAutomaticFormatSelected(env),
    };
    const formatNumberNumber = createFormatActionSpec({
        name: _t("Number"),
        descriptionValue: 1000.12,
        format: "#,##0.00",
    });
    const formatPercent = {
        name: _t("Format as percent"),
        execute: FORMAT_PERCENT_ACTION,
        icon: "o-spreadsheet-Icon.PERCENT",
    };
    const formatNumberPercent = createFormatActionSpec({
        name: _t("Percent"),
        descriptionValue: 0.1012,
        format: "0.00%",
    });
    const formatNumberCurrency = createFormatActionSpec({
        name: _t("Currency"),
        descriptionValue: 1000.12,
        format: (env) => env.model.config.defaultCurrencyFormat ?? DEFAULT_CURRENCY_FORMAT,
    });
    const formatNumberCurrencyRounded = {
        ...createFormatActionSpec({
            name: _t("Currency rounded"),
            descriptionValue: 1000,
            format: (env) => roundFormat(env.model.config.defaultCurrencyFormat ?? DEFAULT_CURRENCY_FORMAT),
        }),
        isVisible: (env) => {
            const currencyFormat = env.model.config.defaultCurrencyFormat;
            return currencyFormat !== roundFormat(currencyFormat ?? DEFAULT_CURRENCY_FORMAT);
        },
    };
    const DEFAULT_CURRENCY_FORMAT = "[$$]#,##0.00";
    const EXAMPLE_DATE = parseLiteral("2023/09/26 10:43:00 PM", DEFAULT_LOCALE);
    const formatCustomCurrency = {
        name: _t("Custom currency"),
        isVisible: (env) => env.loadCurrencies !== undefined,
        execute: (env) => env.openSidePanel("CustomCurrency", {}),
    };
    const formatNumberDate = createFormatActionSpec({
        name: _t("Date"),
        descriptionValue: EXAMPLE_DATE,
        format: (env) => env.model.getters.getLocale().dateFormat,
    });
    const formatNumberTime = createFormatActionSpec({
        name: _t("Time"),
        descriptionValue: EXAMPLE_DATE,
        format: (env) => env.model.getters.getLocale().timeFormat,
    });
    const formatNumberDateTime = createFormatActionSpec({
        name: _t("Date time"),
        descriptionValue: EXAMPLE_DATE,
        format: (env) => {
            const locale = env.model.getters.getLocale();
            return getDateTimeFormat(locale);
        },
    });
    const formatNumberDuration = createFormatActionSpec({
        name: _t("Duration"),
        descriptionValue: "27:51:38",
        format: "hhhh:mm:ss",
    });
    const moreFormats = {
        name: _t("More date formats"),
        execute: (env) => env.openSidePanel("MoreFormats", {}),
    };
    const formatNumberFullDateTime = createFormatActionSpec({
        name: _t("Full date time"),
        format: "dddd d mmmm yyyy hh:mm:ss a",
        descriptionValue: EXAMPLE_DATE,
    });
    const formatNumberFullWeekDayAndMonth = createFormatActionSpec({
        name: _t("Full week day and month"),
        format: "dddd d mmmm yyyy",
        descriptionValue: EXAMPLE_DATE,
    });
    const formatNumberDayAndFullMonth = createFormatActionSpec({
        name: _t("Day and full month"),
        format: "d mmmm yyyy",
        descriptionValue: EXAMPLE_DATE,
    });
    const formatNumberShortWeekDay = createFormatActionSpec({
        name: _t("Short week day"),
        format: "ddd d mmm yyyy",
        descriptionValue: EXAMPLE_DATE,
    });
    const formatNumberDayAndShortMonth = createFormatActionSpec({
        name: _t("Day and short month"),
        format: "d mmm yyyy",
        descriptionValue: EXAMPLE_DATE,
    });
    const formatNumberFullMonth = createFormatActionSpec({
        name: _t("Full month"),
        format: "mmmm yyyy",
        descriptionValue: EXAMPLE_DATE,
    });
    const formatNumberShortMonth = createFormatActionSpec({
        name: _t("Short month"),
        format: "mmm yyyy",
        descriptionValue: EXAMPLE_DATE,
    });
    const incraseDecimalPlaces = {
        name: _t("Increase decimal places"),
        icon: "o-spreadsheet-Icon.INCREASE_DECIMAL",
        execute: (env) => env.model.dispatch("SET_DECIMAL", {
            sheetId: env.model.getters.getActiveSheetId(),
            target: env.model.getters.getSelectedZones(),
            step: 1,
        }),
    };
    const decraseDecimalPlaces = {
        name: _t("Decrease decimal places"),
        icon: "o-spreadsheet-Icon.DECRASE_DECIMAL",
        execute: (env) => env.model.dispatch("SET_DECIMAL", {
            sheetId: env.model.getters.getActiveSheetId(),
            target: env.model.getters.getSelectedZones(),
            step: -1,
        }),
    };
    const formatBold = {
        name: _t("Bold"),
        description: "Ctrl+B",
        execute: (env) => setStyle(env, { bold: !env.model.getters.getCurrentStyle().bold }),
        icon: "o-spreadsheet-Icon.BOLD",
        isActive: (env) => !!env.model.getters.getCurrentStyle().bold,
    };
    const formatItalic = {
        name: _t("Italic"),
        description: "Ctrl+I",
        execute: (env) => setStyle(env, { italic: !env.model.getters.getCurrentStyle().italic }),
        icon: "o-spreadsheet-Icon.ITALIC",
        isActive: (env) => !!env.model.getters.getCurrentStyle().italic,
    };
    const formatUnderline = {
        name: _t("Underline"),
        description: "Ctrl+U",
        execute: (env) => setStyle(env, { underline: !env.model.getters.getCurrentStyle().underline }),
        icon: "o-spreadsheet-Icon.UNDERLINE",
        isActive: (env) => !!env.model.getters.getCurrentStyle().underline,
    };
    const formatStrikethrough = {
        name: _t("Strikethrough"),
        execute: (env) => setStyle(env, { strikethrough: !env.model.getters.getCurrentStyle().strikethrough }),
        icon: "o-spreadsheet-Icon.STRIKE",
        isActive: (env) => !!env.model.getters.getCurrentStyle().strikethrough,
    };
    const formatFontSize = {
        name: _t("Font size"),
        children: fontSizeMenuBuilder(),
        icon: "o-spreadsheet-Icon.FONT_SIZE",
    };
    const formatAlignment = {
        name: _t("Alignment"),
        icon: "o-spreadsheet-Icon.ALIGN_LEFT",
    };
    const formatAlignmentHorizontal = {
        name: _t("Horizontal align"),
        icon: (env) => getHorizontalAlignmentIcon(env),
    };
    const formatAlignmentLeft = {
        name: _t("Left"),
        description: "Ctrl+Shift+L",
        execute: (env) => setStyle(env, { align: "left" }),
        isActive: (env) => getHorizontalAlign(env) === "left",
        icon: "o-spreadsheet-Icon.ALIGN_LEFT",
    };
    const formatAlignmentCenter = {
        name: _t("Center"),
        description: "Ctrl+Shift+E",
        execute: (env) => setStyle(env, { align: "center" }),
        isActive: (env) => getHorizontalAlign(env) === "center",
        icon: "o-spreadsheet-Icon.ALIGN_CENTER",
    };
    const formatAlignmentRight = {
        name: _t("Right"),
        description: "Ctrl+Shift+R",
        execute: (env) => setStyle(env, { align: "right" }),
        isActive: (env) => getHorizontalAlign(env) === "right",
        icon: "o-spreadsheet-Icon.ALIGN_RIGHT",
    };
    const formatAlignmentVertical = {
        name: _t("Vertical align"),
        icon: (env) => getVerticalAlignmentIcon(env),
    };
    const formatAlignmentTop = {
        name: _t("Top"),
        execute: (env) => setStyle(env, { verticalAlign: "top" }),
        isActive: (env) => getVerticalAlign(env) === "top",
        icon: "o-spreadsheet-Icon.ALIGN_TOP",
    };
    const formatAlignmentMiddle = {
        name: _t("Middle"),
        execute: (env) => setStyle(env, { verticalAlign: "middle" }),
        isActive: (env) => getVerticalAlign(env) === "middle",
        icon: "o-spreadsheet-Icon.ALIGN_MIDDLE",
    };
    const formatAlignmentBottom = {
        name: _t("Bottom"),
        execute: (env) => setStyle(env, { verticalAlign: "bottom" }),
        isActive: (env) => getVerticalAlign(env) === "bottom",
        icon: "o-spreadsheet-Icon.ALIGN_BOTTOM",
    };
    const formatWrappingIcon = {
        name: _t("Wrapping"),
        icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW",
    };
    const formatWrapping = {
        name: _t("Wrapping"),
        icon: (env) => getWrapModeIcon(env),
    };
    const formatWrappingOverflow = {
        name: _t("Overflow"),
        execute: (env) => setStyle(env, { wrapping: "overflow" }),
        isActive: (env) => getWrappingMode(env) === "overflow",
        icon: "o-spreadsheet-Icon.WRAPPING_OVERFLOW",
    };
    const formatWrappingWrap = {
        name: _t("Wrap"),
        execute: (env) => setStyle(env, { wrapping: "wrap" }),
        isActive: (env) => getWrappingMode(env) === "wrap",
        icon: "o-spreadsheet-Icon.WRAPPING_WRAP",
    };
    const formatWrappingClip = {
        name: _t("Clip"),
        execute: (env) => setStyle(env, { wrapping: "clip" }),
        isActive: (env) => getWrappingMode(env) === "clip",
        icon: "o-spreadsheet-Icon.WRAPPING_CLIP",
    };
    const textColor = {
        name: _t("Text Color"),
        icon: "o-spreadsheet-Icon.TEXT_COLOR",
    };
    const fillColor = {
        name: _t("Fill Color"),
        icon: "o-spreadsheet-Icon.FILL_COLOR",
    };
    const formatCF = {
        name: _t("Conditional formatting"),
        execute: OPEN_CF_SIDEPANEL_ACTION,
        icon: "o-spreadsheet-Icon.CONDITIONAL_FORMAT",
    };
    const clearFormat = {
        name: _t("Clear formatting"),
        description: "Ctrl+<",
        execute: (env) => env.model.dispatch("CLEAR_FORMATTING", {
            sheetId: env.model.getters.getActiveSheetId(),
            target: env.model.getters.getSelectedZones(),
        }),
        icon: "o-spreadsheet-Icon.CLEAR_FORMAT",
    };
    function fontSizeMenuBuilder() {
        return FONT_SIZES.map((fs) => {
            return {
                name: fs.toString(),
                sequence: fs,
                id: `font_size_${fs}`,
                execute: (env) => setStyle(env, { fontSize: fs }),
                isActive: (env) => isFontSizeSelected(env, fs),
            };
        });
    }
    function isAutomaticFormatSelected(env) {
        const activeCell = env.model.getters.getCell(env.model.getters.getActivePosition());
        return !activeCell || !activeCell.format;
    }
    function isFormatSelected(env, format) {
        const activeCell = env.model.getters.getCell(env.model.getters.getActivePosition());
        return activeCell?.format === format;
    }
    function isFontSizeSelected(env, fontSize) {
        const currentFontSize = env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
        return currentFontSize === fontSize;
    }
    function getHorizontalAlign(env) {
        const style = env.model.getters.getCurrentStyle();
        if (style.align) {
            return style.align;
        }
        const cell = env.model.getters.getActiveCell();
        return cell.defaultAlign;
    }
    function getVerticalAlign(env) {
        const style = env.model.getters.getCurrentStyle();
        if (style.verticalAlign) {
            return style.verticalAlign;
        }
        return DEFAULT_VERTICAL_ALIGN;
    }
    function getWrappingMode(env) {
        const style = env.model.getters.getCurrentStyle();
        if (style.wrapping) {
            return style.wrapping;
        }
        return DEFAULT_WRAPPING_MODE;
    }
    function getHorizontalAlignmentIcon(env) {
        const horizontalAlign = getHorizontalAlign(env);
        switch (horizontalAlign) {
            case "right":
                return "o-spreadsheet-Icon.ALIGN_RIGHT";
            case "center":
                return "o-spreadsheet-Icon.ALIGN_CENTER";
            default:
                return "o-spreadsheet-Icon.ALIGN_LEFT";
        }
    }
    function getVerticalAlignmentIcon(env) {
        const verticalAlign = getVerticalAlign(env);
        switch (verticalAlign) {
            case "top":
                return "o-spreadsheet-Icon.ALIGN_TOP";
            case "middle":
                return "o-spreadsheet-Icon.ALIGN_MIDDLE";
            default:
                return "o-spreadsheet-Icon.ALIGN_BOTTOM";
        }
    }
    function getWrapModeIcon(env) {
        const wrapMode = getWrappingMode(env);
        switch (wrapMode) {
            case "wrap":
                return "o-spreadsheet-Icon.WRAPPING_WRAP";
            case "clip":
                return "o-spreadsheet-Icon.WRAPPING_CLIP";
            default:
                return "o-spreadsheet-Icon.WRAPPING_OVERFLOW";
        }
    }

    var ACTION_FORMAT = /*#__PURE__*/Object.freeze({
        __proto__: null,
        clearFormat: clearFormat,
        decraseDecimalPlaces: decraseDecimalPlaces,
        fillColor: fillColor,
        formatAlignment: formatAlignment,
        formatAlignmentBottom: formatAlignmentBottom,
        formatAlignmentCenter: formatAlignmentCenter,
        formatAlignmentHorizontal: formatAlignmentHorizontal,
        formatAlignmentLeft: formatAlignmentLeft,
        formatAlignmentMiddle: formatAlignmentMiddle,
        formatAlignmentRight: formatAlignmentRight,
        formatAlignmentTop: formatAlignmentTop,
        formatAlignmentVertical: formatAlignmentVertical,
        formatBold: formatBold,
        formatCF: formatCF,
        formatCustomCurrency: formatCustomCurrency,
        formatFontSize: formatFontSize,
        formatItalic: formatItalic,
        formatNumberAutomatic: formatNumberAutomatic,
        formatNumberCurrency: formatNumberCurrency,
        formatNumberCurrencyRounded: formatNumberCurrencyRounded,
        formatNumberDate: formatNumberDate,
        formatNumberDateTime: formatNumberDateTime,
        formatNumberDayAndFullMonth: formatNumberDayAndFullMonth,
        formatNumberDayAndShortMonth: formatNumberDayAndShortMonth,
        formatNumberDuration: formatNumberDuration,
        formatNumberFullDateTime: formatNumberFullDateTime,
        formatNumberFullMonth: formatNumberFullMonth,
        formatNumberFullWeekDayAndMonth: formatNumberFullWeekDayAndMonth,
        formatNumberNumber: formatNumberNumber,
        formatNumberPercent: formatNumberPercent,
        formatNumberShortMonth: formatNumberShortMonth,
        formatNumberShortWeekDay: formatNumberShortWeekDay,
        formatNumberTime: formatNumberTime,
        formatPercent: formatPercent,
        formatStrikethrough: formatStrikethrough,
        formatUnderline: formatUnderline,
        formatWrapping: formatWrapping,
        formatWrappingClip: formatWrappingClip,
        formatWrappingIcon: formatWrappingIcon,
        formatWrappingOverflow: formatWrappingOverflow,
        formatWrappingWrap: formatWrappingWrap,
        incraseDecimalPlaces: incraseDecimalPlaces,
        moreFormats: moreFormats,
        textColor: textColor
    });

    function interactiveFreezeColumnsRows(env, dimension, base) {
        const sheetId = env.model.getters.getActiveSheetId();
        const cmd = dimension === "COL" ? "FREEZE_COLUMNS" : "FREEZE_ROWS";
        const result = env.model.dispatch(cmd, { sheetId, quantity: base });
        if (result.isCancelledBecause("MergeOverlap" /* CommandResult.MergeOverlap */)) {
            env.raiseError(MergeErrorMessage);
        }
    }

    const hideCols = {
        name: HIDE_COLUMNS_NAME,
        execute: (env) => {
            const columns = env.model.getters.getElementsFromSelection("COL");
            env.model.dispatch("HIDE_COLUMNS_ROWS", {
                sheetId: env.model.getters.getActiveSheetId(),
                dimension: "COL",
                elements: columns,
            });
        },
        isVisible: NOT_ALL_VISIBLE_COLS_SELECTED,
        icon: "o-spreadsheet-Icon.HIDE_COL",
    };
    const unhideCols = {
        name: _t("Unhide columns"),
        execute: (env) => {
            const columns = env.model.getters.getElementsFromSelection("COL");
            env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
                sheetId: env.model.getters.getActiveSheetId(),
                dimension: "COL",
                elements: columns,
            });
        },
        isVisible: (env) => {
            const hiddenCols = env.model.getters
                .getHiddenColsGroups(env.model.getters.getActiveSheetId())
                .flat();
            const currentCols = env.model.getters.getElementsFromSelection("COL");
            return currentCols.some((col) => hiddenCols.includes(col));
        },
    };
    const unhideAllCols = {
        name: _t("Unhide all columns"),
        execute: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
                sheetId,
                dimension: "COL",
                elements: Array.from(Array(env.model.getters.getNumberCols(sheetId)).keys()),
            });
        },
        isVisible: (env) => env.model.getters.getHiddenColsGroups(env.model.getters.getActiveSheetId()).length > 0,
    };
    const hideRows = {
        name: HIDE_ROWS_NAME,
        execute: (env) => {
            const rows = env.model.getters.getElementsFromSelection("ROW");
            env.model.dispatch("HIDE_COLUMNS_ROWS", {
                sheetId: env.model.getters.getActiveSheetId(),
                dimension: "ROW",
                elements: rows,
            });
        },
        isVisible: NOT_ALL_VISIBLE_ROWS_SELECTED,
        icon: "o-spreadsheet-Icon.HIDE_ROW",
    };
    const unhideRows = {
        name: _t("Unhide rows"),
        execute: (env) => {
            const columns = env.model.getters.getElementsFromSelection("ROW");
            env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
                sheetId: env.model.getters.getActiveSheetId(),
                dimension: "ROW",
                elements: columns,
            });
        },
        isVisible: (env) => {
            const hiddenRows = env.model.getters
                .getHiddenRowsGroups(env.model.getters.getActiveSheetId())
                .flat();
            const currentRows = env.model.getters.getElementsFromSelection("ROW");
            return currentRows.some((col) => hiddenRows.includes(col));
        },
    };
    const unhideAllRows = {
        name: _t("Unhide all rows"),
        execute: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
                sheetId,
                dimension: "ROW",
                elements: Array.from(Array(env.model.getters.getNumberRows(sheetId)).keys()),
            });
        },
        isVisible: (env) => env.model.getters.getHiddenRowsGroups(env.model.getters.getActiveSheetId()).length > 0,
    };
    const unFreezePane = {
        name: _t("Unfreeze"),
        isVisible: (env) => {
            const { xSplit, ySplit } = env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId());
            return xSplit + ySplit > 0;
        },
        execute: (env) => env.model.dispatch("UNFREEZE_COLUMNS_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
        }),
        icon: "o-spreadsheet-Icon.UNFREEZE",
    };
    const freezePane = {
        name: _t("Freeze"),
        icon: "o-spreadsheet-Icon.FREEZE",
    };
    const unFreezeRows = {
        name: _t("No rows"),
        execute: (env) => env.model.dispatch("UNFREEZE_ROWS", {
            sheetId: env.model.getters.getActiveSheetId(),
        }),
        isReadonlyAllowed: true,
        isVisible: (env) => !!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).ySplit,
    };
    const freezeFirstRow = {
        name: _t("1 row"),
        execute: (env) => interactiveFreezeColumnsRows(env, "ROW", 1),
        isReadonlyAllowed: true,
    };
    const freezeSecondRow = {
        name: _t("2 rows"),
        execute: (env) => interactiveFreezeColumnsRows(env, "ROW", 2),
        isReadonlyAllowed: true,
    };
    const freezeCurrentRow = {
        name: _t("Up to current row"),
        execute: (env) => {
            const { bottom } = env.model.getters.getSelectedZone();
            interactiveFreezeColumnsRows(env, "ROW", bottom + 1);
        },
        isReadonlyAllowed: true,
    };
    const unFreezeCols = {
        name: _t("No columns"),
        execute: (env) => env.model.dispatch("UNFREEZE_COLUMNS", {
            sheetId: env.model.getters.getActiveSheetId(),
        }),
        isReadonlyAllowed: true,
        isVisible: (env) => !!env.model.getters.getPaneDivisions(env.model.getters.getActiveSheetId()).xSplit,
    };
    const freezeFirstCol = {
        name: _t("1 column"),
        execute: (env) => interactiveFreezeColumnsRows(env, "COL", 1),
        isReadonlyAllowed: true,
    };
    const freezeSecondCol = {
        name: _t("2 columns"),
        execute: (env) => interactiveFreezeColumnsRows(env, "COL", 2),
        isReadonlyAllowed: true,
    };
    const freezeCurrentCol = {
        name: _t("Up to current column"),
        execute: (env) => {
            const { right } = env.model.getters.getSelectedZone();
            interactiveFreezeColumnsRows(env, "COL", right + 1);
        },
        isReadonlyAllowed: true,
    };
    const viewGridlines = {
        name: (env) => env.model.getters.getGridLinesVisibility(env.model.getters.getActiveSheetId())
            ? _t("Hide gridlines")
            : _t("Show gridlines"),
        execute: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            env.model.dispatch("SET_GRID_LINES_VISIBILITY", {
                sheetId,
                areGridLinesVisible: !env.model.getters.getGridLinesVisibility(sheetId),
            });
        },
        icon: "o-spreadsheet-Icon.SHOW_HIDE_GRID",
    };
    const viewFormulas = {
        name: (env) => env.model.getters.shouldShowFormulas() ? "Hide formulas" : _t("Show formulas"),
        execute: (env) => env.model.dispatch("SET_FORMULA_VISIBILITY", { show: !env.model.getters.shouldShowFormulas() }),
        isReadonlyAllowed: true,
        icon: "o-spreadsheet-Icon.SHOW_HIDE_FORMULA",
    };
    const createRemoveFilter = {
        name: (env) => selectionContainsFilter(env) ? _t("Remove selected filters") : _t("Create filter"),
        isActive: (env) => selectionContainsFilter(env),
        isEnabled: (env) => !cannotCreateFilter(env),
        execute: (env) => createRemoveFilterAction(env),
        icon: "o-spreadsheet-Icon.FILTER_ICON_INACTIVE",
    };
    const groupColumns = {
        name: (env) => {
            const selection = env.model.getters.getSelectedZone();
            if (selection.left === selection.right) {
                return _t("Group column %s", numberToLetters(selection.left));
            }
            return _t("Group columns %s - %s", numberToLetters(selection.left), numberToLetters(selection.right));
        },
        execute: (env) => groupHeadersAction(env, "COL"),
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            const selection = env.model.getters.getSelectedZone();
            const groups = env.model.getters.getHeaderGroupsInZone(sheetId, "COL", selection);
            return (IS_ONLY_ONE_RANGE(env) &&
                !groups.some((group) => group.start === selection.left && group.end === selection.right));
        },
        icon: "o-spreadsheet-Icon.GROUP_COLUMNS",
    };
    const groupRows = {
        name: (env) => {
            const selection = env.model.getters.getSelectedZone();
            if (selection.top === selection.bottom) {
                return _t("Group row %s", String(selection.top + 1));
            }
            return _t("Group rows %s - %s", String(selection.top + 1), String(selection.bottom + 1));
        },
        execute: (env) => groupHeadersAction(env, "ROW"),
        isVisible: (env) => {
            const sheetId = env.model.getters.getActiveSheetId();
            const selection = env.model.getters.getSelectedZone();
            const groups = env.model.getters.getHeaderGroupsInZone(sheetId, "ROW", selection);
            return (IS_ONLY_ONE_RANGE(env) &&
                !groups.some((group) => group.start === selection.top && group.end === selection.bottom));
        },
        icon: "o-spreadsheet-Icon.GROUP_ROWS",
    };
    const ungroupColumns = {
        name: (env) => {
            const selection = env.model.getters.getSelectedZone();
            if (selection.left === selection.right) {
                return _t("Ungroup column %s", numberToLetters(selection.left));
            }
            return _t("Ungroup columns %s - %s", numberToLetters(selection.left), numberToLetters(selection.right));
        },
        execute: (env) => ungroupHeaders(env, "COL"),
        icon: "o-spreadsheet-Icon.UNGROUP_COLUMNS",
    };
    const ungroupRows = {
        name: (env) => {
            const selection = env.model.getters.getSelectedZone();
            if (selection.top === selection.bottom) {
                return _t("Ungroup row %s", String(selection.top + 1));
            }
            return _t("Ungroup rows %s - %s", String(selection.top + 1), String(selection.bottom + 1));
        },
        execute: (env) => ungroupHeaders(env, "ROW"),
        icon: "o-spreadsheet-Icon.UNGROUP_ROWS",
    };
    function selectionContainsFilter(env) {
        const sheetId = env.model.getters.getActiveSheetId();
        const selectedZones = env.model.getters.getSelectedZones();
        return env.model.getters.doesZonesContainFilter(sheetId, selectedZones);
    }
    function cannotCreateFilter(env) {
        return !areZonesContinuous(...env.model.getters.getSelectedZones());
    }
    function createRemoveFilterAction(env) {
        if (selectionContainsFilter(env)) {
            env.model.dispatch("REMOVE_FILTER_TABLE", {
                sheetId: env.model.getters.getActiveSheetId(),
                target: env.model.getters.getSelectedZones(),
            });
            return;
        }
        if (cannotCreateFilter(env)) {
            return;
        }
        env.model.selection.selectTableAroundSelection();
        const sheetId = env.model.getters.getActiveSheetId();
        const selection = env.model.getters.getSelectedZones();
        interactiveAddFilter(env, sheetId, selection);
    }
    function groupHeadersAction(env, dim) {
        const selection = env.model.getters.getSelectedZone();
        const sheetId = env.model.getters.getActiveSheetId();
        env.model.dispatch("GROUP_HEADERS", {
            sheetId,
            dimension: dim,
            start: dim === "COL" ? selection.left : selection.top,
            end: dim === "COL" ? selection.right : selection.bottom,
        });
    }
    function ungroupHeaders(env, dim) {
        const selection = env.model.getters.getSelectedZone();
        const sheetId = env.model.getters.getActiveSheetId();
        env.model.dispatch("UNGROUP_HEADERS", {
            sheetId,
            dimension: dim,
            start: dim === "COL" ? selection.left : selection.top,
            end: dim === "COL" ? selection.right : selection.bottom,
        });
    }
    function canUngroupHeaders(env, dimension) {
        const sheetId = env.model.getters.getActiveSheetId();
        const selection = env.model.getters.getSelectedZones();
        return (selection.length === 1 &&
            env.model.getters.getHeaderGroupsInZone(sheetId, dimension, selection[0]).length > 0);
    }

    var ACTION_VIEW = /*#__PURE__*/Object.freeze({
        __proto__: null,
        canUngroupHeaders: canUngroupHeaders,
        createRemoveFilter: createRemoveFilter,
        freezeCurrentCol: freezeCurrentCol,
        freezeCurrentRow: freezeCurrentRow,
        freezeFirstCol: freezeFirstCol,
        freezeFirstRow: freezeFirstRow,
        freezePane: freezePane,
        freezeSecondCol: freezeSecondCol,
        freezeSecondRow: freezeSecondRow,
        groupColumns: groupColumns,
        groupRows: groupRows,
        hideCols: hideCols,
        hideRows: hideRows,
        unFreezeCols: unFreezeCols,
        unFreezePane: unFreezePane,
        unFreezeRows: unFreezeRows,
        ungroupColumns: ungroupColumns,
        ungroupRows: ungroupRows,
        unhideAllCols: unhideAllCols,
        unhideAllRows: unhideAllRows,
        unhideCols: unhideCols,
        unhideRows: unhideRows,
        viewFormulas: viewFormulas,
        viewGridlines: viewGridlines
    });

    const colMenuRegistry = new MenuItemRegistry();
    colMenuRegistry
        .add("cut", {
        ...cut,
        sequence: 10,
    })
        .add("copy", {
        ...copy,
        sequence: 20,
    })
        .add("paste", {
        ...paste,
        sequence: 30,
    })
        .add("paste_special", {
        ...pasteSpecial,
        sequence: 40,
        separator: true,
    })
        .addChild("paste_value_only", ["paste_special"], {
        ...pasteSpecialValue,
        sequence: 10,
    })
        .addChild("paste_format_only", ["paste_special"], {
        ...pasteSpecialFormat,
        sequence: 20,
    })
        .add("sort_columns", {
        ...sortRange,
        name: (env) => env.model.getters.getActiveCols().size > 1 ? _t("Sort columns") : _t("Sort column"),
        sequence: 50,
        separator: true,
    })
        .addChild("sort_ascending", ["sort_columns"], {
        ...sortAscending,
        sequence: 10,
    })
        .addChild("sort_descending", ["sort_columns"], {
        ...sortDescending,
        sequence: 20,
    })
        .add("add_column_before", {
        ...colInsertColsBefore,
        sequence: 70,
    })
        .add("add_column_after", {
        ...colInsertColsAfter,
        sequence: 80,
    })
        .add("delete_column", {
        ...deleteCols,
        sequence: 90,
        icon: "o-spreadsheet-Icon.DELETE",
    })
        .add("clear_column", {
        ...clearCols,
        sequence: 100,
        icon: "o-spreadsheet-Icon.CLEAR",
    })
        .add("hide_columns", {
        ...hideCols,
        sequence: 105,
        separator: true,
    })
        .add("unhide_columns", {
        ...unhideCols,
        sequence: 106,
        separator: true,
    })
        .add("conditional_formatting", {
        ...formatCF,
        sequence: 110,
        separator: true,
    })
        .add("group_columns", {
        sequence: 120,
        ...groupColumns,
    })
        .add("ungroup_columns", {
        sequence: 130,
        ...ungroupColumns,
        isVisible: (env) => canUngroupHeaders(env, "COL"),
    });

    const numberFormatMenuRegistry = new MenuItemRegistry();
    numberFormatMenuRegistry
        .add("format_number_automatic", {
        ...formatNumberAutomatic,
        sequence: 10,
        separator: true,
    })
        .add("format_number_number", {
        ...formatNumberNumber,
        sequence: 20,
    })
        .add("format_number_percent", {
        ...formatNumberPercent,
        sequence: 30,
        separator: true,
    })
        .add("format_number_currency", {
        ...formatNumberCurrency,
        sequence: 40,
    })
        .add("format_number_currency_rounded", {
        ...formatNumberCurrencyRounded,
        sequence: 50,
    })
        .add("format_custom_currency", {
        ...formatCustomCurrency,
        sequence: 60,
        separator: true,
    })
        .add("format_number_date", {
        ...formatNumberDate,
        sequence: 70,
    })
        .add("format_number_time", {
        ...formatNumberTime,
        sequence: 80,
    })
        .add("format_number_date_time", {
        ...formatNumberDateTime,
        sequence: 90,
    })
        .add("format_number_duration", {
        ...formatNumberDuration,
        sequence: 100,
        separator: true,
    })
        .add("more_formats", {
        ...moreFormats,
        sequence: 110,
    });
    const formatNumberMenuItemSpec = {
        name: _t("More formats"),
        icon: "o-spreadsheet-Icon.NUMBER_FORMATS",
        children: [() => numberFormatMenuRegistry.getAll()],
    };

    const rowMenuRegistry = new MenuItemRegistry();
    rowMenuRegistry
        .add("cut", {
        ...cut,
        sequence: 10,
    })
        .add("copy", {
        ...copy,
        sequence: 20,
    })
        .add("paste", {
        ...paste,
        sequence: 30,
    })
        .add("paste_special", {
        ...pasteSpecial,
        sequence: 40,
        separator: true,
    })
        .addChild("paste_value_only", ["paste_special"], {
        ...pasteSpecialValue,
        sequence: 10,
    })
        .addChild("paste_format_only", ["paste_special"], {
        ...pasteSpecialFormat,
        sequence: 20,
    })
        .add("add_row_before", {
        ...rowInsertRowBefore,
        sequence: 50,
    })
        .add("add_row_after", {
        ...rowInsertRowsAfter,
        sequence: 60,
    })
        .add("delete_row", {
        ...deleteRows,
        sequence: 70,
        icon: "o-spreadsheet-Icon.DELETE",
    })
        .add("clear_row", {
        ...clearRows,
        sequence: 80,
        icon: "o-spreadsheet-Icon.CLEAR",
    })
        .add("hide_rows", {
        ...hideRows,
        sequence: 85,
        separator: true,
    })
        .add("unhide_rows", {
        ...unhideRows,
        sequence: 86,
        separator: true,
    })
        .add("conditional_formatting", {
        ...formatCF,
        sequence: 90,
        separator: true,
    })
        .add("group_rows", {
        sequence: 100,
        ...groupRows,
    })
        .add("ungroup_rows", {
        sequence: 110,
        ...ungroupRows,
        isVisible: (env) => canUngroupHeaders(env, "ROW"),
    });

    function getSheetMenuRegistry(args) {
        const sheetMenuRegistry = new MenuItemRegistry();
        sheetMenuRegistry
            .add("delete", {
            ...deleteSheet,
            sequence: 10,
        })
            .add("duplicate", {
            ...duplicateSheet,
            sequence: 20,
        })
            .add("rename", {
            ...renameSheet(args),
            sequence: 30,
        })
            .add("move_right", {
            ...sheetMoveRight,
            sequence: 40,
        })
            .add("move_left", {
            ...sheetMoveLeft,
            sequence: 50,
        })
            .add("hide_sheet", {
            ...hideSheet,
            sequence: 60,
        });
        return sheetMenuRegistry;
    }

    const topbarMenuRegistry = new MenuItemRegistry();
    topbarMenuRegistry
        // ---------------------------------------------------------------------
        // FILE MENU ITEMS
        // ---------------------------------------------------------------------
        .add("file", {
        name: _t("File"),
        sequence: 10,
    })
        .addChild("settings", ["file"], {
        name: _t("Settings"),
        sequence: 100,
        execute: (env) => env.openSidePanel("Settings"),
        icon: "o-spreadsheet-Icon.COG",
    })
        // ---------------------------------------------------------------------
        // EDIT MENU ITEMS
        // ---------------------------------------------------------------------
        .add("edit", {
        name: _t("Edit"),
        sequence: 20,
    })
        .addChild("undo", ["edit"], {
        ...undo,
        sequence: 10,
    })
        .addChild("redo", ["edit"], {
        ...redo,
        sequence: 20,
        separator: true,
    })
        .addChild("copy", ["edit"], {
        ...copy,
        sequence: 30,
    })
        .addChild("cut", ["edit"], {
        ...cut,
        sequence: 40,
    })
        .addChild("paste", ["edit"], {
        ...paste,
        sequence: 50,
    })
        .addChild("paste_special", ["edit"], {
        ...pasteSpecial,
        sequence: 60,
        separator: true,
    })
        .addChild("paste_special_value", ["edit", "paste_special"], {
        ...pasteSpecialValue,
        sequence: 10,
    })
        .addChild("paste_special_format", ["edit", "paste_special"], {
        ...pasteSpecialFormat,
        sequence: 20,
    })
        .addChild("find_and_replace", ["edit"], {
        ...findAndReplace,
        sequence: 65,
        separator: true,
    })
        .addChild("delete", ["edit"], {
        name: _t("Delete"),
        icon: "o-spreadsheet-Icon.DELETE",
        sequence: 70,
    })
        .addChild("edit_delete_cell_values", ["edit", "delete"], {
        ...deleteValues,
        sequence: 10,
    })
        .addChild("edit_delete_row", ["edit", "delete"], {
        ...deleteRows,
        sequence: 20,
    })
        .addChild("edit_delete_column", ["edit", "delete"], {
        ...deleteCols,
        sequence: 30,
    })
        .addChild("edit_delete_cell_shift_up", ["edit", "delete"], {
        ...deleteCellShiftUp,
        sequence: 40,
    })
        .addChild("edit_delete_cell_shift_left", ["edit", "delete"], {
        ...deleteCellShiftLeft,
        sequence: 50,
    })
        .addChild("edit_unhide_columns", ["edit"], {
        ...unhideAllCols,
        sequence: 80,
    })
        .addChild("edit_unhide_rows", ["edit"], {
        ...unhideAllRows,
        sequence: 80,
    })
        // ---------------------------------------------------------------------
        // VIEW MENU ITEMS
        // ---------------------------------------------------------------------
        .add("view", {
        name: _t("View"),
        sequence: 30,
    })
        .addChild("unfreeze_panes", ["view"], {
        ...unFreezePane,
        sequence: 4,
    })
        .addChild("freeze_panes", ["view"], {
        ...freezePane,
        sequence: 5,
    })
        .addChild("unfreeze_rows", ["view", "freeze_panes"], {
        ...unFreezeRows,
        sequence: 5,
    })
        .addChild("freeze_first_row", ["view", "freeze_panes"], {
        ...freezeFirstRow,
        sequence: 10,
    })
        .addChild("freeze_second_row", ["view", "freeze_panes"], {
        ...freezeSecondRow,
        sequence: 15,
    })
        .addChild("freeze_current_row", ["view", "freeze_panes"], {
        ...freezeCurrentRow,
        sequence: 20,
        separator: true,
    })
        .addChild("unfreeze_columns", ["view", "freeze_panes"], {
        ...unFreezeCols,
        sequence: 25,
    })
        .addChild("freeze_first_col", ["view", "freeze_panes"], {
        ...freezeFirstCol,
        sequence: 30,
    })
        .addChild("freeze_second_col", ["view", "freeze_panes"], {
        ...freezeSecondCol,
        sequence: 35,
    })
        .addChild("freeze_current_col", ["view", "freeze_panes"], {
        ...freezeCurrentCol,
        sequence: 40,
    })
        .addChild("group_headers", ["view"], {
        name: _t("Group"),
        sequence: 15,
        separator: true,
        icon: "o-spreadsheet-Icon.PLUS_IN_BOX",
        isVisible: IS_ONLY_ONE_RANGE,
    })
        .addChild("group_columns", ["view", "group_headers"], {
        ...groupColumns,
        sequence: 5,
    })
        .addChild("ungroup_columns", ["view", "group_headers"], {
        ...ungroupColumns,
        isVisible: (env) => canUngroupHeaders(env, "COL"),
        sequence: 10,
    })
        .addChild("group_rows", ["view", "group_headers"], {
        ...groupRows,
        sequence: 15,
    })
        .addChild("ungroup_rows", ["view", "group_headers"], {
        ...ungroupRows,
        isVisible: (env) => canUngroupHeaders(env, "ROW"),
        sequence: 20,
    })
        .addChild("view_gridlines", ["view"], {
        ...viewGridlines,
        sequence: 15,
    })
        .addChild("view_formulas", ["view"], {
        ...viewFormulas,
        sequence: 20,
    })
        // ---------------------------------------------------------------------
        // INSERT MENU ITEMS
        // ---------------------------------------------------------------------
        .add("insert", {
        name: _t("Insert"),
        sequence: 40,
    })
        .addChild("insert_row", ["insert"], {
        ...insertRow,
        sequence: 10,
    })
        .addChild("insert_row_before", ["insert", "insert_row"], {
        ...topBarInsertRowsBefore,
        sequence: 10,
    })
        .addChild("insert_row_after", ["insert", "insert_row"], {
        ...topBarInsertRowsAfter,
        sequence: 20,
    })
        .addChild("insert_column", ["insert"], {
        ...insertCol,
        sequence: 20,
    })
        .addChild("insert_column_before", ["insert", "insert_column"], {
        ...topBarInsertColsBefore,
        sequence: 10,
    })
        .addChild("insert_column_after", ["insert", "insert_column"], {
        ...topBarInsertColsAfter,
        sequence: 20,
    })
        .addChild("insert_cell", ["insert"], {
        ...insertCell,
        sequence: 43,
    })
        .addChild("insert_cell_down", ["insert", "insert_cell"], {
        ...insertCellShiftDown,
        name: _t("Shift down"),
        sequence: 10,
    })
        .addChild("insert_cell_right", ["insert", "insert_cell"], {
        ...insertCellShiftRight,
        name: _t("Shift right"),
        sequence: 20,
    })
        .addChild("insert_sheet", ["insert"], {
        ...insertSheet,
        sequence: 80,
        separator: true,
    })
        .addChild("insert_chart", ["insert"], {
        ...insertChart,
        sequence: 50,
    })
        .addChild("insert_image", ["insert"], {
        ...insertImage,
        sequence: 55,
    })
        .addChild("insert_function", ["insert"], {
        ...insertFunction,
        sequence: 60,
    })
        .addChild("insert_function_sum", ["insert", "insert_function"], {
        ...insertFunctionSum,
        sequence: 0,
    })
        .addChild("insert_function_average", ["insert", "insert_function"], {
        ...insertFunctionAverage,
        sequence: 10,
    })
        .addChild("insert_function_count", ["insert", "insert_function"], {
        ...insertFunctionCount,
        sequence: 20,
    })
        .addChild("insert_function_max", ["insert", "insert_function"], {
        ...insertFunctionMax,
        sequence: 30,
    })
        .addChild("insert_function_min", ["insert", "insert_function"], {
        ...insertFunctionMin,
        sequence: 40,
        separator: true,
    })
        .addChild("categorie_function_all", ["insert", "insert_function"], {
        ...categorieFunctionAll,
        sequence: 50,
    })
        .addChild("categories_function_list", ["insert", "insert_function"], categoriesFunctionListMenuBuilder)
        .addChild("insert_link", ["insert"], {
        ...insertLink,
        separator: true,
        sequence: 70,
    })
        // ---------------------------------------------------------------------
        // FORMAT MENU ITEMS
        // ---------------------------------------------------------------------
        .add("format", { name: _t("Format"), sequence: 50 })
        .addChild("format_number", ["format"], {
        ...formatNumberMenuItemSpec,
        name: _t("Number"),
        sequence: 10,
        separator: true,
    })
        .addChild("format_bold", ["format"], {
        ...formatBold,
        sequence: 20,
    })
        .addChild("format_italic", ["format"], {
        ...formatItalic,
        sequence: 30,
    })
        .addChild("format_underline", ["format"], {
        ...formatUnderline,
        sequence: 40,
    })
        .addChild("format_strikethrough", ["format"], {
        ...formatStrikethrough,
        sequence: 50,
        separator: true,
    })
        .addChild("format_font_size", ["format"], {
        ...formatFontSize,
        sequence: 60,
        separator: true,
    })
        .addChild("format_alignment", ["format"], {
        ...formatAlignment,
        sequence: 70,
    })
        .addChild("format_alignment_left", ["format", "format_alignment"], {
        ...formatAlignmentLeft,
        sequence: 10,
    })
        .addChild("format_alignment_center", ["format", "format_alignment"], {
        ...formatAlignmentCenter,
        sequence: 20,
    })
        .addChild("format_alignment_right", ["format", "format_alignment"], {
        ...formatAlignmentRight,
        sequence: 30,
        separator: true,
    })
        .addChild("format_alignment_top", ["format", "format_alignment"], {
        ...formatAlignmentTop,
        sequence: 40,
    })
        .addChild("format_alignment_middle", ["format", "format_alignment"], {
        ...formatAlignmentMiddle,
        sequence: 50,
    })
        .addChild("format_alignment_bottom", ["format", "format_alignment"], {
        ...formatAlignmentBottom,
        sequence: 60,
        separator: true,
    })
        .addChild("format_wrapping", ["format"], {
        ...formatWrappingIcon,
        sequence: 80,
        separator: true,
    })
        .addChild("format_wrapping_overflow", ["format", "format_wrapping"], {
        ...formatWrappingOverflow,
        sequence: 10,
    })
        .addChild("format_wrapping_wrap", ["format", "format_wrapping"], {
        ...formatWrappingWrap,
        sequence: 20,
    })
        .addChild("format_wrapping_clip", ["format", "format_wrapping"], {
        ...formatWrappingClip,
        sequence: 30,
    })
        .addChild("format_cf", ["format"], {
        ...formatCF,
        sequence: 90,
        separator: true,
    })
        .addChild("format_clearFormat", ["format"], {
        ...clearFormat,
        sequence: 100,
        separator: true,
    })
        // ---------------------------------------------------------------------
        // DATA MENU ITEMS
        // ---------------------------------------------------------------------
        .add("data", {
        name: _t("Data"),
        sequence: 60,
    })
        .addChild("sort_range", ["data"], {
        ...sortRange,
        sequence: 10,
        separator: true,
    })
        .addChild("sort_ascending", ["data", "sort_range"], {
        ...sortAscending,
        sequence: 10,
    })
        .addChild("sort_descending", ["data", "sort_range"], {
        ...sortDescending,
        sequence: 20,
    })
        .addChild("data_cleanup", ["data"], {
        ...dataCleanup,
        sequence: 15,
    })
        .addChild("remove_duplicates", ["data", "data_cleanup"], {
        ...removeDuplicates,
        sequence: 10,
    })
        .addChild("trim_whitespace", ["data", "data_cleanup"], {
        ...trimWhitespace,
        sequence: 20,
    })
        .addChild("split_to_columns", ["data"], {
        ...splitToColumns,
        sequence: 20,
    })
        .addChild("data_validation", ["data"], {
        name: _t("Data Validation"),
        execute: (env) => {
            env.openSidePanel("DataValidation");
        },
        icon: "o-spreadsheet-Icon.DATA_VALIDATION",
        sequence: 30,
        separator: true,
    })
        .addChild("add_data_filter", ["data"], {
        ...addDataFilter,
        sequence: 40,
        separator: true,
    })
        .addChild("remove_data_filter", ["data"], {
        ...removeDataFilter,
        sequence: 40,
        separator: true,
    });

    class OTRegistry extends Registry {
        /**
         * Add a transformation function to the registry. When the executed command
         * happened, all the commands in toTransforms should be transformed using the
         * transformation function given
         */
        addTransformation(executed, toTransforms, fn) {
            for (let toTransform of toTransforms) {
                if (!this.content[toTransform]) {
                    this.content[toTransform] = new Map();
                }
                this.content[toTransform].set(executed, fn);
            }
            return this;
        }
        /**
         * Get the transformation function to transform the command toTransform, after
         * that the executed command happened.
         */
        getTransformation(toTransform, executed) {
            return this.content[toTransform] && this.content[toTransform].get(executed);
        }
    }
    const otRegistry = new OTRegistry();

    const arrowMap = {
        ArrowDown: "down",
        ArrowLeft: "left",
        ArrowRight: "right",
        ArrowUp: "up",
    };
    function updateSelectionWithArrowKeys(ev, selection) {
        const direction = arrowMap[ev.key];
        if (ev.shiftKey) {
            selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
        }
        else {
            selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
        }
    }

    const uuidGenerator$1 = new UuidGenerator();
    css /* scss */ `
  .o-selection {
    .o-selection-input {
      padding: 2px 0px;

      input {
        padding: 4px 6px;
        border-radius: 4px;
        box-sizing: border-box;
      }
      input:focus {
        outline: none;
      }
      input.o-required,
      input.o-focused {
        border: 1px solid;
      }
      input.o-focused {
        border-width: 2px;
        padding: 3px 5px;
      }
      input.o-invalid {
        /* The background-color is similar to the bootstrap alert-danger class but, because of the commit 0358a76d,
         * which avoids being parasitized by the dark-mode in spreadsheet, we cannot use this class.
         * TODO: Replace with the bootstrap alert-danger class when we support dark mode
         */
        background-color: #ffdddd;
        border-width: 2px;
      }
      button.o-btn {
        color: #333;
      }
      button.o-btn-action {
        margin: 8px 1px;
        border-radius: 4px;
        border: 1px solid #dadce0;
        color: #188038;
        font-size: 14px;
        height: 25px;
      }
      .error-icon {
        right: 7px;
        top: 7px;
      }
    }
    /** Make the character a bit bigger
    compared to its neighbor INPUT box  */
    .o-remove-selection {
      font-size: calc(100% + 4px);
    }
  }
`;
    /**
     * This component can be used when the user needs to input some
     * ranges. He can either input the ranges with the regular DOM `<input/>`
     * displayed or by selecting zones on the grid.
     *
     * onSelectionChanged is called every time the input value
     * changes.
     */
    class SelectionInput extends owl.Component {
        static template = "o-spreadsheet-SelectionInput";
        id = uuidGenerator$1.uuidv4();
        previousRanges = this.props.ranges() || [];
        originSheet = this.env.model.getters.getActiveSheetId();
        state = owl.useState({
            isMissing: false,
            mode: "select-range",
        });
        focusedInput = owl.useRef("focusedInput");
        get ranges() {
            const existingSelectionRanges = this.env.model.getters.getSelectionInput(this.id);
            const ranges = existingSelectionRanges.length
                ? existingSelectionRanges
                : this.props.ranges().map((xc, id) => ({
                    xc,
                    id,
                    isFocused: false,
                }));
            return ranges.map((range) => ({
                ...range,
                isValidRange: range.xc === "" || this.env.model.getters.isRangeValid(range.xc),
            }));
        }
        get hasFocus() {
            return this.ranges.filter((i) => i.isFocused).length > 0;
        }
        get canAddRange() {
            return !this.props.hasSingleRange;
        }
        get isInvalid() {
            return this.props.isInvalid || this.state.isMissing;
        }
        get isConfirmable() {
            return this.hasFocus && this.ranges.every((range) => range.isValidRange);
        }
        get isResettable() {
            return this.previousRanges.join() !== this.ranges.map((r) => r.xc).join();
        }
        setup() {
            owl.useEffect(() => this.focusedInput.el?.focus(), () => [this.focusedInput.el]);
            owl.onMounted(() => this.enableNewSelectionInput());
            owl.onWillUnmount(async () => this.disableNewSelectionInput());
            owl.onPatched(() => this.checkChange());
        }
        enableNewSelectionInput() {
            this.env.model.dispatch("ENABLE_NEW_SELECTION_INPUT", {
                id: this.id,
                initialRanges: this.props.ranges(),
                hasSingleRange: this.props.hasSingleRange,
            });
        }
        disableNewSelectionInput() {
            this.env.model.dispatch("DISABLE_SELECTION_INPUT", { id: this.id });
        }
        checkChange() {
            const value = this.env.model.getters.getSelectionInputValue(this.id);
            const valid = !this.isInvalid && this.ranges.every((range) => range.isValidRange);
            if (valid && this.previousRanges.join() !== value.join()) {
                this.triggerChange();
            }
        }
        getColor(range) {
            const color = range.color || "#000";
            return "color: " + color + ";";
        }
        triggerChange() {
            const ranges = this.env.model.getters.getSelectionInputValue(this.id);
            this.props.onSelectionChanged?.(ranges);
        }
        onKeydown(ev) {
            if (ev.key === "F2") {
                ev.preventDefault();
                ev.stopPropagation();
                this.state.mode = this.state.mode === "select-range" ? "text-edit" : "select-range";
            }
            else if (ev.key.startsWith("Arrow")) {
                ev.stopPropagation();
                if (this.state.mode === "select-range") {
                    ev.preventDefault();
                    updateSelectionWithArrowKeys(ev, this.env.model.selection);
                }
            }
            else if (ev.key === "Enter") {
                const target = ev.target;
                target.blur();
                this.confirm();
            }
        }
        extractRanges(value) {
            return this.props.hasSingleRange ? value.split(",")[0] : value;
        }
        focus(rangeId) {
            this.state.isMissing = false;
            this.state.mode = "select-range";
            this.env.model.dispatch("FOCUS_RANGE", {
                id: this.id,
                rangeId,
            });
        }
        addEmptyInput() {
            this.env.model.dispatch("ADD_EMPTY_RANGE", { id: this.id });
        }
        removeInput(rangeId) {
            this.env.model.dispatch("REMOVE_RANGE", { id: this.id, rangeId });
            this.triggerChange();
            this.props.onSelectionConfirmed?.();
        }
        onInputChanged(rangeId, ev) {
            const target = ev.target;
            const value = this.extractRanges(target.value);
            this.env.model.dispatch("CHANGE_RANGE", {
                id: this.id,
                rangeId,
                value,
            });
            this.triggerChange();
        }
        reset() {
            this.env.model.dispatch("ENABLE_NEW_SELECTION_INPUT", {
                id: this.id,
                initialRanges: this.previousRanges,
                hasSingleRange: this.props.hasSingleRange,
            });
            this.confirm();
        }
        confirm() {
            let anyValidInput = false;
            const existingSelectionRanges = this.env.model.getters.getSelectionInput(this.id);
            const existingSelectionXcs = [];
            for (const range of existingSelectionRanges) {
                if (range.xc === "") {
                    const result = this.env.model.dispatch("REMOVE_RANGE", {
                        id: this.id,
                        rangeId: range.id,
                    });
                    if (result.isSuccessful) {
                        continue;
                    }
                }
                existingSelectionXcs.push(range.xc);
                if (this.env.model.getters.isRangeValid(range.xc)) {
                    anyValidInput = true;
                }
            }
            if (this.props.required && !anyValidInput) {
                this.state.isMissing = true;
            }
            const activeSheetId = this.env.model.getters.getActiveSheetId();
            if (this.originSheet !== activeSheetId) {
                this.env.model.dispatch("ACTIVATE_SHEET", {
                    sheetIdFrom: activeSheetId,
                    sheetIdTo: this.originSheet,
                });
            }
            this.props.onSelectionChanged?.(existingSelectionXcs);
            this.props.onSelectionConfirmed?.();
            this.previousRanges = this.props.ranges();
            if (existingSelectionXcs.join() !== this.previousRanges.join()) {
                this.enableNewSelectionInput();
            }
            this.env.model.dispatch("UNFOCUS_SELECTION_INPUT");
        }
    }
    SelectionInput.props = {
        ranges: Function,
        hasSingleRange: { type: Boolean, optional: true },
        required: { type: Boolean, optional: true },
        isInvalid: { type: Boolean, optional: true },
        class: { type: String, optional: true },
        onSelectionChanged: { type: Function, optional: true },
        onSelectionConfirmed: { type: Function, optional: true },
    };

    css /* scss */ `
  .o-validation-error,
  .o-validation-warning {
    margin-top: 10px;

    .o-icon {
      margin-right: 5px;
      height: 1.2em;
      width: 1.2em;
    }
  }
`;
    class ValidationMessages extends owl.Component {
        static template = "o-spreadsheet-ValidationMessages";
        get divClasses() {
            if (this.props.msgType === "warning") {
                return "o-validation-warning text-warning";
            }
            return "o-validation-error text-danger";
        }
    }
    ValidationMessages.props = {
        messages: Array,
        msgType: String,
    };

    class LineBarPieConfigPanel extends owl.Component {
        static template = "o-spreadsheet-LineBarPieConfigPanel";
        static components = { SelectionInput, ValidationMessages };
        state = owl.useState({
            datasetDispatchResult: undefined,
            labelsDispatchResult: undefined,
        });
        dataSeriesRanges = [];
        labelRange;
        setup() {
            this.dataSeriesRanges = this.props.definition.dataSets;
            this.labelRange = this.props.definition.labelRange;
        }
        get errorMessages() {
            const cancelledReasons = [
                ...(this.state.datasetDispatchResult?.reasons || []),
                ...(this.state.labelsDispatchResult?.reasons || []),
            ];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        get isDatasetInvalid() {
            return !!this.state.datasetDispatchResult?.isCancelledBecause("InvalidDataSet" /* CommandResult.InvalidDataSet */);
        }
        get isLabelInvalid() {
            return !!this.state.labelsDispatchResult?.isCancelledBecause("InvalidLabelRange" /* CommandResult.InvalidLabelRange */);
        }
        onUpdateDataSetsHaveTitle(ev) {
            this.props.updateChart(this.props.figureId, {
                dataSetsHaveTitle: ev.target.checked,
            });
        }
        /**
         * Change the local dataSeriesRanges. The model should be updated when the
         * button "confirm" is clicked
         */
        onDataSeriesRangesChanged(ranges) {
            this.dataSeriesRanges = ranges;
            this.state.datasetDispatchResult = this.props.canUpdateChart(this.props.figureId, {
                dataSets: this.dataSeriesRanges,
            });
        }
        onDataSeriesConfirmed() {
            this.dataSeriesRanges = spreadRange(this.env.model.getters, this.dataSeriesRanges);
            this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
                dataSets: this.dataSeriesRanges,
            });
        }
        getDataSeriesRanges() {
            return this.dataSeriesRanges;
        }
        /**
         * Change the local labelRange. The model should be updated when the
         * button "confirm" is clicked
         */
        onLabelRangeChanged(ranges) {
            this.labelRange = ranges[0];
            this.state.labelsDispatchResult = this.props.canUpdateChart(this.props.figureId, {
                labelRange: this.labelRange,
            });
        }
        onLabelRangeConfirmed() {
            this.state.labelsDispatchResult = this.props.updateChart(this.props.figureId, {
                labelRange: this.labelRange,
            });
        }
        getLabelRange() {
            return this.labelRange || "";
        }
        onUpdateAggregated(ev) {
            this.props.updateChart(this.props.figureId, {
                aggregated: ev.target.checked,
            });
        }
        calculateHeaderPosition() {
            if (this.isDatasetInvalid || this.isLabelInvalid) {
                return undefined;
            }
            const getters = this.env.model.getters;
            const sheetId = getters.getActiveSheetId();
            const labelRange = createRange(getters, sheetId, this.labelRange);
            const dataSets = createDataSets(getters, this.dataSeriesRanges, sheetId, this.props.definition.dataSetsHaveTitle);
            if (dataSets.length) {
                return dataSets[0].dataRange.zone.top + 1;
            }
            else if (labelRange) {
                return labelRange.zone.top + 1;
            }
            return undefined;
        }
    }
    LineBarPieConfigPanel.props = {
        figureId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };

    class BarConfigPanel extends LineBarPieConfigPanel {
        static template = "o-spreadsheet-BarConfigPanel";
        onUpdateStacked(ev) {
            this.props.updateChart(this.props.figureId, {
                stacked: ev.target.checked,
            });
        }
        onUpdateAggregated(ev) {
            this.props.updateChart(this.props.figureId, {
                aggregated: ev.target.checked,
            });
        }
    }

    function startDnd(onMouseMove, onMouseUp, onMouseDown = () => { }) {
        const _onMouseUp = (ev) => {
            onMouseUp(ev);
            window.removeEventListener("mousedown", onMouseDown);
            window.removeEventListener("mouseup", _onMouseUp);
            window.removeEventListener("dragstart", _onDragStart);
            window.removeEventListener("mousemove", onMouseMove);
            window.removeEventListener("wheel", onMouseMove);
        };
        function _onDragStart(ev) {
            ev.preventDefault();
        }
        window.addEventListener("mousedown", onMouseDown);
        window.addEventListener("mouseup", _onMouseUp);
        window.addEventListener("dragstart", _onDragStart);
        window.addEventListener("mousemove", onMouseMove);
        // mouse wheel on window is by default a passive event.
        // preventDefault() is not allowed in passive event handler.
        // https://chromestatus.com/feature/6662647093133312
        window.addEventListener("wheel", onMouseMove, { passive: false });
    }
    /**
     * Function to be used during a mousedown event, this function allows to
     * perform actions related to the mousemove and mouseup events and adjusts the viewport
     * when the new position related to the mousemove event is outside of it.
     * Among inputs are two callback functions. First intended for actions performed during
     * the mousemove event, it receives as parameters the current position of the mousemove
     * (occurrence of the current column and the current row). Second intended for actions
     * performed during the mouseup event.
     */
    function dragAndDropBeyondTheViewport(env, cbMouseMove, cbMouseUp, only = false) {
        let timeOutId = null;
        let currentEv;
        let previousEv;
        let startingEv;
        let startingX;
        let startingY;
        const getters = env.model.getters;
        const sheetId = getters.getActiveSheetId();
        const position = gridOverlayPosition();
        let colIndex;
        let rowIndex;
        const onMouseDown = (ev) => {
            previousEv = ev;
            startingEv = ev;
            startingX = startingEv.clientX - position.left;
            startingY = startingEv.clientY - position.top;
        };
        const onMouseMove = (ev) => {
            currentEv = ev;
            if (timeOutId) {
                return;
            }
            const { x: offsetCorrectionX, y: offsetCorrectionY } = getters.getMainViewportCoordinates();
            let { top, left, bottom, right } = getters.getActiveMainViewport();
            let { scrollX, scrollY } = getters.getActiveSheetDOMScrollInfo();
            const { xSplit, ySplit } = getters.getPaneDivisions(sheetId);
            let canEdgeScroll = false;
            let timeoutDelay = MAX_DELAY;
            const x = currentEv.clientX - position.left;
            colIndex = getters.getColIndex(x);
            if (only !== "vertical") {
                const previousX = previousEv.clientX - position.left;
                const edgeScrollInfoX = getters.getEdgeScrollCol(x, previousX, startingX);
                if (edgeScrollInfoX.canEdgeScroll) {
                    canEdgeScroll = true;
                    timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoX.delay);
                    let newTarget;
                    switch (edgeScrollInfoX.direction) {
                        case "reset":
                            colIndex = xSplit;
                            newTarget = xSplit;
                            break;
                        case 1:
                            colIndex = right;
                            newTarget = left + 1;
                            break;
                        case -1:
                            colIndex = left - 1;
                            while (env.model.getters.isColHidden(sheetId, colIndex)) {
                                colIndex--;
                            }
                            newTarget = colIndex;
                            break;
                    }
                    scrollX = getters.getColDimensions(sheetId, newTarget).start - offsetCorrectionX;
                }
            }
            const y = currentEv.clientY - position.top;
            rowIndex = getters.getRowIndex(y);
            if (only !== "horizontal") {
                const previousY = previousEv.clientY - position.top;
                const edgeScrollInfoY = getters.getEdgeScrollRow(y, previousY, startingY);
                if (edgeScrollInfoY.canEdgeScroll) {
                    canEdgeScroll = true;
                    timeoutDelay = Math.min(timeoutDelay, edgeScrollInfoY.delay);
                    let newTarget;
                    switch (edgeScrollInfoY.direction) {
                        case "reset":
                            rowIndex = ySplit;
                            newTarget = ySplit;
                            break;
                        case 1:
                            rowIndex = bottom;
                            newTarget = top + edgeScrollInfoY.direction;
                            break;
                        case -1:
                            rowIndex = top - 1;
                            while (env.model.getters.isRowHidden(sheetId, rowIndex)) {
                                rowIndex--;
                            }
                            newTarget = rowIndex;
                            break;
                    }
                    scrollY = env.model.getters.getRowDimensions(sheetId, newTarget).start - offsetCorrectionY;
                }
            }
            if (!canEdgeScroll) {
                if (rowIndex === -1) {
                    rowIndex = y < 0 ? 0 : getters.getNumberRows(sheetId) - 1;
                }
                if (colIndex === -1 && x < 0) {
                    colIndex = x < 0 ? 0 : getters.getNumberCols(sheetId) - 1;
                }
            }
            cbMouseMove(colIndex, rowIndex, currentEv);
            if (canEdgeScroll) {
                env.model.dispatch("SET_VIEWPORT_OFFSET", { offsetX: scrollX, offsetY: scrollY });
                timeOutId = setTimeout(() => {
                    timeOutId = null;
                    onMouseMove(currentEv);
                }, Math.round(timeoutDelay));
            }
            previousEv = currentEv;
        };
        const onMouseUp = () => {
            clearTimeout(timeOutId);
            cbMouseUp();
        };
        startDnd(onMouseMove, onMouseUp, onMouseDown);
    }

    const LINE_VERTICAL_PADDING = 1;
    const PICKER_PADDING = 8;
    const ITEM_BORDER_WIDTH = 1;
    const ITEM_EDGE_LENGTH = 18;
    const ITEMS_PER_LINE = 10;
    const MAGNIFIER_EDGE = 16;
    const ITEM_GAP = 2;
    const CONTENT_WIDTH = ITEMS_PER_LINE * (ITEM_EDGE_LENGTH + 2 * ITEM_BORDER_WIDTH) + (ITEMS_PER_LINE - 1) * ITEM_GAP;
    const INNER_GRADIENT_WIDTH = CONTENT_WIDTH - 2 * ITEM_BORDER_WIDTH;
    const INNER_GRADIENT_HEIGHT = CONTENT_WIDTH - 30 - 2 * ITEM_BORDER_WIDTH;
    const CONTAINER_WIDTH = CONTENT_WIDTH + 2 * PICKER_PADDING;
    css /* scss */ `
  .o-color-picker {
    padding: ${PICKER_PADDING}px 0;
    /** FIXME: this is useless, overiden by the popover container */
    box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
    background-color: white;
    line-height: 1.2;
    overflow-y: auto;
    overflow-x: hidden;
    width: ${CONTAINER_WIDTH}px;

    .o-color-picker-section-name {
      margin: 0px ${ITEM_BORDER_WIDTH}px;
      padding: 4px ${PICKER_PADDING}px;
    }
    .colors-grid {
      display: grid;
      padding: ${LINE_VERTICAL_PADDING}px ${PICKER_PADDING}px;
      grid-template-columns: repeat(${ITEMS_PER_LINE}, 1fr);
      grid-gap: ${ITEM_GAP}px;
    }
    .o-color-picker-toggler-button {
      display: flex;
      .o-color-picker-toggler-sign {
        display: flex;
        margin: auto auto;
        width: 55%;
        height: 55%;
        .o-icon {
          width: 100%;
          height: 100%;
        }
      }
    }
    .o-color-picker-line-item {
      width: ${ITEM_EDGE_LENGTH}px;
      height: ${ITEM_EDGE_LENGTH}px;
      margin: 0px;
      border-radius: 50px;
      border: ${ITEM_BORDER_WIDTH}px solid #666666;
      padding: 0px;
      font-size: 16px;
      background: white;
      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
        outline: 1px solid gray;
        cursor: pointer;
      }
    }
    .o-buttons {
      padding: ${PICKER_PADDING}px;
      display: flex;
      .o-cancel {
        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
        width: 100%;
        padding: 5px;
        font-size: 14px;
        background: white;
        border-radius: 4px;
        box-sizing: border-box;
        &:hover:enabled {
          background-color: rgba(0, 0, 0, 0.08);
        }
      }
    }
    .o-add-button {
      border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
      padding: 4px;
      background: white;
      border-radius: 4px;
      &:hover:enabled {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }
    .o-separator {
      border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
      margin-top: ${MENU_SEPARATOR_PADDING}px;
      margin-bottom: ${MENU_SEPARATOR_PADDING}px;
    }

    .o-custom-selector {
      padding: ${PICKER_PADDING + 2}px ${PICKER_PADDING}px;
      position: relative;
      .o-gradient {
        margin-bottom: ${MAGNIFIER_EDGE / 2}px;
        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
        box-sizing: border-box;
        width: ${INNER_GRADIENT_WIDTH + 2 * ITEM_BORDER_WIDTH}px;
        height: ${INNER_GRADIENT_HEIGHT + 2 * ITEM_BORDER_WIDTH}px;
        position: relative;
      }

      .magnifier {
        height: ${MAGNIFIER_EDGE}px;
        width: ${MAGNIFIER_EDGE}px;
        box-sizing: border-box;
        border-radius: 50%;
        border: 2px solid #fff;
        box-shadow: 0px 0px 3px #c0c0c0;
        position: absolute;
        z-index: 2;
      }
      .saturation {
        background: linear-gradient(to right, #fff 0%, transparent 100%);
      }
      .lightness {
        background: linear-gradient(to top, #000 0%, transparent 100%);
      }
      .o-hue-picker {
        border: ${ITEM_BORDER_WIDTH}px solid #c0c0c0;
        box-sizing: border-box;
        width: 100%;
        height: 12px;
        border-radius: 4px;
        background: linear-gradient(
          to right,
          hsl(0 100% 50%) 0%,
          hsl(0.2turn 100% 50%) 20%,
          hsl(0.3turn 100% 50%) 30%,
          hsl(0.4turn 100% 50%) 40%,
          hsl(0.5turn 100% 50%) 50%,
          hsl(0.6turn 100% 50%) 60%,
          hsl(0.7turn 100% 50%) 70%,
          hsl(0.8turn 100% 50%) 80%,
          hsl(0.9turn 100% 50%) 90%,
          hsl(1turn 100% 50%) 100%
        );
        position: relative;
        cursor: crosshair;
      }
      .o-hue-slider {
        margin-top: -3px;
      }
      .o-custom-input-preview {
        padding: 2px 0px;
        display: flex;
        input {
          box-sizing: border-box;
          width: 50%;
          border-radius: 4px;
          padding: 4px 23px 4px 10px;
          height: 24px;
          border: 1px solid #c0c0c0;
          margin-right: 2px;
        }
        .o-wrong-color {
          /** FIXME bootstrap class instead? */
          outline-color: red;
          border-color: red;
          &:focus {
            outline-style: solid;
            outline-width: 1px;
          }
        }
      }
      .o-custom-input-buttons {
        padding: 2px 0px;
        display: flex;
        justify-content: end;
      }
      .o-color-preview {
        border: 1px solid #c0c0c0;
        border-radius: 4px;
        width: 50%;
      }
    }
  }
`;
    class ColorPicker extends owl.Component {
        static template = "o-spreadsheet-ColorPicker";
        static defaultProps = { currentColor: "" };
        static components = { Popover };
        COLORS = COLOR_PICKER_DEFAULTS;
        state = owl.useState({
            showGradient: false,
            currentHslaColor: isColorValid(this.props.currentColor)
                ? { ...hexToHSLA(this.props.currentColor), a: 1 }
                : { h: 0, s: 100, l: 100, a: 1 },
            customHexColor: isColorValid(this.props.currentColor) ? toHex(this.props.currentColor) : "",
        });
        get colorPickerStyle() {
            if (this.props.maxHeight !== undefined && this.props.maxHeight <= 0) {
                return cssPropertiesToCss({ display: "none" });
            }
            return "";
        }
        get popoverProps() {
            return {
                anchorRect: this.props.anchorRect,
                maxHeight: this.props.maxHeight,
                positioning: "BottomLeft",
                verticalOffset: 0,
            };
        }
        get gradientHueStyle() {
            const hue = this.state.currentHslaColor?.h || 0;
            return cssPropertiesToCss({
                background: `hsl(${hue} 100% 50%)`,
            });
        }
        get sliderStyle() {
            const hue = this.state.currentHslaColor?.h || 0;
            const delta = Math.round((hue / 360) * INNER_GRADIENT_WIDTH);
            const left = clip(delta, 1, INNER_GRADIENT_WIDTH) - ICON_EDGE_LENGTH / 2;
            return cssPropertiesToCss({
                "margin-left": `${left}px`,
            });
        }
        get pointerStyle() {
            const { s, l } = this.state.currentHslaColor || { s: 0, l: 0 };
            const left = Math.round(INNER_GRADIENT_WIDTH * clip(s / 100, 0, 1));
            const top = Math.round(INNER_GRADIENT_HEIGHT * clip(1 - (2 * l) / (200 - s), 0, 1));
            return cssPropertiesToCss({
                left: `${-MAGNIFIER_EDGE / 2 + left}px`,
                top: `${-MAGNIFIER_EDGE / 2 + top}px`,
                background: hslaToHex(this.state.currentHslaColor),
            });
        }
        get colorPreviewStyle() {
            return cssPropertiesToCss({
                "background-color": hslaToHex(this.state.currentHslaColor),
            });
        }
        get checkmarkColor() {
            return chartFontColor(this.props.currentColor);
        }
        get isHexColorInputValid() {
            return !this.state.customHexColor || isColorValid(this.state.customHexColor);
        }
        setCustomGradient({ x, y }) {
            const offsetX = clip(x, 0, INNER_GRADIENT_WIDTH);
            const offsetY = clip(y, 0, INNER_GRADIENT_HEIGHT);
            const deltaX = offsetX / INNER_GRADIENT_WIDTH;
            const deltaY = offsetY / INNER_GRADIENT_HEIGHT;
            const s = 100 * deltaX;
            const l = 100 * (1 - deltaY) * (1 - 0.5 * deltaX);
            this.updateColor({ s, l });
        }
        setCustomHue(x) {
            // needs to be capped such that h is in [0°, 359°]
            const h = Math.round(clip((360 * x) / INNER_GRADIENT_WIDTH, 0, 359));
            this.updateColor({ h });
        }
        updateColor(newHsl) {
            this.state.currentHslaColor = { ...this.state.currentHslaColor, ...newHsl };
            this.state.customHexColor = hslaToHex(this.state.currentHslaColor);
        }
        onColorClick(color) {
            if (color) {
                this.props.onColorPicked(toHex(color));
            }
        }
        resetColor() {
            this.props.onColorPicked("");
        }
        toggleColorPicker() {
            this.state.showGradient = !this.state.showGradient;
        }
        dragGradientPointer(ev) {
            const initialGradientCoordinates = { x: ev.offsetX, y: ev.offsetY };
            this.setCustomGradient(initialGradientCoordinates);
            const initialMousePosition = { x: ev.clientX, y: ev.clientY };
            const onMouseMove = (ev) => {
                const currentMousePosition = { x: ev.clientX, y: ev.clientY };
                const deltaX = currentMousePosition.x - initialMousePosition.x;
                const deltaY = currentMousePosition.y - initialMousePosition.y;
                const currentGradientCoordinates = {
                    x: initialGradientCoordinates.x + deltaX,
                    y: initialGradientCoordinates.y + deltaY,
                };
                this.setCustomGradient(currentGradientCoordinates);
            };
            startDnd(onMouseMove, () => { });
        }
        dragHuePointer(ev) {
            const initialX = ev.offsetX;
            const initialMouseX = ev.clientX;
            this.setCustomHue(initialX);
            const onMouseMove = (ev) => {
                const currentMouseX = ev.clientX;
                const deltaX = currentMouseX - initialMouseX;
                const x = initialX + deltaX;
                this.setCustomHue(x);
            };
            startDnd(onMouseMove, () => { });
        }
        setHexColor(ev) {
            // only support HEX code input
            const val = ev.target.value.slice(0, 7);
            this.state.customHexColor = val;
            if (!isColorValid(val)) ;
            else {
                this.state.currentHslaColor = { ...hexToHSLA(val), a: 1 };
            }
        }
        addCustomColor(ev) {
            if (!isHSLAValid(this.state.currentHslaColor) || !isColorValid(this.state.customHexColor)) {
                return;
            }
            this.props.onColorPicked(toHex(this.state.customHexColor));
        }
        isSameColor(color1, color2) {
            return isSameColor(color1, color2);
        }
    }
    ColorPicker.props = {
        onColorPicked: Function,
        currentColor: { type: String, optional: true },
        maxHeight: { type: Number, optional: true },
        anchorRect: Object,
    };

    css /* scss */ `
  .o-color-picker-widget {
    display: flex;
    position: relative;
    align-items: center;

    .o-color-picker-button-style {
      display: flex;
      justify-content: center;
      align-items: center;
      margin: 2px;
      padding: 3px;
      border-radius: 2px;
      cursor: pointer;
      &:not([disabled]):hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }

    .o-color-picker-button {
      height: 30px;
      > span {
        border-bottom: 4px solid;
        height: 16px;
        margin-top: 2px;
      }

      &[disabled] {
        pointer-events: none;
        opacity: 0.3;
      }
    }
  }
`;
    class ColorPickerWidget extends owl.Component {
        static template = "o-spreadsheet-ColorPickerWidget";
        static components = { ColorPicker };
        colorPickerButtonRef = owl.useRef("colorPickerButton");
        get iconStyle() {
            return this.props.currentColor
                ? `border-color: ${this.props.currentColor}`
                : "border-bottom-style: hidden";
        }
        get colorPickerAnchorRect() {
            const button = this.colorPickerButtonRef.el;
            const buttonRect = button.getBoundingClientRect();
            return {
                x: buttonRect.x,
                y: buttonRect.y,
                width: buttonRect.width,
                height: buttonRect.height,
            };
        }
    }
    ColorPickerWidget.props = {
        currentColor: { type: String, optional: true },
        toggleColorPicker: Function,
        showColorPicker: Boolean,
        onColorPicked: Function,
        icon: String,
        title: { type: String, optional: true },
        disabled: { type: Boolean, optional: true },
        dropdownMaxHeight: { type: Number, optional: true },
        class: { type: String, optional: true },
    };

    class LineBarPieDesignPanel extends owl.Component {
        static template = "o-spreadsheet-LineBarPieDesignPanel";
        static components = { ColorPickerWidget };
        state = owl.useState({
            title: "",
            fillColorTool: false,
        });
        onClick(ev) {
            this.state.fillColorTool = false;
        }
        setup() {
            this.state.title = _t(this.props.definition.title);
            owl.useExternalListener(window, "click", this.onClick);
        }
        toggleColorPicker() {
            this.state.fillColorTool = !this.state.fillColorTool;
        }
        updateBackgroundColor(color) {
            this.props.updateChart(this.props.figureId, {
                background: color,
            });
        }
        updateTitle() {
            this.props.updateChart(this.props.figureId, {
                title: this.state.title,
            });
        }
        updateSelect(attr, ev) {
            this.props.updateChart(this.props.figureId, {
                [attr]: ev.target.value,
            });
        }
    }
    LineBarPieDesignPanel.props = {
        figureId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };

    class BarChartDesignPanel extends LineBarPieDesignPanel {
        static template = "o-spreadsheet-BarChartDesignPanel";
    }

    class GaugeChartConfigPanel extends owl.Component {
        static template = "o-spreadsheet-GaugeChartConfigPanel";
        static components = { SelectionInput, ValidationMessages };
        state = owl.useState({
            dataRangeDispatchResult: undefined,
        });
        dataRange = this.props.definition.dataRange;
        get configurationErrorMessages() {
            const cancelledReasons = [...(this.state.dataRangeDispatchResult?.reasons || [])];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        get isDataRangeInvalid() {
            return !!this.state.dataRangeDispatchResult?.isCancelledBecause("InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */);
        }
        onDataRangeChanged(ranges) {
            this.dataRange = ranges[0];
            this.state.dataRangeDispatchResult = this.props.canUpdateChart(this.props.figureId, {
                dataRange: this.dataRange,
            });
        }
        updateDataRange() {
            this.state.dataRangeDispatchResult = this.props.updateChart(this.props.figureId, {
                dataRange: this.dataRange,
            });
        }
        getDataRange() {
            return this.dataRange || "";
        }
    }
    GaugeChartConfigPanel.props = {
        figureId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };

    css /* scss */ `
  .o-gauge-color-set {
    table {
      table-layout: fixed;
      margin-top: 2%;
      display: table;
      text-align: left;
      font-size: 12px;
      line-height: 18px;
      width: 100%;
    }
    th.o-gauge-color-set-colorPicker {
      width: 8%;
    }
    th.o-gauge-color-set-text {
      width: 40%;
    }
    th.o-gauge-color-set-value {
      width: 22%;
    }
    th.o-gauge-color-set-type {
      width: 30%;
    }
    input,
    select {
      width: 100%;
      height: 100%;
      box-sizing: border-box;
    }
  }
`;
    class GaugeChartDesignPanel extends owl.Component {
        static template = "o-spreadsheet-GaugeChartDesignPanel";
        static components = { ColorPickerWidget, ValidationMessages };
        state = owl.useState({
            title: "",
            openedMenu: undefined,
            sectionRuleDispatchResult: undefined,
            sectionRule: deepCopy(this.props.definition.sectionRule),
        });
        setup() {
            this.state.title = _t(this.props.definition.title);
            owl.useExternalListener(window, "click", this.closeMenus);
        }
        get designErrorMessages() {
            const cancelledReasons = [...(this.state.sectionRuleDispatchResult?.reasons || [])];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        updateBackgroundColor(color) {
            this.state.openedMenu = undefined;
            this.props.updateChart(this.props.figureId, {
                background: color,
            });
        }
        updateTitle() {
            this.props.updateChart(this.props.figureId, {
                title: this.state.title,
            });
        }
        isRangeMinInvalid() {
            return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("EmptyGaugeRangeMin" /* CommandResult.EmptyGaugeRangeMin */) ||
                this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMinNaN" /* CommandResult.GaugeRangeMinNaN */) ||
                this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */));
        }
        isRangeMaxInvalid() {
            return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("EmptyGaugeRangeMax" /* CommandResult.EmptyGaugeRangeMax */) ||
                this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMaxNaN" /* CommandResult.GaugeRangeMaxNaN */) ||
                this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeRangeMinBiggerThanRangeMax" /* CommandResult.GaugeRangeMinBiggerThanRangeMax */));
        }
        // ---------------------------------------------------------------------------
        // COLOR_SECTION_TEMPLATE
        // ---------------------------------------------------------------------------
        get isLowerInflectionPointInvalid() {
            return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */) ||
                this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeLowerBiggerThanUpper" /* CommandResult.GaugeLowerBiggerThanUpper */));
        }
        get isUpperInflectionPointInvalid() {
            return !!(this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */) ||
                this.state.sectionRuleDispatchResult?.isCancelledBecause("GaugeLowerBiggerThanUpper" /* CommandResult.GaugeLowerBiggerThanUpper */));
        }
        updateSectionColor(target, color) {
            const sectionRule = deepCopy(this.state.sectionRule);
            sectionRule.colors[target] = color;
            this.updateSectionRule(sectionRule);
            this.closeMenus();
        }
        toggleMenu(menu) {
            const isSelected = this.state.openedMenu === menu;
            this.closeMenus();
            if (!isSelected) {
                this.state.openedMenu = menu;
            }
        }
        updateSectionRule(sectionRule) {
            this.state.sectionRuleDispatchResult = this.props.updateChart(this.props.figureId, {
                sectionRule,
            });
        }
        canUpdateSectionRule(sectionRule) {
            this.state.sectionRuleDispatchResult = this.props.canUpdateChart(this.props.figureId, {
                sectionRule,
            });
        }
        closeMenus() {
            this.state.openedMenu = undefined;
        }
    }
    GaugeChartDesignPanel.props = {
        figureId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };

    class LineConfigPanel extends LineBarPieConfigPanel {
        static template = "o-spreadsheet-LineConfigPanel";
        get canTreatLabelsAsText() {
            const chart = this.env.model.getters.getChart(this.props.figureId);
            if (chart && chart instanceof LineChart) {
                return canChartParseLabels(chart.labelRange, this.env.model.getters);
            }
            return false;
        }
        onUpdateLabelsAsText(ev) {
            this.props.updateChart(this.props.figureId, {
                labelsAsText: ev.target.checked,
            });
        }
        onUpdateStacked(ev) {
            this.props.updateChart(this.props.figureId, {
                stacked: ev.target.checked,
            });
        }
        onUpdateAggregated(ev) {
            this.props.updateChart(this.props.figureId, {
                aggregated: ev.target.checked,
            });
        }
        onUpdateCumulative(ev) {
            this.props.updateChart(this.props.figureId, {
                cumulative: ev.target.checked,
            });
        }
    }

    class LineChartDesignPanel extends LineBarPieDesignPanel {
        static template = "o-spreadsheet-LineChartDesignPanel";
    }

    class ScorecardChartConfigPanel extends owl.Component {
        static template = "o-spreadsheet-ScorecardChartConfigPanel";
        static components = { SelectionInput, ValidationMessages };
        state = owl.useState({
            keyValueDispatchResult: undefined,
            baselineDispatchResult: undefined,
        });
        keyValue = this.props.definition.keyValue;
        baseline = this.props.definition.baseline;
        get errorMessages() {
            const cancelledReasons = [
                ...(this.state.keyValueDispatchResult?.reasons || []),
                ...(this.state.baselineDispatchResult?.reasons || []),
            ];
            return cancelledReasons.map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
        }
        get isKeyValueInvalid() {
            return !!this.state.keyValueDispatchResult?.isCancelledBecause("InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */);
        }
        get isBaselineInvalid() {
            return !!this.state.keyValueDispatchResult?.isCancelledBecause("InvalidScorecardBaseline" /* CommandResult.InvalidScorecardBaseline */);
        }
        onKeyValueRangeChanged(ranges) {
            this.keyValue = ranges[0];
            this.state.keyValueDispatchResult = this.props.canUpdateChart(this.props.figureId, {
                keyValue: this.keyValue,
            });
        }
        updateKeyValueRange() {
            this.state.keyValueDispatchResult = this.props.updateChart(this.props.figureId, {
                keyValue: this.keyValue,
            });
        }
        getKeyValueRange() {
            return this.keyValue || "";
        }
        onBaselineRangeChanged(ranges) {
            this.baseline = ranges[0];
            this.state.baselineDispatchResult = this.props.canUpdateChart(this.props.figureId, {
                baseline: this.baseline,
            });
        }
        updateBaselineRange() {
            this.state.baselineDispatchResult = this.props.updateChart(this.props.figureId, {
                baseline: this.baseline,
            });
        }
        getBaselineRange() {
            return this.baseline || "";
        }
        updateBaselineMode(ev) {
            this.props.updateChart(this.props.figureId, { baselineMode: ev.target.value });
        }
    }
    ScorecardChartConfigPanel.props = {
        figureId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };

    class ScorecardChartDesignPanel extends owl.Component {
        static template = "o-spreadsheet-ScorecardChartDesignPanel";
        static components = { ColorPickerWidget };
        state = owl.useState({
            title: "",
            openedColorPicker: undefined,
        });
        setup() {
            this.state.title = _t(this.props.definition.title);
            owl.useExternalListener(window, "click", this.closeMenus);
        }
        updateTitle() {
            this.props.updateChart(this.props.figureId, {
                title: this.state.title,
            });
        }
        translate(term) {
            return _t(term);
        }
        updateBaselineDescr(ev) {
            this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
        }
        toggleColorPicker(colorPickerId) {
            if (this.state.openedColorPicker === colorPickerId) {
                this.state.openedColorPicker = undefined;
            }
            else {
                this.state.openedColorPicker = colorPickerId;
            }
        }
        setColor(color, colorPickerId) {
            switch (colorPickerId) {
                case "backgroundColor":
                    this.props.updateChart(this.props.figureId, { background: color });
                    break;
                case "baselineColorDown":
                    this.props.updateChart(this.props.figureId, { baselineColorDown: color });
                    break;
                case "baselineColorUp":
                    this.props.updateChart(this.props.figureId, { baselineColorUp: color });
                    break;
            }
            this.closeMenus();
        }
        closeMenus() {
            this.state.openedColorPicker = undefined;
        }
    }
    ScorecardChartDesignPanel.props = {
        figureId: String,
        definition: Object,
        updateChart: Function,
        canUpdateChart: Function,
    };

    const chartSidePanelComponentRegistry = new Registry();
    chartSidePanelComponentRegistry
        .add("line", {
        configuration: LineConfigPanel,
        design: LineChartDesignPanel,
    })
        .add("bar", {
        configuration: BarConfigPanel,
        design: BarChartDesignPanel,
    })
        .add("pie", {
        configuration: LineBarPieConfigPanel,
        design: LineBarPieDesignPanel,
    })
        .add("gauge", {
        configuration: GaugeChartConfigPanel,
        design: GaugeChartDesignPanel,
    })
        .add("scorecard", {
        configuration: ScorecardChartConfigPanel,
        design: ScorecardChartDesignPanel,
    });

    css /* scss */ `
  .o-chart {
    .o-panel {
      display: flex;
      .o-panel-element {
        flex: 1 0 auto;
        padding: 8px 0px;
        text-align: center;
        cursor: pointer;
        border-right: 1px solid darkgray;
        &.inactive {
          background-color: ${BACKGROUND_HEADER_COLOR};
          border-bottom: 1px solid darkgray;
        }
        .fa {
          margin-right: 4px;
        }
      }
      .o-panel-element:last-child {
        border-right: none;
      }
    }
  }
`;
    class ChartPanel extends owl.Component {
        static template = "o-spreadsheet-ChartPanel";
        state;
        get figureId() {
            return this.state.figureId;
        }
        setup() {
            const selectedFigureId = this.env.model.getters.getSelectedFigureId();
            if (!selectedFigureId) {
                this.props.onCloseSidePanel();
                return;
            }
            this.state = owl.useState({
                panel: "configuration",
                figureId: selectedFigureId,
            });
            owl.onWillUpdateProps(() => {
                const selectedFigureId = this.env.model.getters.getSelectedFigureId();
                if (selectedFigureId && selectedFigureId !== this.state.figureId) {
                    this.state.figureId = selectedFigureId;
                }
                if (!this.env.model.getters.isChartDefined(this.figureId)) {
                    this.props.onCloseSidePanel();
                    return;
                }
            });
        }
        updateChart(figureId, updateDefinition) {
            if (figureId !== this.figureId) {
                return;
            }
            const definition = {
                ...this.getChartDefinition(),
                ...updateDefinition,
            };
            return this.env.model.dispatch("UPDATE_CHART", {
                definition,
                id: figureId,
                sheetId: this.env.model.getters.getFigureSheetId(figureId),
            });
        }
        canUpdateChart(figureId, updateDefinition) {
            if (figureId !== this.figureId || !this.env.model.getters.isChartDefined(figureId)) {
                return;
            }
            const definition = {
                ...this.getChartDefinition(),
                ...updateDefinition,
            };
            return this.env.model.canDispatch("UPDATE_CHART", {
                definition,
                id: figureId,
                sheetId: this.env.model.getters.getFigureSheetId(figureId),
            });
        }
        onTypeChange(type) {
            const context = this.env.model.getters.getContextCreationChart(this.figureId);
            if (!context) {
                throw new Error("Chart not defined.");
            }
            const definition = getChartDefinitionFromContextCreation(context, type);
            this.env.model.dispatch("UPDATE_CHART", {
                definition,
                id: this.figureId,
                sheetId: this.env.model.getters.getFigureSheetId(this.figureId),
            });
        }
        get chartPanel() {
            const type = this.env.model.getters.getChartType(this.figureId);
            if (!type) {
                throw new Error("Chart not defined.");
            }
            const chartPanel = chartSidePanelComponentRegistry.get(type);
            if (!chartPanel) {
                throw new Error(`Component is not defined for type ${type}`);
            }
            return chartPanel;
        }
        getChartDefinition(figureId = this.figureId) {
            return this.env.model.getters.getChartDefinition(figureId);
        }
        get chartTypes() {
            return getChartTypes();
        }
        activatePanel(panel) {
            this.state.panel = panel;
        }
    }
    ChartPanel.props = {
        onCloseSidePanel: Function,
    };

    css /* scss */ `
  .o-spreadsheet {
    .o-icon {
      .small-text {
        font: bold 9px sans-serif;
      }
      .heavy-text {
        font: bold 16px sans-serif;
      }
    }
  }
`;
    // -----------------------------------------------------------------------------
    // We need here the svg of the icons that we need to convert to images for the renderer
    // -----------------------------------------------------------------------------
    const ARROW_DOWN = '<svg class="o-cf-icon arrow-down" width="10" height="10" focusable="false" viewBox="0 0 448 512"><path fill="#DC6965" d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z"></path></svg>';
    const ARROW_UP = '<svg class="o-cf-icon arrow-up" width="10" height="10" focusable="false" viewBox="0 0 448 512"><path fill="#00A04A" d="M34.9 289.5l-22.2-22.2c-9.4-9.4-9.4-24.6 0-33.9L207 39c9.4-9.4 24.6-9.4 33.9 0l194.3 194.3c9.4 9.4 9.4 24.6 0 33.9L413 289.4c-9.5 9.5-25 9.3-34.3-.4L264 168.6V456c0 13.3-10.7 24-24 24h-32c-13.3 0-24-10.7-24-24V168.6L69.2 289.1c-9.3 9.8-24.8 10-34.3.4z"></path></svg>';
    const ARROW_RIGHT = '<svg class="o-cf-icon arrow-right" width="10" height="10" focusable="false" viewBox="0 0 448 512"><path fill="#F0AD4E" d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"></path></svg>';
    const SMILE = '<svg class="o-cf-icon smile" width="10" height="10" focusable="false" viewBox="0 0 496 512"><path fill="#00A04A" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm4 72.6c-20.8 25-51.5 39.4-84 39.4s-63.2-14.3-84-39.4c-8.5-10.2-23.7-11.5-33.8-3.1-10.2 8.5-11.5 23.6-3.1 33.8 30 36 74.1 56.6 120.9 56.6s90.9-20.6 120.9-56.6c8.5-10.2 7.1-25.3-3.1-33.8-10.1-8.4-25.3-7.1-33.8 3.1z"></path></svg>';
    const MEH = '<svg class="o-cf-icon meh" width="10" height="10" focusable="false" viewBox="0 0 496 512"><path fill="#F0AD4E" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm8 144H160c-13.2 0-24 10.8-24 24s10.8 24 24 24h176c13.2 0 24-10.8 24-24s-10.8-24-24-24z"></path></svg>';
    const FROWN = '<svg class="o-cf-icon frown" width="10" height="10" focusable="false" viewBox="0 0 496 512"><path fill="#DC6965" d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 448c-110.3 0-200-89.7-200-200S137.7 56 248 56s200 89.7 200 200-89.7 200-200 200zm-80-216c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160-64c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32zm-80 128c-40.2 0-78 17.7-103.8 48.6-8.5 10.2-7.1 25.3 3.1 33.8 10.2 8.4 25.3 7.1 33.8-3.1 16.6-19.9 41-31.4 66.9-31.4s50.3 11.4 66.9 31.4c8.1 9.7 23.1 11.9 33.8 3.1 10.2-8.5 11.5-23.6 3.1-33.8C326 321.7 288.2 304 248 304z"></path></svg>';
    const GREEN_DOT = '<svg class="o-cf-icon green-dot" width="10" height="10" focusable="false" viewBox="0 0 512 512"><path fill="#00A04A" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z"></path></svg>';
    const YELLOW_DOT = '<svg class="o-cf-icon yellow-dot" width="10" height="10" focusable="false" viewBox="0 0 512 512"><path fill="#F0AD4E" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z"></path></svg>';
    const RED_DOT = '<svg class="o-cf-icon red-dot" width="10" height="10" focusable="false" viewBox="0 0 512 512"><path fill="#DC6965" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z"></path></svg>';
    function loadIconImage(svg) {
        /** We have to add xmlns, as it's not added by owl in the canvas */
        svg = `<svg xmlns="http://www.w3.org/2000/svg" ${svg.slice(4)}`;
        const image = new Image();
        image.src = "data:image/svg+xml; charset=utf8, " + encodeURIComponent(svg);
        return image;
    }
    const ICONS = {
        arrowGood: {
            template: "ARROW_UP",
            img: loadIconImage(ARROW_UP),
        },
        arrowNeutral: {
            template: "ARROW_RIGHT",
            img: loadIconImage(ARROW_RIGHT),
        },
        arrowBad: {
            template: "ARROW_DOWN",
            img: loadIconImage(ARROW_DOWN),
        },
        smileyGood: {
            template: "SMILE",
            img: loadIconImage(SMILE),
        },
        smileyNeutral: {
            template: "MEH",
            img: loadIconImage(MEH),
        },
        smileyBad: {
            template: "FROWN",
            img: loadIconImage(FROWN),
        },
        dotGood: {
            template: "GREEN_DOT",
            img: loadIconImage(GREEN_DOT),
        },
        dotNeutral: {
            template: "YELLOW_DOT",
            img: loadIconImage(YELLOW_DOT),
        },
        dotBad: {
            template: "RED_DOT",
            img: loadIconImage(RED_DOT),
        },
    };
    const ICON_SETS = {
        arrows: {
            good: "arrowGood",
            neutral: "arrowNeutral",
            bad: "arrowBad",
        },
        smiley: {
            good: "smileyGood",
            neutral: "smileyNeutral",
            bad: "smileyBad",
        },
        dots: {
            good: "dotGood",
            neutral: "dotNeutral",
            bad: "dotBad",
        },
    };

    css /* scss */ `
  .o-icon-picker {
    position: absolute;
    z-index: ${ComponentsImportance.IconPicker};
    box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
    background-color: white;
    padding: 2px 1px;
  }
  .o-cf-icon-line {
    display: flex;
    padding: 3px 6px;
  }
  .o-icon-picker-item {
    margin: 0px 2px;
    &:hover {
      background-color: rgba(0, 0, 0, 0.08);
      outline: 1px solid gray;
    }
  }
`;
    class IconPicker extends owl.Component {
        static template = "o-spreadsheet-IconPicker";
        icons = ICONS;
        iconSets = ICON_SETS;
        onIconClick(icon) {
            if (icon) {
                this.props.onIconPicked(icon);
            }
        }
    }
    IconPicker.props = {
        onIconPicked: Function,
    };

    function useDragAndDropListItems() {
        let dndHelper;
        const previousCursor = document.body.style.cursor;
        let cleanupFns = [];
        const cleanUp = () => {
            dndHelper = undefined;
            document.body.style.cursor = previousCursor;
            cleanupFns.forEach((fn) => fn());
            cleanupFns = [];
        };
        const start = (direction, args) => {
            const onChange = () => {
                document.body.style.cursor = "move";
                if (!dndHelper)
                    return;
                Object.assign(state.itemsStyle, dndHelper.getItemStyles());
                args.onChange?.();
            };
            state.cancel = () => {
                state.draggedItemId = undefined;
                state.itemsStyle = {};
                document.body.style.cursor = previousCursor;
                args.onCancel?.();
            };
            const onDragEnd = (itemId, indexAtEnd) => {
                state.draggedItemId = undefined;
                state.itemsStyle = {};
                document.body.style.cursor = previousCursor;
                args.onDragEnd?.(itemId, indexAtEnd);
                cleanUp();
            };
            document.body.style.cursor = "move";
            state.draggedItemId = args.draggedItemId;
            const container = direction === "horizontal"
                ? new HorizontalContainer(args.containerEl)
                : new VerticalContainer(args.containerEl);
            dndHelper = new DOMDndHelper({
                ...args,
                container,
                onChange,
                onDragEnd,
                onCancel: state.cancel,
            });
            startDnd(dndHelper.onMouseMove.bind(dndHelper), dndHelper.onMouseUp.bind(dndHelper));
            const onScroll = dndHelper.onScroll.bind(dndHelper);
            args.containerEl.addEventListener("scroll", onScroll);
            cleanupFns.push(() => args.containerEl.removeEventListener("scroll", onScroll));
            cleanupFns.push(dndHelper.destroy.bind(dndHelper));
        };
        owl.onWillUnmount(() => {
            cleanUp();
        });
        const state = owl.useState({
            itemsStyle: {},
            draggedItemId: undefined,
            start,
            cancel: () => { },
        });
        return state;
    }
    class DOMDndHelper {
        draggedItemId;
        items;
        container;
        initialMousePosition;
        currentMousePosition;
        initialScroll;
        minPosition;
        maxPosition;
        edgeScrollIntervalId;
        onChange;
        onCancel;
        onDragEnd;
        /**
         * The dead zone is an area in which the mousemove events are ignored.
         *
         * This is useful when swapping the dragged item with a larger item. After the swap,
         * the mouse is still hovering on the item  we just swapped with. In this case, we don't want
         * a mouse move to trigger another swap the other way around, so we create a dead zone. We will clear
         * the dead zone when the mouse leaves the swapped item.
         */
        deadZone;
        constructor(args) {
            this.items = args.items.map((item) => ({ ...item, positionAtStart: item.position }));
            this.draggedItemId = args.draggedItemId;
            this.container = args.container;
            this.onChange = args.onChange;
            this.onCancel = args.onCancel;
            this.onDragEnd = args.onDragEnd;
            this.initialMousePosition = args.initialMousePosition;
            this.currentMousePosition = args.initialMousePosition;
            this.initialScroll = this.container.scroll;
            this.minPosition = this.items[0].position;
            this.maxPosition =
                this.items[this.items.length - 1].position + this.items[this.items.length - 1].size;
        }
        getItemStyles() {
            const styles = {};
            for (let item of this.items) {
                styles[item.id] = this.getItemStyle(item.id);
            }
            return styles;
        }
        getItemStyle(itemId) {
            const position = this.container.cssPositionProperty;
            const style = {};
            style.position = "relative";
            style[position] = (this.getItemsPositions()[itemId] || 0) + "px";
            style.transition = `${position} 0.5s`;
            style["pointer-events"] = "none";
            if (this.draggedItemId === itemId) {
                style.transition = `${position} 0s`;
                style["z-index"] = "1000";
            }
            return cssPropertiesToCss(style);
        }
        onScroll() {
            this.moveDraggedItemToPosition(this.currentMousePosition + this.scrollOffset);
        }
        onMouseMove(ev) {
            if (ev.button !== 0) {
                this.onCancel();
                return;
            }
            const mousePosition = this.container.getMousePosition(ev);
            this.currentMousePosition = mousePosition;
            if (mousePosition < this.container.start || mousePosition > this.container.end) {
                this.startEdgeScroll(mousePosition < this.container.start ? -1 : 1);
                return;
            }
            else {
                this.stopEdgeScroll();
            }
            this.moveDraggedItemToPosition(mousePosition + this.scrollOffset);
        }
        moveDraggedItemToPosition(position) {
            const hoveredItemIndex = this.getHoveredItemIndex(position, this.items);
            const draggedItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
            const draggedItem = this.items[draggedItemIndex];
            if (this.deadZone && this.isInZone(position, this.deadZone)) {
                this.onChange(this.getItemsPositions());
                return;
            }
            else if (this.isInZone(position, {
                start: draggedItem.position,
                end: draggedItem.position + draggedItem.size,
            })) {
                this.deadZone = undefined;
            }
            if (draggedItemIndex === hoveredItemIndex) {
                this.onChange(this.getItemsPositions());
                return;
            }
            const startIndex = Math.min(draggedItemIndex, hoveredItemIndex);
            const endIndex = Math.max(draggedItemIndex, hoveredItemIndex);
            const direction = Math.sign(hoveredItemIndex - draggedItemIndex);
            let draggedItemMoveSize = 0;
            for (let i = startIndex; i <= endIndex; i++) {
                if (i === draggedItemIndex) {
                    continue;
                }
                this.items[i].position -= direction * draggedItem.size;
                draggedItemMoveSize += this.items[i].size;
            }
            draggedItem.position += direction * draggedItemMoveSize;
            this.items.sort((item1, item2) => item1.position - item2.position);
            this.deadZone =
                direction > 0
                    ? { start: position, end: draggedItem.position }
                    : { start: draggedItem.position + draggedItem.size, end: position };
            this.onChange(this.getItemsPositions());
        }
        onMouseUp(ev) {
            if (ev.button !== 0) {
                this.onCancel();
            }
            ev.stopPropagation();
            ev.preventDefault();
            const targetItemIndex = this.items.findIndex((item) => item.id === this.draggedItemId);
            this.onDragEnd(this.draggedItemId, targetItemIndex);
            this.stopEdgeScroll();
            return false;
        }
        startEdgeScroll(direction) {
            if (this.edgeScrollIntervalId)
                return;
            this.edgeScrollIntervalId = window.setInterval(() => {
                const offset = direction * 3;
                let newPosition = this.currentMousePosition + offset;
                if (newPosition < Math.min(this.container.start, this.minPosition)) {
                    newPosition = Math.min(this.container.start, this.minPosition);
                }
                else if (newPosition > Math.max(this.container.end, this.maxPosition)) {
                    newPosition = Math.max(this.container.end, this.maxPosition);
                }
                this.container.scroll += offset;
            }, 5);
        }
        stopEdgeScroll() {
            window.clearInterval(this.edgeScrollIntervalId);
            this.edgeScrollIntervalId = undefined;
        }
        /**
         * Get the index of the item the given mouse position is inside.
         * If the mouse is outside the container, return the first or last item index.
         */
        getHoveredItemIndex(mousePosition, items) {
            if (mousePosition <= this.minPosition)
                return 0;
            if (mousePosition >= this.maxPosition)
                return items.length - 1;
            return items.findIndex((item) => item.position + item.size >= mousePosition);
        }
        getItemsPositions() {
            const positions = {};
            for (let item of this.items) {
                if (item.id !== this.draggedItemId) {
                    positions[item.id] = item.position - item.positionAtStart;
                    continue;
                }
                const mouseOffset = this.currentMousePosition - this.initialMousePosition;
                let start = mouseOffset + this.scrollOffset;
                start = Math.max(this.minPosition - item.positionAtStart, start);
                start = Math.min(this.maxPosition - item.positionAtStart - item.size, start);
                positions[item.id] = start;
            }
            return positions;
        }
        isInZone(position, zone) {
            return position >= zone.start && position <= zone.end;
        }
        get scrollOffset() {
            return this.container.scroll - this.initialScroll;
        }
        destroy() {
            this.stopEdgeScroll();
        }
    }
    class ContainerWrapper {
        el;
        constructor(el) {
            this.el = el;
        }
        get containerRect() {
            return this.el.getBoundingClientRect();
        }
    }
    class VerticalContainer extends ContainerWrapper {
        get start() {
            return this.containerRect.top;
        }
        get end() {
            return this.containerRect.bottom;
        }
        get cssPositionProperty() {
            return "top";
        }
        get scroll() {
            return this.el.scrollTop;
        }
        set scroll(scroll) {
            this.el.scrollTop = scroll;
        }
        getMousePosition(ev) {
            return ev.clientY;
        }
    }
    class HorizontalContainer extends ContainerWrapper {
        get start() {
            return this.containerRect.left;
        }
        get end() {
            return this.containerRect.right;
        }
        get cssPositionProperty() {
            return "left";
        }
        get scroll() {
            return this.el.scrollLeft;
        }
        set scroll(scroll) {
            this.el.scrollLeft = scroll;
        }
        getMousePosition(ev) {
            return ev.clientX;
        }
    }

    css /* scss */ `
  .o-cf-preview-list {
    .o-cf-preview {
      &.o-cf-cursor-ptr {
        cursor: pointer;
      }

      border-bottom: 1px solid #ccc;
      height: 60px;
      padding: 10px;
      position: relative;
      cursor: pointer;
      &:hover,
      &.o-cf-dragging {
        background-color: #ebebeb;
      }

      &:not(:hover) .o-cf-delete-button {
        display: none;
      }
      .o-cf-preview-icon {
        border: 1px solid lightgrey;
        position: absolute;
        height: 50px;
        width: 50px;
      }
      .o-cf-preview-description {
        left: 65px;
        margin-bottom: auto;
        margin-right: 8px;
        margin-top: auto;
        position: relative;
        width: 142px;
        .o-cf-preview-description-rule {
          margin-bottom: 4px;
          font-weight: 600;
          color: #303030;
          max-height: 2.8em;
          line-height: 1.4em;
        }
        .o-cf-preview-range {
          font-size: 12px;
        }
      }
      .o-cf-delete {
        left: 90%;
        top: 39%;
        position: absolute;
      }
      &:not(:hover):not(.o-cf-dragging) .o-cf-drag-handle {
        display: none !important;
      }
      .o-cf-drag-handle {
        left: -8px;
        cursor: move;
        .o-icon {
          width: 6px;
          height: 30px;
        }
      }
    }
  }
`;
    class ConditionalFormatPreviewList extends owl.Component {
        static template = "o-spreadsheet-ConditionalFormatPreviewList";
        icons = ICONS;
        dragAndDrop = useDragAndDropListItems();
        cfListRef = owl.useRef("cfList");
        setup() {
            owl.onWillUpdateProps((nextProps) => {
                if (!deepEquals(this.props.conditionalFormats, nextProps.conditionalFormats)) {
                    this.dragAndDrop.cancel();
                }
            });
        }
        getPreviewDivStyle(cf) {
            return this.dragAndDrop.itemsStyle[cf.id] || "";
        }
        getPreviewImageStyle(rule) {
            if (rule.type === "CellIsRule") {
                return cssPropertiesToCss(cellStyleToCss(rule.style));
            }
            else if (rule.type === "ColorScaleRule") {
                const minColor = colorNumberString(rule.minimum.color);
                const midColor = rule.midpoint ? colorNumberString(rule.midpoint.color) : null;
                const maxColor = colorNumberString(rule.maximum.color);
                const baseString = "background-image: linear-gradient(to right, ";
                return midColor
                    ? baseString + minColor + ", " + midColor + ", " + maxColor + ")"
                    : baseString + minColor + ", " + maxColor + ")";
            }
            return "";
        }
        getDescription(cf) {
            switch (cf.rule.type) {
                case "CellIsRule":
                    const description = CellIsOperators[cf.rule.operator];
                    if (cf.rule.values.length === 1) {
                        return `${description} ${cf.rule.values[0]}`;
                    }
                    if (cf.rule.values.length === 2) {
                        return _t("%s %s and %s", description, cf.rule.values[0], cf.rule.values[1]);
                    }
                    return description;
                case "ColorScaleRule":
                    return CfTerms.ColorScale;
                case "IconSetRule":
                    return CfTerms.IconSet;
            }
        }
        deleteConditionalFormat(cf) {
            this.env.model.dispatch("REMOVE_CONDITIONAL_FORMAT", {
                id: cf.id,
                sheetId: this.env.model.getters.getActiveSheetId(),
            });
        }
        onMouseDown(cf, event) {
            if (event.button !== 0)
                return;
            const previewRects = Array.from(this.cfListRef.el.children).map((previewEl) => getBoundingRectAsPOJO(previewEl));
            const items = this.props.conditionalFormats.map((cf, index) => ({
                id: cf.id,
                size: previewRects[index].height,
                position: previewRects[index].y,
            }));
            this.dragAndDrop.start("vertical", {
                draggedItemId: cf.id,
                initialMousePosition: event.clientY,
                items: items,
                containerEl: this.cfListRef.el,
                onDragEnd: (cfId, finalIndex) => this.onDragEnd(cfId, finalIndex),
            });
        }
        onDragEnd(cfId, finalIndex) {
            const originalIndex = this.props.conditionalFormats.findIndex((sheet) => sheet.id === cfId);
            const delta = originalIndex - finalIndex;
            if (delta !== 0) {
                this.env.model.dispatch("CHANGE_CONDITIONAL_FORMAT_PRIORITY", {
                    cfId,
                    delta,
                    sheetId: this.env.model.getters.getActiveSheetId(),
                });
            }
        }
    }
    ConditionalFormatPreviewList.props = {
        conditionalFormats: Array,
        onPreviewClick: Function,
        onAddConditionalFormat: Function,
    };

    css /* scss */ `
  label {
    vertical-align: middle;
  }
  .o_cf_radio_item {
    margin-right: 10%;
  }
  .radio input:checked {
    color: #e9ecef;
    border-color: #00a09d;
    background-color: #00a09d;
  }
  .o-cf-editor {
    border-bottom: solid;
    border-color: lightgrey;
  }
  .o-cf {
    .o-cf-type-selector {
      *,
      ::after,
      ::before {
        box-sizing: border-box;
      }
      margin-top: 10px;
      display: flex;
    }
    .o-section-subtitle:first-child {
      margin-top: 0px;
    }
    .o-cf-ruleEditor {
      font-size: 12px;
      line-height: 1.5;
      .o-selection-cf {
        margin-bottom: 3%;
      }
      .o-cell-content {
        font-size: 12px;
        font-weight: 500;
        padding: 0 12px;
        margin: 0;
        line-height: 35px;
      }
    }
    .o-cf-error {
      color: red;
      margin-top: 10px;
    }
  }
  .o-cf-cell-is-rule {
    .o-cf-preview-line {
      border: 1px solid darkgrey;
      padding: 10px;
    }
    .o-cell-is-operator {
      margin-bottom: 5px;
      width: 96%;
    }
    .o-cell-is-value {
      margin-bottom: 5px;
      width: 96%;
    }
    .o-color-picker-widget .o-color-picker-button {
      pointer-events: all;
      cursor: default;
    }
  }
  .o-cf-color-scale-editor {
    .o-threshold {
      display: flex;
      flex-direction: row;
      justify-content: center;
      align-items: center;
      select {
        width: 100%;
      }
      .o-threshold-value {
        margin-left: 2%;
        width: 20%;
        min-width: 0px; // input overflows in Firefox otherwise
      }
      .o-threshold-value:disabled {
        background-color: #edebed;
      }
    }
    .o-cf-preview-gradient {
      border: 1px solid darkgrey;
      padding: 10px;
      border-radius: 4px;
    }
  }
  .o-cf-iconset-rule {
    font-size: 12;
    .o-cf-iconsets {
      display: flex;
      justify-content: space-between;
      .o-cf-iconset {
        border: 1px solid #dadce0;
        border-radius: 4px;
        display: inline-flex;
        padding: 5px 8px;
        width: 25%;
        cursor: pointer;
        justify-content: space-between;
        .o-cf-icon {
          display: inline;
          margin-left: 1%;
          margin-right: 1%;
        }
        svg {
          vertical-align: baseline;
        }
      }
      .o-cf-iconset:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }
    .o-inflection {
      .o-cf-icon-button {
        display: inline-block;
        border: 1px solid #dadce0;
        border-radius: 4px;
        cursor: pointer;
        padding: 1px 2px;
      }
      .o-cf-icon-button:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
      table {
        table-layout: fixed;
        margin-top: 2%;
        display: table;
        text-align: left;
        font-size: 12px;
        line-height: 18px;
        width: 100%;
      }
      th.o-cf-iconset-icons {
        width: 8%;
      }
      th.o-cf-iconset-text {
        width: 28%;
      }
      th.o-cf-iconset-operator {
        width: 14%;
      }
      th.o-cf-iconset-type {
        width: 28%;
      }
      th.o-cf-iconset-value {
        width: 26%;
      }
      input,
      select {
        width: 100%;
        height: 100%;
        box-sizing: border-box;
      }
    }
    .o-cf-iconset-reverse {
      margin-bottom: 2%;
      margin-top: 2%;
      .o-cf-label {
        display: inline-block;
        vertical-align: bottom;
        margin-bottom: 2px;
      }
    }
  }
`;
    class ConditionalFormattingEditor extends owl.Component {
        static template = "o-spreadsheet-ConditionalFormattingEditor";
        static components = {
            SelectionInput,
            IconPicker,
            ColorPickerWidget,
            ConditionalFormatPreviewList,
        };
        icons = ICONS;
        cellIsOperators = CellIsOperators;
        iconSets = ICON_SETS;
        getTextDecoration = getTextDecoration;
        colorNumberString = colorNumberString;
        state;
        setup() {
            const cf = this.props.editedCf || {
                id: this.env.model.uuidGenerator.uuidv4(),
                ranges: this.env.model.getters
                    .getSelectedZones()
                    .map((zone) => this.env.model.getters.zoneToXC(this.env.model.getters.getActiveSheetId(), zone)),
            };
            this.state = owl.useState({
                currentCF: cf,
                currentCFType: this.props.editedCf?.rule.type || "CellIsRule",
                errors: [],
                rules: this.getDefaultRules(),
            });
            if (this.props.editedCf) {
                switch (this.props.editedCf.rule.type) {
                    case "CellIsRule":
                        this.state.rules.cellIs = this.props.editedCf.rule;
                        break;
                    case "ColorScaleRule":
                        this.state.rules.colorScale = this.props.editedCf.rule;
                        break;
                    case "IconSetRule":
                        this.state.rules.iconSet = this.props.editedCf.rule;
                        break;
                }
            }
            owl.useExternalListener(window, "click", this.closeMenus);
        }
        get isRangeValid() {
            return this.state.errors.includes("EmptyRange" /* CommandResult.EmptyRange */);
        }
        errorMessage(error) {
            return CfTerms.Errors[error] || CfTerms.Errors.Unexpected;
        }
        saveConditionalFormat() {
            if (this.state.currentCF) {
                const invalidRanges = this.state.currentCF.ranges.some((xc) => !xc.match(rangeReference));
                if (invalidRanges) {
                    this.state.errors = ["InvalidRange" /* CommandResult.InvalidRange */];
                    return;
                }
                const sheetId = this.env.model.getters.getActiveSheetId();
                const locale = this.env.model.getters.getLocale();
                const result = this.env.model.dispatch("ADD_CONDITIONAL_FORMAT", {
                    cf: {
                        rule: canonicalizeCFRule(this.getEditorRule(), locale),
                        id: this.state.currentCF.id,
                    },
                    ranges: this.state.currentCF.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
                    sheetId,
                });
                if (!result.isSuccessful) {
                    this.state.errors = result.reasons;
                }
                else {
                    this.props.onExitEdition();
                }
            }
        }
        /**
         * Get the rule currently edited with the editor
         */
        getEditorRule() {
            switch (this.state.currentCFType) {
                case "CellIsRule":
                    return this.state.rules.cellIs;
                case "ColorScaleRule":
                    return this.state.rules.colorScale;
                case "IconSetRule":
                    return this.state.rules.iconSet;
            }
        }
        getDefaultRules() {
            return {
                cellIs: {
                    type: "CellIsRule",
                    operator: "IsNotEmpty",
                    values: [],
                    style: { fillColor: "#b6d7a8" },
                },
                colorScale: {
                    type: "ColorScaleRule",
                    minimum: { type: "value", color: 0xffffff },
                    midpoint: undefined,
                    maximum: { type: "value", color: 0x6aa84f },
                },
                iconSet: {
                    type: "IconSetRule",
                    icons: {
                        upper: "arrowGood",
                        middle: "arrowNeutral",
                        lower: "arrowBad",
                    },
                    upperInflectionPoint: {
                        type: "percentage",
                        value: "66",
                        operator: "gt",
                    },
                    lowerInflectionPoint: {
                        type: "percentage",
                        value: "33",
                        operator: "gt",
                    },
                },
            };
        }
        changeRuleType(ruleType) {
            if (this.state.currentCFType === ruleType || !this.state.rules) {
                return;
            }
            this.state.errors = [];
            this.state.currentCFType = ruleType;
        }
        onRangesChanged(ranges) {
            if (this.state.currentCF) {
                this.state.currentCF.ranges = ranges;
            }
        }
        /*****************************************************************************
         * Common
         ****************************************************************************/
        toggleMenu(menu) {
            const isSelected = this.state.openedMenu === menu;
            this.closeMenus();
            if (!isSelected) {
                this.state.openedMenu = menu;
            }
        }
        closeMenus() {
            this.state.openedMenu = undefined;
        }
        /*****************************************************************************
         * Cell Is Rule
         ****************************************************************************/
        get isValue1Invalid() {
            return !!this.state.errors?.includes("FirstArgMissing" /* CommandResult.FirstArgMissing */);
        }
        get isValue2Invalid() {
            return !!this.state.errors?.includes("SecondArgMissing" /* CommandResult.SecondArgMissing */);
        }
        toggleStyle(tool) {
            const style = this.state.rules.cellIs.style;
            style[tool] = !style[tool];
            this.closeMenus();
        }
        onKeydown(event) {
            if (event.key === "F4") {
                const target = event.target;
                const update = this.env.model.getters.getCycledReference({ start: target.selectionStart ?? 0, end: target.selectionEnd ?? 0 }, target.value);
                if (!update) {
                    return;
                }
                target.value = update.content;
                target.setSelectionRange(update.selection.start, update.selection.end);
                target.dispatchEvent(new Event("input"));
            }
        }
        setColor(target, color) {
            this.state.rules.cellIs.style[target] = color;
            this.closeMenus();
        }
        /*****************************************************************************
         * Color Scale Rule
         ****************************************************************************/
        isValueInvalid(threshold) {
            switch (threshold) {
                case "minimum":
                    return (this.state.errors.includes("MinInvalidFormula" /* CommandResult.MinInvalidFormula */) ||
                        this.state.errors.includes("MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */) ||
                        this.state.errors.includes("MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */) ||
                        this.state.errors.includes("MinNaN" /* CommandResult.MinNaN */));
                case "midpoint":
                    return (this.state.errors.includes("MidInvalidFormula" /* CommandResult.MidInvalidFormula */) ||
                        this.state.errors.includes("MidNaN" /* CommandResult.MidNaN */) ||
                        this.state.errors.includes("MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */));
                case "maximum":
                    return (this.state.errors.includes("MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */) ||
                        this.state.errors.includes("MaxNaN" /* CommandResult.MaxNaN */));
                default:
                    return false;
            }
        }
        setColorScaleColor(target, color) {
            const point = this.state.rules.colorScale[target];
            if (point) {
                point.color = Number.parseInt(color.substr(1), 16);
            }
            this.closeMenus();
        }
        getPreviewGradient() {
            const rule = this.state.rules.colorScale;
            const minColor = colorNumberString(rule.minimum.color);
            const midColor = colorNumberString(rule.midpoint?.color || DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
            const maxColor = colorNumberString(rule.maximum.color);
            const baseString = "background-image: linear-gradient(to right, ";
            return rule.midpoint === undefined
                ? baseString + minColor + ", " + maxColor + ")"
                : baseString + minColor + ", " + midColor + ", " + maxColor + ")";
        }
        getThresholdColor(threshold) {
            return threshold
                ? colorNumberString(threshold.color)
                : colorNumberString(DEFAULT_COLOR_SCALE_MIDPOINT_COLOR);
        }
        onMidpointChange(ev) {
            const type = ev.target.value;
            const rule = this.state.rules.colorScale;
            if (type === "none") {
                rule.midpoint = undefined;
            }
            else {
                rule.midpoint = {
                    color: DEFAULT_COLOR_SCALE_MIDPOINT_COLOR,
                    value: "",
                    ...rule.midpoint,
                    type,
                };
            }
        }
        /*****************************************************************************
         * Icon Set
         ****************************************************************************/
        isInflectionPointInvalid(inflectionPoint) {
            switch (inflectionPoint) {
                case "lowerInflectionPoint":
                    return (this.state.errors.includes("ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */) ||
                        this.state.errors.includes("ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */) ||
                        this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
                case "upperInflectionPoint":
                    return (this.state.errors.includes("ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */) ||
                        this.state.errors.includes("ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */) ||
                        this.state.errors.includes("LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */));
                default:
                    return true;
            }
        }
        reverseIcons() {
            const icons = this.state.rules.iconSet.icons;
            const upper = icons.upper;
            icons.upper = icons.lower;
            icons.lower = upper;
        }
        setIconSet(iconSet) {
            const icons = this.state.rules.iconSet.icons;
            icons.upper = this.iconSets[iconSet].good;
            icons.middle = this.iconSets[iconSet].neutral;
            icons.lower = this.iconSets[iconSet].bad;
        }
        setIcon(target, icon) {
            this.state.rules.iconSet.icons[target] = icon;
        }
    }
    ConditionalFormattingEditor.props = {
        editedCf: { type: Object, optional: true },
        onExitEdition: Function,
    };

    class ConditionalFormattingPanel extends owl.Component {
        static template = "o-spreadsheet-ConditionalFormattingPanel";
        static components = {
            ConditionalFormatPreviewList,
            ConditionalFormattingEditor,
        };
        activeSheetId;
        state = owl.useState({
            mode: "list",
        });
        setup() {
            this.activeSheetId = this.env.model.getters.getActiveSheetId();
            const sheetId = this.env.model.getters.getActiveSheetId();
            const rules = this.env.model.getters.getRulesSelection(sheetId, this.props.selection || []);
            if (rules.length === 1) {
                const cf = this.conditionalFormats.find((c) => c.id === rules[0]);
                if (cf) {
                    this.editConditionalFormat(cf);
                }
            }
            owl.onWillUpdateProps((nextProps) => {
                const newActiveSheetId = this.env.model.getters.getActiveSheetId();
                if (newActiveSheetId !== this.activeSheetId) {
                    this.activeSheetId = newActiveSheetId;
                    this.switchToList();
                }
                else if (nextProps.selection !== this.props.selection) {
                    const sheetId = this.env.model.getters.getActiveSheetId();
                    const rules = this.env.model.getters.getRulesSelection(sheetId, nextProps.selection || []);
                    if (rules.length === 1) {
                        const cf = this.conditionalFormats.find((c) => c.id === rules[0]);
                        if (cf) {
                            this.editConditionalFormat(cf);
                        }
                    }
                    else {
                        this.switchToList();
                    }
                }
            });
        }
        get conditionalFormats() {
            const cfs = this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
            return cfs.map((cf) => ({
                ...cf,
                rule: localizeCFRule(cf.rule, this.env.model.getters.getLocale()),
            }));
        }
        switchToList() {
            this.state.mode = "list";
            this.state.editedCf = undefined;
        }
        addConditionalFormat() {
            this.state.mode = "edit";
        }
        editConditionalFormat(cf) {
            this.state.mode = "edit";
            this.state.editedCf = cf;
        }
    }
    ConditionalFormattingPanel.props = {
        selection: { type: Object, optional: true },
        onCloseSidePanel: Function,
    };

    css /* scss */ `
  .o-custom-currency {
    .o-format-proposals {
      color: black;
    }
  }
`;
    class CustomCurrencyPanel extends owl.Component {
        static template = "o-spreadsheet-CustomCurrencyPanel";
        availableCurrencies;
        state;
        setup() {
            this.availableCurrencies = [];
            this.state = owl.useState({
                selectedCurrencyIndex: 0,
                currencyCode: "",
                currencySymbol: "",
                selectedFormatIndex: 0,
            });
            owl.onWillStart(() => this.updateAvailableCurrencies());
        }
        get formatProposals() {
            const currency = this.availableCurrencies[this.state.selectedCurrencyIndex];
            const position = currency.position;
            const opposite = currency.position === "before" ? "after" : "before";
            const symbol = this.state.currencySymbol.trim() ? this.state.currencySymbol : "";
            const code = this.state.currencyCode.trim() ? this.state.currencyCode : "";
            const decimalPlaces = currency.decimalPlaces;
            if (!symbol && !code) {
                return [];
            }
            const simple = symbol ? createCurrencyFormat({ symbol, position, decimalPlaces }) : "";
            const rounded = simple ? roundFormat(simple) : "";
            const simpleWithCode = createCurrencyFormat({ symbol, position, decimalPlaces, code });
            const roundedWithCode = roundFormat(simpleWithCode);
            const simpleOpposite = symbol
                ? createCurrencyFormat({ symbol, position: opposite, decimalPlaces })
                : "";
            const roundedOpposite = simpleOpposite ? roundFormat(simpleOpposite) : "";
            const simpleOppositeWithCode = createCurrencyFormat({
                symbol,
                position: opposite,
                decimalPlaces,
                code,
            });
            const roundedOppositeWithCode = roundFormat(simpleOppositeWithCode);
            const formats = new Set([
                rounded,
                simple,
                roundedWithCode,
                simpleWithCode,
                roundedOpposite,
                simpleOpposite,
                roundedOppositeWithCode,
                simpleOppositeWithCode,
            ]);
            return [...formats]
                .filter((format) => format !== "")
                .map((format) => ({
                format,
                example: formatValue(1000.0, { format, locale: this.env.model.getters.getLocale() }),
            }));
        }
        get isSameFormat() {
            const selectedFormat = this.formatProposals[this.state.selectedFormatIndex];
            return selectedFormat ? selectedFormat.format === this.getCommonFormat() : false;
        }
        async updateAvailableCurrencies() {
            if (currenciesRegistry.getAll().length === 0) {
                const currencies = (await this.env.loadCurrencies?.()) || [];
                currencies.forEach((currency, index) => {
                    currenciesRegistry.add(index.toString(), currency);
                });
            }
            const emptyCurrency = {
                name: _t(CustomCurrencyTerms.Custom),
                code: "",
                symbol: "",
                decimalPlaces: 2,
                position: "after",
            };
            this.availableCurrencies = [emptyCurrency, ...currenciesRegistry.getAll()];
        }
        updateSelectCurrency(ev) {
            const target = ev.target;
            this.state.selectedCurrencyIndex = parseInt(target.value, 10);
            const currency = this.availableCurrencies[this.state.selectedCurrencyIndex];
            this.state.currencyCode = currency.code;
            this.state.currencySymbol = currency.symbol;
        }
        updateCode(ev) {
            const target = ev.target;
            this.state.currencyCode = target.value;
            this.initAvailableCurrencies();
        }
        updateSymbol(ev) {
            const target = ev.target;
            this.state.currencySymbol = target.value;
            this.initAvailableCurrencies();
        }
        updateSelectFormat(ev) {
            const target = ev.target;
            this.state.selectedFormatIndex = parseInt(target.value, 10);
        }
        apply() {
            const selectedFormat = this.formatProposals[this.state.selectedFormatIndex];
            this.env.model.dispatch("SET_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                format: selectedFormat.format,
            });
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        initAvailableCurrencies() {
            this.state.selectedCurrencyIndex = 0;
        }
        getCommonFormat() {
            const selectedZones = this.env.model.getters.getSelectedZones();
            const sheetId = this.env.model.getters.getActiveSheetId();
            const cells = selectedZones
                .map((zone) => this.env.model.getters.getEvaluatedCellsInZone(sheetId, zone))
                .flat();
            const firstFormat = cells[0].format;
            return cells.every((cell) => cell.format === firstFormat) ? firstFormat : undefined;
        }
        currencyDisplayName(currency) {
            return currency.name + (currency.code ? ` (${currency.code})` : "");
        }
    }
    CustomCurrencyPanel.props = {
        onCloseSidePanel: Function,
    };

    css /* scss */ `
  .o-find-and-replace {
    outline: none;
    height: 100%;
    .o-input-search-container {
      display: flex;
      .o-input-with-count {
        flex-grow: 1;
        width: auto;
      }
      .o-input-without-count {
        width: 100%;
      }
      .o-input-count {
        width: fit-content;
        padding: 4px 0 4px 4px;
      }
    }
  }
`;
    class FindAndReplacePanel extends owl.Component {
        static template = "o-spreadsheet-FindAndReplacePanel";
        state = owl.useState(this.initialState());
        debounceTimeoutId;
        showFormulaState = false;
        searchInput = owl.useRef("searchInput");
        get hasSearchResult() {
            return this.env.model.getters.getCurrentSelectedMatchIndex() !== null;
        }
        get pendingSearch() {
            return this.debounceTimeoutId !== undefined;
        }
        setup() {
            this.showFormulaState = this.env.model.getters.shouldShowFormulas();
            owl.onMounted(() => this.searchInput.el?.focus());
            owl.onWillUnmount(() => {
                clearTimeout(this.debounceTimeoutId);
                this.env.model.dispatch("CLEAR_SEARCH");
                this.env.model.dispatch("SET_FORMULA_VISIBILITY", { show: this.showFormulaState });
            });
            owl.useEffect(() => {
                this.state.searchOptions.searchFormulas = this.env.model.getters.shouldShowFormulas();
                this.searchFormulas();
            }, () => [this.env.model.getters.shouldShowFormulas()]);
        }
        onInput(ev) {
            this.state.toSearch = ev.target.value;
            this.debouncedUpdateSearch();
        }
        onKeydownSearch(ev) {
            if (ev.key === "Enter") {
                ev.preventDefault();
                ev.stopPropagation();
                this.onSelectNextCell();
            }
        }
        onKeydownReplace(ev) {
            if (ev.key === "Enter") {
                ev.preventDefault();
                ev.stopPropagation();
                this.replace();
            }
        }
        searchFormulas() {
            this.env.model.dispatch("SET_FORMULA_VISIBILITY", {
                show: this.state.searchOptions.searchFormulas,
            });
            this.updateSearch();
        }
        onSelectPreviousCell() {
            this.env.model.dispatch("SELECT_SEARCH_PREVIOUS_MATCH");
        }
        onSelectNextCell() {
            this.env.model.dispatch("SELECT_SEARCH_NEXT_MATCH");
        }
        updateSearch() {
            this.env.model.dispatch("UPDATE_SEARCH", {
                toSearch: this.state.toSearch,
                searchOptions: this.state.searchOptions,
            });
        }
        debouncedUpdateSearch() {
            clearTimeout(this.debounceTimeoutId);
            this.debounceTimeoutId = setTimeout(() => {
                this.updateSearch();
                this.debounceTimeoutId = undefined;
            }, 200);
        }
        replace() {
            this.env.model.dispatch("REPLACE_SEARCH", {
                replaceWith: this.state.replaceWith,
            });
        }
        replaceAll() {
            this.env.model.dispatch("REPLACE_ALL_SEARCH", {
                replaceWith: this.state.replaceWith,
            });
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        initialState() {
            return {
                toSearch: "",
                replaceWith: "",
                searchOptions: {
                    matchCase: false,
                    exactMatch: false,
                    searchFormulas: false,
                },
            };
        }
    }
    FindAndReplacePanel.props = {
        onCloseSidePanel: Function,
    };

    css /* scss */ `
  .o-more-formats-panel {
    .format-preview {
      height: 48px;
      background-color: white;

      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }
    .check-icon {
      width: 24px;
    }
  }
`;
    const DATE_FORMAT_ACTIONS = createActions([
        formatNumberFullDateTime,
        formatNumberFullWeekDayAndMonth,
        formatNumberDayAndFullMonth,
        formatNumberShortWeekDay,
        formatNumberDayAndShortMonth,
        formatNumberFullMonth,
        formatNumberShortMonth,
        formatNumberDate,
        formatNumberTime,
        formatNumberDateTime,
        formatNumberDuration,
    ]);
    class MoreFormatsPanel extends owl.Component {
        static template = "o-spreadsheet-MoreFormatsPanel";
        get dateFormatsActions() {
            return DATE_FORMAT_ACTIONS;
        }
    }
    MoreFormatsPanel.props = {
        onCloseSidePanel: Function,
    };

    css /* scss */ `
  .o-checkbox-selection {
    height: 150px;
  }
`;
    class RemoveDuplicatesPanel extends owl.Component {
        static template = "o-spreadsheet-RemoveDuplicatesPanel";
        static components = { ValidationMessages };
        state = owl.useState({
            hasHeader: false,
            columns: {},
        });
        setup() {
            owl.onWillUpdateProps(() => this.updateColumns());
        }
        toggleHasHeader() {
            this.state.hasHeader = !this.state.hasHeader;
        }
        toggleAllColumns() {
            const newState = !this.isEveryColumnSelected;
            for (const index in this.state.columns) {
                this.state.columns[index] = newState;
            }
        }
        toggleColumn(colIndex) {
            this.state.columns[colIndex] = !this.state.columns[colIndex];
        }
        onRemoveDuplicates() {
            this.env.model.dispatch("REMOVE_DUPLICATES", {
                hasHeader: this.state.hasHeader,
                columns: this.getColsToAnalyze(),
            });
        }
        getColLabel(colKey) {
            const col = parseInt(colKey);
            let colLabel = _t("Column %s", numberToLetters(col));
            if (this.state.hasHeader) {
                const sheetId = this.env.model.getters.getActiveSheetId();
                const row = this.env.model.getters.getSelectedZone().top;
                const colHeader = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
                if (colHeader.type !== "empty") {
                    colLabel += ` - ${colHeader.value}`;
                }
            }
            return colLabel;
        }
        get isEveryColumnSelected() {
            return Object.values(this.state.columns).every((value) => value === true);
        }
        get errorMessages() {
            const cancelledReasons = this.env.model.canDispatch("REMOVE_DUPLICATES", {
                hasHeader: this.state.hasHeader,
                columns: this.getColsToAnalyze(),
            }).reasons;
            const errors = new Set();
            for (const reason of cancelledReasons) {
                errors.add(RemoveDuplicateTerms.Errors[reason] || RemoveDuplicateTerms.Errors.Unexpected);
            }
            return Array.from(errors);
        }
        get selectionStatisticalInformation() {
            const dimension = zoneToDimension(this.env.model.getters.getSelectedZone());
            return _t("%(row_count)s rows and %(column_count)s columns selected", {
                row_count: dimension.numberOfRows,
                column_count: dimension.numberOfCols,
            });
        }
        get canConfirm() {
            return this.errorMessages.length === 0;
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        updateColumns() {
            const zone = this.env.model.getters.getSelectedZone();
            const oldColumns = this.state.columns;
            const newColumns = {};
            for (let i = zone.left; i <= zone.right; i++) {
                newColumns[i] = i in oldColumns ? oldColumns[i] : true;
            }
            this.state.columns = newColumns;
        }
        getColsToAnalyze() {
            return Object.keys(this.state.columns)
                .filter((colIndex) => this.state.columns[colIndex])
                .map((colIndex) => parseInt(colIndex));
        }
    }

    css /* scss */ `
  .o-locale-preview {
    color: dimgrey;
  }
`;
    class SettingsPanel extends owl.Component {
        static template = "o-spreadsheet-SettingsPanel";
        loadedLocales = [];
        setup() {
            owl.onWillStart(() => this.loadLocales());
        }
        onLocaleChange(code) {
            const locale = this.loadedLocales.find((l) => l.code === code);
            if (!locale)
                return;
            this.env.model.dispatch("UPDATE_LOCALE", { locale });
        }
        async loadLocales() {
            this.loadedLocales = (await this.env.loadLocales())
                .filter(isValidLocale)
                .sort((a, b) => a.name.localeCompare(b.name));
        }
        get numberFormatPreview() {
            const locale = this.env.model.getters.getLocale();
            return formatValue(1234567.89, { format: "#,##0.00", locale });
        }
        get dateFormatPreview() {
            const locale = this.env.model.getters.getLocale();
            return formatValue(1.6, { format: locale.dateFormat, locale });
        }
        get dateTimeFormatPreview() {
            const locale = this.env.model.getters.getLocale();
            const dateTimeFormat = getDateTimeFormat(locale);
            return formatValue(1.6, { format: dateTimeFormat, locale });
        }
        get currentLocale() {
            return this.env.model.getters.getLocale();
        }
        get supportedLocales() {
            const currentLocale = this.currentLocale;
            const localeInLoadedLocales = this.loadedLocales.find((l) => l.code === currentLocale.code);
            if (!localeInLoadedLocales) {
                const locales = [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
                return locales;
            }
            else if (!deepEquals(currentLocale, localeInLoadedLocales)) {
                const index = this.loadedLocales.indexOf(localeInLoadedLocales);
                const locales = [...this.loadedLocales];
                locales[index] = currentLocale;
                locales.sort((a, b) => a.name.localeCompare(b.name));
                return locales;
            }
            return this.loadedLocales;
        }
    }
    SettingsPanel.props = {
        onCloseSidePanel: Function,
    };

    const SplitToColumnsInteractiveContent = {
        SplitIsDestructive: _t("This will overwrite data in the subsequent columns. Split anyway?"),
    };
    function interactiveSplitToColumns(env, separator, addNewColumns) {
        let result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", { separator, addNewColumns });
        if (result.isCancelledBecause("SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */)) {
            env.askConfirmation(SplitToColumnsInteractiveContent.SplitIsDestructive, () => {
                result = env.model.dispatch("SPLIT_TEXT_INTO_COLUMNS", {
                    separator,
                    addNewColumns,
                    force: true,
                });
            });
        }
        return result;
    }

    const dataValidationEvaluatorRegistry = new Registry();
    dataValidationEvaluatorRegistry.add("textContains", {
        type: "textContains",
        isValueValid: (value, criterion) => {
            const strValue = String(value);
            return strValue.toLowerCase().includes(criterion.values[0].toLowerCase());
        },
        getErrorString: (criterion) => {
            return _t('The value must be a text that contains "%s"', criterion.values[0]);
        },
        isCriterionValueValid: (value) => !!value,
        criterionValueErrorString: DVTerms.CriterionError.notEmptyValue,
        numberOfValues: () => 1,
        name: _t("Text contains"),
        getPreview: (criterion) => _t('Text contains "%s"', criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("textNotContains", {
        type: "textNotContains",
        isValueValid: (value, criterion) => {
            const strValue = String(value);
            return !strValue.toLowerCase().includes(criterion.values[0].toLowerCase());
        },
        getErrorString: (criterion) => {
            return _t('The value must be a text that does not contain "%s"', criterion.values[0]);
        },
        isCriterionValueValid: (value) => !!value,
        criterionValueErrorString: DVTerms.CriterionError.notEmptyValue,
        numberOfValues: () => 1,
        name: _t("Text does not contains"),
        getPreview: (criterion) => _t('Text does not contain "%s"', criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("textIs", {
        type: "textIs",
        isValueValid: (value, criterion) => {
            const strValue = String(value);
            return strValue.toLowerCase() === criterion.values[0].toLowerCase();
        },
        getErrorString: (criterion) => {
            return _t('The value must be exactly "%s"', criterion.values[0]);
        },
        isCriterionValueValid: (value) => !!value,
        criterionValueErrorString: DVTerms.CriterionError.notEmptyValue,
        numberOfValues: () => 1,
        name: _t("Text is exactly"),
        getPreview: (criterion) => _t('Text is exactly "%s"', criterion.values[0]),
    });
    /** Note: this regex doesn't allow for all the RFC-compliant mail addresses but should be enough for our purpose. */
    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/;
    dataValidationEvaluatorRegistry.add("textIsEmail", {
        type: "textIsEmail",
        isValueValid: (value) => typeof value === "string" && emailRegex.test(value),
        getErrorString: () => _t("The value must be a valid email address"),
        isCriterionValueValid: () => true,
        criterionValueErrorString: "",
        numberOfValues: () => 0,
        name: _t("Text is valid email"),
        getPreview: () => _t("Text is valid email"),
    });
    dataValidationEvaluatorRegistry.add("textIsLink", {
        type: "textIsLink",
        isValueValid: (value) => detectLink(value) !== undefined,
        getErrorString: () => _t("The value must be a valid link"),
        isCriterionValueValid: () => true,
        criterionValueErrorString: "",
        numberOfValues: () => 0,
        name: _t("Text is valid link"),
        getPreview: () => _t("Text is valid link"),
    });
    dataValidationEvaluatorRegistry.add("dateIs", {
        type: "dateIs",
        isValueValid: (value, criterion) => {
            const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];
            const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);
            if (dateValue === undefined || criterionValue === undefined) {
                return false;
            }
            if (["lastWeek", "lastMonth", "lastYear"].includes(criterion.dateValue)) {
                const today = jsDateToRoundNumber(DateTime.now());
                return isDateBetween(dateValue, today, criterionValue);
            }
            return areDatesSameDay(dateValue, criterionValue);
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            return criterion.dateValue === "exactDate"
                ? _t("The value must be the date %s", getDateCriterionLocalizedValues(criterion, locale)[0])
                : _t("The value must be %s", DVTerms.DateIs[criterion.dateValue]);
        },
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: DVTerms.CriterionError.dateValue,
        numberOfValues: (criterion) => (criterion.dateValue === "exactDate" ? 1 : 0),
        name: _t("Date is"),
        getPreview: (criterion, getters) => {
            return criterion.dateValue === "exactDate"
                ? _t("Date is %s", getDateCriterionFormattedValues(criterion, getters)[0])
                : _t("Date is %s", DVTerms.DateIs[criterion.dateValue]);
        },
    });
    dataValidationEvaluatorRegistry.add("dateIsBefore", {
        type: "dateIsBefore",
        isValueValid: (value, criterion) => {
            const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];
            const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);
            return (dateValue !== undefined &&
                criterionValue !== undefined &&
                isDateStrictlyBefore(dateValue, criterionValue));
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            return criterion.dateValue === "exactDate"
                ? _t("The value must be a date before %s", getDateCriterionLocalizedValues(criterion, locale)[0])
                : _t("The value must be a date before %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: DVTerms.CriterionError.dateValue,
        numberOfValues: (criterion) => (criterion.dateValue === "exactDate" ? 1 : 0),
        name: _t("Date is before"),
        getPreview: (criterion, getters) => {
            return criterion.dateValue === "exactDate"
                ? _t("Date is before %s", getDateCriterionFormattedValues(criterion, getters)[0])
                : _t("Date is before %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
    });
    dataValidationEvaluatorRegistry.add("dateIsOnOrBefore", {
        type: "dateIsOnOrBefore",
        isValueValid: (value, criterion) => {
            const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];
            const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);
            return (dateValue !== undefined &&
                criterionValue !== undefined &&
                isDateBefore(dateValue, criterionValue));
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            return criterion.dateValue === "exactDate"
                ? _t("The value must be a date on or before %s", getDateCriterionLocalizedValues(criterion, locale)[0])
                : _t("The value must be a date on or before %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: DVTerms.CriterionError.dateValue,
        numberOfValues: (criterion) => (criterion.dateValue === "exactDate" ? 1 : 0),
        name: _t("Date is on or before"),
        getPreview: (criterion, getters) => {
            return criterion.dateValue === "exactDate"
                ? _t("Date is on or before %s", getDateCriterionFormattedValues(criterion, getters)[0])
                : _t("Date is on or before %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
    });
    dataValidationEvaluatorRegistry.add("dateIsAfter", {
        type: "dateIsAfter",
        isValueValid: (value, criterion) => {
            const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];
            const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);
            return (dateValue !== undefined &&
                criterionValue !== undefined &&
                isDateStrictlyAfter(dateValue, criterionValue));
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            return criterion.dateValue === "exactDate"
                ? _t("The value must be a date after %s", getDateCriterionLocalizedValues(criterion, locale)[0])
                : _t("The value must be a date after %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: DVTerms.CriterionError.dateValue,
        numberOfValues: (criterion) => (criterion.dateValue === "exactDate" ? 1 : 0),
        name: _t("Date is after"),
        getPreview: (criterion, getters) => {
            return criterion.dateValue === "exactDate"
                ? _t("Date is after %s", getDateCriterionFormattedValues(criterion, getters)[0])
                : _t("Date is after %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
    });
    dataValidationEvaluatorRegistry.add("dateIsOnOrAfter", {
        type: "dateIsOnOrAfter",
        isValueValid: (value, criterion) => {
            const criterionValue = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE)[0];
            const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);
            return (dateValue !== undefined &&
                criterionValue !== undefined &&
                isDateAfter(dateValue, criterionValue));
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            return criterion.dateValue === "exactDate"
                ? _t("The value must be a date on or after %s", getDateCriterionLocalizedValues(criterion, locale)[0])
                : _t("The value must be a date on or after %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: DVTerms.CriterionError.dateValue,
        numberOfValues: (criterion) => (criterion.dateValue === "exactDate" ? 1 : 0),
        name: _t("Date is on or after"),
        getPreview: (criterion, getters) => {
            return criterion.dateValue === "exactDate"
                ? _t("Date is on or after %s", getDateCriterionFormattedValues(criterion, getters)[0])
                : _t("Date is on or after %s", DVTerms.DateIsBefore[criterion.dateValue]);
        },
    });
    dataValidationEvaluatorRegistry.add("dateIsBetween", {
        type: "dateIsBetween",
        isValueValid: (value, criterion) => {
            const criterionValues = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE);
            const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);
            if (dateValue === undefined ||
                criterionValues[0] === undefined ||
                criterionValues[1] === undefined) {
                return false;
            }
            return isDateBetween(dateValue, criterionValues[0], criterionValues[1]);
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const criterionValues = getDateCriterionLocalizedValues(criterion, locale);
            return _t("The value must be a date between %s and %s", criterionValues[0], criterionValues[1]);
        },
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: DVTerms.CriterionError.dateValue,
        numberOfValues: () => 2,
        name: _t("Date is between"),
        getPreview: (criterion, getters) => {
            const values = getDateCriterionFormattedValues(criterion, getters);
            return _t("Date is between %s and %s", values[0], values[1]);
        },
    });
    dataValidationEvaluatorRegistry.add("dateIsNotBetween", {
        type: "dateIsNotBetween",
        isValueValid: (value, criterion) => {
            const criterionValues = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE);
            const dateValue = valueToDateNumber(value, DEFAULT_LOCALE);
            if (dateValue === undefined ||
                criterionValues[0] === undefined ||
                criterionValues[1] === undefined) {
                return false;
            }
            return !isDateBetween(dateValue, criterionValues[0], criterionValues[1]);
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const criterionValues = getDateCriterionLocalizedValues(criterion, locale);
            return _t("The value must be a date not between %s and %s", criterionValues[0], criterionValues[1]);
        },
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: DVTerms.CriterionError.dateValue,
        numberOfValues: () => 2,
        name: _t("Date is not between"),
        getPreview: (criterion, getters) => {
            const values = getDateCriterionFormattedValues(criterion, getters);
            return _t("Date is not between %s and %s", values[0], values[1]);
        },
    });
    dataValidationEvaluatorRegistry.add("dateIsValid", {
        type: "dateIsValid",
        isValueValid: (value) => {
            return valueToDateNumber(value, DEFAULT_LOCALE) !== undefined;
        },
        getErrorString: () => _t("The value must be a valid date"),
        isCriterionValueValid: (value) => checkValueIsDate(value),
        criterionValueErrorString: "",
        numberOfValues: () => 0,
        name: _t("Is valid date"),
        getPreview: () => _t("Date is valid"),
    });
    dataValidationEvaluatorRegistry.add("isEqual", {
        type: "isEqual",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];
            if (criterionValue === undefined) {
                return false;
            }
            return value === criterionValue;
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must be equal to %s", values[0]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 1,
        name: _t("Is equal to"),
        getPreview: (criterion) => _t("Value is equal to %s", criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("isNotEqual", {
        type: "isNotEqual",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];
            if (criterionValue === undefined) {
                return false;
            }
            return value !== criterionValue;
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must not be equal to %s", values[0]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 1,
        name: _t("Is not equal to"),
        getPreview: (criterion) => _t("Value is not equal to %s", criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("isGreaterThan", {
        type: "isGreaterThan",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];
            if (criterionValue === undefined) {
                return false;
            }
            return value > criterionValue;
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must be greater than %s", values[0]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 1,
        name: _t("Is greater than"),
        getPreview: (criterion) => _t("Value is greater than %s", criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("isGreaterOrEqualTo", {
        type: "isGreaterOrEqualTo",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];
            if (criterionValue === undefined) {
                return false;
            }
            return value >= criterionValue;
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must be greater or equal to %s", values[0]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 1,
        name: _t("Is greater or equal to"),
        getPreview: (criterion) => _t("Value is greater or equal to %s", criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("isLessThan", {
        type: "isLessThan",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];
            if (criterionValue === undefined) {
                return false;
            }
            return value < criterionValue;
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must be less than %s", values[0]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 1,
        name: _t("Is less than"),
        getPreview: (criterion) => _t("Value is less than %s", criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("isLessOrEqualTo", {
        type: "isLessOrEqualTo",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValue = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE)[0];
            if (criterionValue === undefined) {
                return false;
            }
            return value <= criterionValue;
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must be less or equal to %s", values[0]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 1,
        name: _t("Is less or equal to"),
        getPreview: (criterion) => _t("Value is less or equal to %s", criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("isBetween", {
        type: "isBetween",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValues = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE);
            if (criterionValues[0] === undefined || criterionValues[1] === undefined) {
                return false;
            }
            return isNumberBetween(value, criterionValues[0], criterionValues[1]);
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must be between %s and %s", values[0], values[1]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 2,
        name: _t("Is between"),
        getPreview: (criterion) => _t("Value is between %s and %s", criterion.values[0], criterion.values[1]),
    });
    dataValidationEvaluatorRegistry.add("isNotBetween", {
        type: "isNotBetween",
        isValueValid: (value, criterion) => {
            if (typeof value !== "number") {
                return false;
            }
            const criterionValues = getCriterionValuesAsNumber(criterion, DEFAULT_LOCALE);
            if (criterionValues[0] === undefined || criterionValues[1] === undefined) {
                return false;
            }
            return !isNumberBetween(value, criterionValues[0], criterionValues[1]);
        },
        getErrorString: (criterion, getters) => {
            const locale = getters.getLocale();
            const values = getNumberCriterionlocalizedValues(criterion, locale);
            return _t("The value must not be between %s and %s", values[0], values[1]);
        },
        isCriterionValueValid: (value) => checkValueIsNumber(value),
        criterionValueErrorString: DVTerms.CriterionError.numberValue,
        numberOfValues: () => 2,
        name: _t("Is not between"),
        getPreview: (criterion) => _t("Value is not between %s and %s", criterion.values[0], criterion.values[1]),
    });
    dataValidationEvaluatorRegistry.add("isBoolean", {
        type: "isBoolean",
        isValueValid: (value) => value === "" || typeof value === "boolean",
        getErrorString: () => _t("The value must be a boolean"),
        isCriterionValueValid: () => true,
        criterionValueErrorString: "",
        numberOfValues: () => 0,
        name: _t("Checkbox"),
        getPreview: () => _t("Checkbox"),
    });
    dataValidationEvaluatorRegistry.add("isValueInList", {
        type: "isValueInList",
        isValueValid: (value, criterion) => {
            if (value === null) {
                return false;
            }
            return criterion.values
                .map((str) => str.toLowerCase())
                .includes(value.toString().toLowerCase());
        },
        getErrorString: (criterion) => _t("The value must be one of: %s", criterion.values.join(", ")),
        isCriterionValueValid: () => true,
        criterionValueErrorString: "",
        numberOfValues: () => undefined,
        allowedValues: "onlyLiterals",
        name: _t("Value in list"),
        getPreview: (criterion) => _t("Value one of: %s", criterion.values.join(", ")),
    });
    dataValidationEvaluatorRegistry.add("isValueInRange", {
        type: "isValueInList",
        isValueValid: (value, criterion, getters, sheetId) => {
            if (!value) {
                return false;
            }
            const range = getters.getRangeFromSheetXC(sheetId, criterion.values[0]);
            const criterionValues = getters.getRangeValues(range);
            return criterionValues
                .filter(isNotNull)
                .map((value) => value.toString().toLowerCase())
                .includes(value.toString().toLowerCase());
        },
        getErrorString: (criterion) => _t("The value must be a value in the range %s", criterion.values[0]),
        isCriterionValueValid: (value) => rangeReference.test(value),
        criterionValueErrorString: DVTerms.CriterionError.validRange,
        numberOfValues: () => 1,
        allowedValues: "onlyLiterals",
        name: _t("Value in range"),
        getPreview: (criterion) => _t("Value in range %s", criterion.values[0]),
    });
    dataValidationEvaluatorRegistry.add("customFormula", {
        type: "customFormula",
        isValueValid: (value, criterion) => {
            const parsedValue = parseLiteral(criterion.values[0], DEFAULT_LOCALE);
            if (typeof parsedValue === "number" || typeof parsedValue === "boolean") {
                return !!parsedValue;
            }
            return false;
        },
        getErrorString: () => _t("The value does not match the custom formula data validation rule"),
        isCriterionValueValid: () => true,
        criterionValueErrorString: "",
        numberOfValues: () => 1,
        allowedValues: "onlyFormulas",
        name: _t("Custom formula"),
        getPreview: (criterion) => _t("Custom formula %s", criterion.values[0]),
    });
    function getNumberCriterionlocalizedValues(criterion, locale) {
        return criterion.values.map((value) => value !== undefined ? localizeContent(value, locale) : CellErrorType.InvalidReference);
    }
    function getDateCriterionLocalizedValues(criterion, locale) {
        const values = getDateNumberCriterionValues(criterion, DEFAULT_LOCALE);
        return values.map((value) => value !== undefined
            ? formatValue(value, { locale, format: locale.dateFormat })
            : CellErrorType.InvalidReference);
    }
    function checkValueIsDate(value) {
        const valueAsNumber = valueToDateNumber(value, DEFAULT_LOCALE);
        return valueAsNumber !== undefined;
    }
    function checkValueIsNumber(value) {
        const valueAsNumber = tryToNumber(value, DEFAULT_LOCALE);
        return valueAsNumber !== undefined;
    }
    function getDateCriterionFormattedValues(criterion, getters) {
        const locale = getters.getLocale();
        return criterion.values.map((valueStr) => {
            if (valueStr.startsWith("=")) {
                return valueStr;
            }
            const value = parseLiteral(valueStr, locale);
            if (typeof value === "number") {
                return formatValue(value, { format: locale.dateFormat, locale });
            }
            return "";
        });
    }

    function interactiveStopEdition(env) {
        const result = env.model.dispatch("STOP_EDITION");
        if (result.isCancelledBecause("BlockingValidationRule" /* CommandResult.BlockingValidationRule */)) {
            const editedCell = env.model.getters.getCurrentEditedCell();
            const cellXc = toXC(editedCell.col, editedCell.row);
            const rule = env.model.getters.getValidationRuleForCell(editedCell);
            if (!rule) {
                return;
            }
            const evaluator = dataValidationEvaluatorRegistry.get(rule.criterion.type);
            const errorStr = evaluator.getErrorString(rule.criterion, env.model.getters, editedCell.sheetId);
            env.raiseError(_t("The data you entered in %s violates the data validation rule set on the cell:\n%s", cellXc, errorStr));
            env.model.dispatch("CANCEL_EDITION");
        }
    }

    const SEPARATORS = [
        { name: _t("Detect automatically"), value: "auto" },
        { name: _t("Custom separator"), value: "custom" },
        { name: _t("Space"), value: " " },
        { name: _t("Comma"), value: "," },
        { name: _t("Semicolon"), value: ";" },
        { name: _t("Line Break"), value: NEWLINE },
    ];
    class SplitIntoColumnsPanel extends owl.Component {
        static template = "o-spreadsheet-SplitIntoColumnsPanel";
        static components = { ValidationMessages };
        state = owl.useState({ separatorValue: "auto", addNewColumns: false, customSeparator: "" });
        setup() {
            owl.onWillUpdateProps(() => {
                // The feature makes no sense if we are editing a cell, because then the selection isn't active
                // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
                if (this.env.model.getters.getEditionMode() !== "inactive") {
                    this.props.onCloseSidePanel();
                }
            });
            owl.onMounted(() => {
                interactiveStopEdition(this.env);
            });
        }
        onSeparatorChange(value) {
            this.state.separatorValue = value;
        }
        updateCustomSeparator(ev) {
            if (!ev.target)
                return;
            this.state.customSeparator = ev.target.value;
        }
        updateAddNewColumnsCheckbox(ev) {
            if (!ev.target)
                return;
            this.state.addNewColumns = ev.target.checked;
        }
        confirm() {
            const result = interactiveSplitToColumns(this.env, this.separatorValue, this.state.addNewColumns);
            if (result.isSuccessful) {
                this.props.onCloseSidePanel();
            }
        }
        get errorMessages() {
            const cancelledReasons = this.env.model.canDispatch("SPLIT_TEXT_INTO_COLUMNS", {
                separator: this.separatorValue,
                addNewColumns: this.state.addNewColumns,
                force: true,
            }).reasons;
            const errors = new Set();
            for (const reason of cancelledReasons) {
                switch (reason) {
                    case "SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */:
                    case "EmptySplitSeparator" /* CommandResult.EmptySplitSeparator */:
                        break;
                    default:
                        errors.add(SplitToColumnsTerms.Errors[reason] || SplitToColumnsTerms.Errors.Unexpected);
                }
            }
            return Array.from(errors);
        }
        get warningMessages() {
            const warnings = [];
            const cancelledReasons = this.env.model.canDispatch("SPLIT_TEXT_INTO_COLUMNS", {
                separator: this.separatorValue,
                addNewColumns: this.state.addNewColumns,
                force: false,
            }).reasons;
            if (cancelledReasons.includes("SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */)) {
                warnings.push(SplitToColumnsTerms.Errors["SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */]);
            }
            return warnings;
        }
        get separatorValue() {
            if (this.state.separatorValue === "custom") {
                return this.state.customSeparator;
            }
            else if (this.state.separatorValue === "auto") {
                return this.env.model.getters.getAutomaticSeparator();
            }
            return this.state.separatorValue;
        }
        get separators() {
            return SEPARATORS;
        }
        get isConfirmDisabled() {
            return !this.separatorValue || this.errorMessages.length > 0;
        }
    }
    SplitIntoColumnsPanel.props = {
        onCloseSidePanel: Function,
    };

    /** This component looks like a select input, but on click it opens a Menu with the items given as props instead of a dropdown */
    class SelectMenu extends owl.Component {
        static template = "o-spreadsheet-SelectMenu";
        static components = { Menu };
        selectRef = owl.useRef("select");
        selectRect = useAbsoluteBoundingRect(this.selectRef);
        state = owl.useState({
            isMenuOpen: false,
        });
        onClick() {
            this.state.isMenuOpen = true;
        }
        onMenuClosed() {
            this.state.isMenuOpen = false;
        }
        get menuPosition() {
            return {
                x: this.selectRect.x,
                y: this.selectRect.y,
            };
        }
    }
    SelectMenu.props = {
        menuItems: Array,
        selectedValue: String,
        class: { type: String, optional: true },
    };

    class DataValidationCriterionForm extends owl.Component {
        setup() {
            owl.onMounted(() => {
                interactiveStopEdition(this.env);
            });
        }
        updateCriterion(criterion) {
            const filteredCriterion = {
                ...this.props.criterion,
                ...criterion,
            };
            this.props.onCriterionChanged(filteredCriterion);
        }
    }
    DataValidationCriterionForm.props = {
        criterion: Object,
        onCriterionChanged: Function,
    };

    css /* scss */ `
  .o-dv-input {
    .o-invalid {
      background-color: #ffdddd;
    }
    .error-icon {
      right: 7px;
      top: 7px;
    }
  }
`;
    class DataValidationInput extends owl.Component {
        static template = "o-spreadsheet-DataValidationInput";
        static defaultProps = {
            value: "",
            onKeyDown: () => { },
            focused: false,
            onBlur: () => { },
        };
        inputRef = owl.useRef("input");
        setup() {
            owl.useEffect(() => {
                if (this.props.focused) {
                    this.inputRef.el.focus();
                }
            }, () => [this.props.focused, this.inputRef.el]);
        }
        state = owl.useState({
            shouldDisplayError: !!this.props.value, // Don't display error if user inputted nothing yet
        });
        onValueChanged(ev) {
            this.state.shouldDisplayError = true;
            this.props.onValueChanged(ev.target.value);
        }
        get placeholder() {
            const evaluator = dataValidationEvaluatorRegistry.get(this.props.criterionType);
            if (evaluator.allowedValues === "onlyFormulas") {
                return _t("Formula");
            }
            else if (evaluator.allowedValues === "onlyLiterals") {
                return _t("Value");
            }
            return _t("Value or formula");
        }
        get errorMessage() {
            if (!this.state.shouldDisplayError) {
                return undefined;
            }
            return this.env.model.getters.getDataValidationInvalidCriterionValueMessage(this.props.criterionType, canonicalizeContent(this.props.value, this.env.model.getters.getLocale()));
        }
    }
    DataValidationInput.props = {
        value: { type: String, optional: true },
        criterionType: String,
        onValueChanged: Function,
        onKeyDown: { type: Function, optional: true },
        focused: { type: Boolean, optional: true },
        onBlur: { type: Function, optional: true },
        onFocus: { type: Function, optional: true },
    };

    const DATES_VALUES = {
        today: _t("today"),
        yesterday: _t("yesterday"),
        tomorrow: _t("tomorrow"),
        lastWeek: _t("in the past week"),
        lastMonth: _t("in the past month"),
        lastYear: _t("in the past year"),
        exactDate: _t("exact date"),
    };
    class DataValidationDateCriterionForm extends DataValidationCriterionForm {
        static template = "o-spreadsheet-DataValidationDateCriterion";
        static components = { DataValidationInput };
        setup() {
            super.setup();
            const setupDefault = (props) => {
                if (props.criterion.dateValue === undefined) {
                    this.updateCriterion({ dateValue: "exactDate" });
                }
            };
            owl.onWillUpdateProps(setupDefault);
            owl.onWillStart(() => setupDefault(this.props));
        }
        onValueChanged(value) {
            this.updateCriterion({ values: [value] });
        }
        onDateValueChanged(ev) {
            const dateValue = ev.target.value;
            this.updateCriterion({ dateValue });
        }
        get dateValues() {
            return Object.keys(DATES_VALUES).map((key) => ({
                value: key,
                title: DATES_VALUES[key],
            }));
        }
    }

    class DataValidationDoubleInputCriterionForm extends DataValidationCriterionForm {
        static template = "o-spreadsheet-DataValidationDoubleInput";
        static components = { DataValidationInput };
        onFirstValueChanged(value) {
            const values = this.props.criterion.values;
            this.updateCriterion({
                values: [value, values[1]],
            });
        }
        onSecondValueChanged(value) {
            const values = this.props.criterion.values;
            this.updateCriterion({
                values: [values[0], value],
            });
        }
    }

    class DataValidationSingleInputCriterionForm extends DataValidationCriterionForm {
        static template = "o-spreadsheet-DataValidationSingleInput";
        static components = { DataValidationInput };
        onValueChanged(value) {
            const criterion = deepCopy(this.props.criterion);
            criterion.values[0] = value;
            this.updateCriterion(criterion);
        }
    }

    css /* scss */ `
  .o-dv-list-item-delete {
    color: #666666;
    cursor: pointer;
  }
`;
    class DataValidationListCriterionForm extends DataValidationCriterionForm {
        static template = "o-spreadsheet-DataValidationListCriterionForm";
        static components = { DataValidationInput };
        state = owl.useState({
            numberOfValues: Math.max(this.props.criterion.values.length, 2),
        });
        setup() {
            super.setup();
            const setupDefault = (props) => {
                if (props.criterion.displayStyle === undefined) {
                    this.updateCriterion({ displayStyle: "arrow" });
                }
            };
            owl.onWillUpdateProps(setupDefault);
            owl.onWillStart(() => setupDefault(this.props));
        }
        onValueChanged(value, index) {
            const values = [...this.displayedValues];
            values[index] = value;
            this.updateCriterion({ values });
        }
        onAddAnotherValue() {
            this.state.numberOfValues++;
        }
        removeItem(index) {
            const values = [...this.displayedValues];
            values.splice(index, 1);
            this.state.numberOfValues--;
            this.updateCriterion({ values });
        }
        onChangedDisplayStyle(ev) {
            const displayStyle = ev.target.value;
            this.updateCriterion({ displayStyle });
        }
        onKeyDown(ev, index) {
            if ((ev.key === "Enter" || ev.key === "Tab") && index === this.state.numberOfValues - 1) {
                this.onAddAnotherValue();
                this.state.focusedValueIndex = index + 1;
                ev.preventDefault();
            }
            else if (ev.key === "Enter") {
                this.state.focusedValueIndex = index + 1;
            }
        }
        onBlurInput() {
            this.state.focusedValueIndex = undefined;
        }
        get displayedValues() {
            const values = [];
            for (let i = 0; i < this.state.numberOfValues; i++) {
                values.push(this.props.criterion.values[i] || "");
            }
            return values;
        }
    }

    class DataValidationValueInRangeCriterionForm extends DataValidationCriterionForm {
        static template = "o-spreadsheet-DataValidationValueInRangeCriterionForm";
        static components = { SelectionInput };
        setup() {
            super.setup();
            const setupDefault = (props) => {
                if (props.criterion.displayStyle === undefined) {
                    this.updateCriterion({ displayStyle: "arrow" });
                }
            };
            owl.onWillUpdateProps(setupDefault);
            owl.onWillStart(() => setupDefault(this.props));
        }
        onRangeChanged(rangeXc) {
            this.updateCriterion({ values: [rangeXc] });
        }
        onChangedDisplayStyle(ev) {
            const displayStyle = ev.target.value;
            this.updateCriterion({ displayStyle });
        }
    }

    const dvCriterionCategoriesSequences = {
        list: 10,
        text: 20,
        date: 30,
        number: 40,
        misc: 50,
    };
    const dataValidationPanelCriteriaRegistry = new Registry();
    dataValidationPanelCriteriaRegistry.add("textContains", {
        type: "textContains",
        component: DataValidationSingleInputCriterionForm,
        category: "text",
        sequence: 10,
    });
    dataValidationPanelCriteriaRegistry.add("textNotContains", {
        type: "textNotContains",
        component: DataValidationSingleInputCriterionForm,
        category: "text",
        sequence: 20,
    });
    dataValidationPanelCriteriaRegistry.add("textIs", {
        type: "textIs",
        component: DataValidationSingleInputCriterionForm,
        category: "text",
        sequence: 30,
    });
    dataValidationPanelCriteriaRegistry.add("textIsEmail", {
        type: "textIsEmail",
        component: undefined,
        category: "text",
        sequence: 40,
    });
    dataValidationPanelCriteriaRegistry.add("textIsLink", {
        type: "textIsLink",
        component: undefined,
        category: "text",
        sequence: 50,
    });
    dataValidationPanelCriteriaRegistry.add("dateIs", {
        type: "dateIs",
        component: DataValidationDateCriterionForm,
        category: "date",
        sequence: 20,
    });
    dataValidationPanelCriteriaRegistry.add("dateIsBefore", {
        type: "dateIsBefore",
        component: DataValidationDateCriterionForm,
        category: "date",
        sequence: 30,
    });
    dataValidationPanelCriteriaRegistry.add("dateIsOnOrBefore", {
        type: "dateIsOnOrBefore",
        component: DataValidationDateCriterionForm,
        category: "date",
        sequence: 40,
    });
    dataValidationPanelCriteriaRegistry.add("dateIsAfter", {
        type: "dateIsAfter",
        component: DataValidationDateCriterionForm,
        category: "date",
        sequence: 50,
    });
    dataValidationPanelCriteriaRegistry.add("dateIsOnOrAfter", {
        type: "dateIsOnOrAfter",
        component: DataValidationDateCriterionForm,
        category: "date",
        sequence: 60,
    });
    dataValidationPanelCriteriaRegistry.add("dateIsBetween", {
        type: "dateIsBetween",
        component: DataValidationDoubleInputCriterionForm,
        category: "date",
        sequence: 70,
    });
    dataValidationPanelCriteriaRegistry.add("dateIsNotBetween", {
        type: "dateIsNotBetween",
        component: DataValidationDoubleInputCriterionForm,
        category: "date",
        sequence: 80,
    });
    dataValidationPanelCriteriaRegistry.add("dateIsValid", {
        type: "dateIsValid",
        component: undefined,
        category: "date",
        sequence: 10,
    });
    dataValidationPanelCriteriaRegistry.add("isEqual", {
        type: "isEqual",
        component: DataValidationSingleInputCriterionForm,
        category: "number",
        sequence: 10,
    });
    dataValidationPanelCriteriaRegistry.add("isNotEqual", {
        type: "isNotEqual",
        component: DataValidationSingleInputCriterionForm,
        category: "number",
        sequence: 20,
    });
    dataValidationPanelCriteriaRegistry.add("isGreaterThan", {
        type: "isGreaterThan",
        component: DataValidationSingleInputCriterionForm,
        category: "number",
        sequence: 50,
    });
    dataValidationPanelCriteriaRegistry.add("isGreaterOrEqualTo", {
        type: "isGreaterOrEqualTo",
        component: DataValidationSingleInputCriterionForm,
        category: "number",
        sequence: 60,
    });
    dataValidationPanelCriteriaRegistry.add("isLessThan", {
        type: "isLessThan",
        component: DataValidationSingleInputCriterionForm,
        category: "number",
        sequence: 30,
    });
    dataValidationPanelCriteriaRegistry.add("isLessOrEqualTo", {
        type: "isLessOrEqualTo",
        component: DataValidationSingleInputCriterionForm,
        category: "number",
        sequence: 40,
    });
    dataValidationPanelCriteriaRegistry.add("isBetween", {
        type: "isBetween",
        component: DataValidationDoubleInputCriterionForm,
        category: "number",
        sequence: 70,
    });
    dataValidationPanelCriteriaRegistry.add("isNotBetween", {
        type: "isNotBetween",
        component: DataValidationDoubleInputCriterionForm,
        category: "number",
        sequence: 80,
    });
    dataValidationPanelCriteriaRegistry.add("isBoolean", {
        type: "isBoolean",
        component: undefined,
        category: "misc",
        sequence: 10,
    });
    dataValidationPanelCriteriaRegistry.add("isValueInList", {
        type: "isValueInList",
        component: DataValidationListCriterionForm,
        category: "list",
        sequence: 10,
    });
    dataValidationPanelCriteriaRegistry.add("isValueInRange", {
        type: "isValueInRange",
        component: DataValidationValueInRangeCriterionForm,
        category: "list",
        sequence: 20,
    });
    dataValidationPanelCriteriaRegistry.add("customFormula", {
        type: "customFormula",
        component: DataValidationSingleInputCriterionForm,
        category: "misc",
        sequence: 20,
    });
    function getDataValidationCriterionMenuItems(callback) {
        const items = dataValidationPanelCriteriaRegistry.getAll().sort((a, b) => {
            if (a.category === b.category) {
                return a.sequence - b.sequence;
            }
            return dvCriterionCategoriesSequences[a.category] - dvCriterionCategoriesSequences[b.category];
        });
        const actionSpecs = items.map((item, index) => {
            const evaluator = dataValidationEvaluatorRegistry.get(item.type);
            return {
                name: evaluator.name,
                id: item.type,
                separator: item.category !== items[index + 1]?.category,
                execute: () => callback(item.type),
            };
        });
        return createActions(actionSpecs);
    }

    css /* scss */ `
  .o-sidePanel .o-sidePanelBody .o-dv-form {
    .o-section {
      padding: 16px 16px 0 16px;
    }
  }
`;
    class DataValidationEditor extends owl.Component {
        static template = "o-spreadsheet-DataValidationEditor";
        static components = { SelectionInput, SelectMenu };
        state = owl.useState({ rule: this.defaultDataValidationRule });
        setup() {
            if (this.props.rule) {
                const sheetId = this.env.model.getters.getActiveSheetId();
                this.state.rule = {
                    ...this.props.rule,
                    ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, sheetId)),
                };
                this.state.rule.criterion.type = this.props.rule.criterion.type;
            }
        }
        onCriterionTypeChanged(type) {
            this.state.rule.criterion.type = type;
        }
        onRangesChanged(ranges) {
            this.state.rule.ranges = ranges;
        }
        onCriterionChanged(criterion) {
            this.state.rule.criterion = criterion;
        }
        changeRuleIsBlocking(ev) {
            const isBlocking = ev.target.value;
            this.state.rule.isBlocking = isBlocking === "true";
        }
        onSave() {
            if (!this.canSave) {
                return;
            }
            this.env.model.dispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload);
            this.props.onExit();
        }
        get canSave() {
            return this.env.model.canDispatch("ADD_DATA_VALIDATION_RULE", this.dispatchPayload)
                .isSuccessful;
        }
        get dispatchPayload() {
            const rule = { ...this.state.rule, ranges: undefined };
            const locale = this.env.model.getters.getLocale();
            const criterion = rule.criterion;
            const criterionEvaluator = dataValidationEvaluatorRegistry.get(criterion.type);
            const sheetId = this.env.model.getters.getActiveSheetId();
            const values = criterion.values
                .slice(0, criterionEvaluator.numberOfValues(criterion))
                .map((value) => value?.trim())
                .filter((value) => value !== "" && value !== undefined)
                .map((value) => canonicalizeContent(value, locale));
            rule.criterion = { ...criterion, values };
            return {
                sheetId,
                ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
                rule,
            };
        }
        get dvCriterionMenuItems() {
            return getDataValidationCriterionMenuItems((type) => this.onCriterionTypeChanged(type));
        }
        get selectedCriterionName() {
            const selectedType = this.state.rule.criterion.type;
            return dataValidationEvaluatorRegistry.get(selectedType).name;
        }
        get defaultDataValidationRule() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const ranges = this.env.model.getters
                .getSelectedZones()
                .map((zone) => zoneToXc(this.env.model.getters.getUnboundedZone(sheetId, zone)));
            return {
                id: this.env.model.uuidGenerator.uuidv4(),
                criterion: { type: "textContains", values: [""] },
                ranges,
            };
        }
        get criterionComponent() {
            return dataValidationPanelCriteriaRegistry.get(this.state.rule.criterion.type).component;
        }
    }
    DataValidationEditor.props = {
        rule: { type: Object, optional: true },
        onExit: Function,
    };

    css /* scss */ `
  .o-sidePanel {
    .o-dv-preview {
      height: 70px;
      box-sizing: border-box;
      cursor: pointer;
      border-bottom: 1px solid ${FIGURE_BORDER_COLOR};

      .o-dv-container {
        min-width: 0; // otherwise flex won't shrink correctly
      }

      .o-dv-preview-description {
        font-size: 13px;
      }

      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }

      &:not(:hover) .o-dv-preview-delete {
        display: none !important;
      }
    }
  }
`;
    class DataValidationPreview extends owl.Component {
        static template = "o-spreadsheet-DataValidationPreview";
        deleteDataValidation() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            this.env.model.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: this.props.rule.id });
        }
        get rangesString() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            return this.props.rule.ranges
                .map((range) => this.env.model.getters.getRangeString(range, sheetId))
                .join(", ");
        }
        get descriptionString() {
            return dataValidationEvaluatorRegistry
                .get(this.props.rule.criterion.type)
                .getPreview(this.props.rule.criterion, this.env.model.getters);
        }
    }
    DataValidationPreview.props = {
        onClick: Function,
        rule: Object,
    };

    class DataValidationPanel extends owl.Component {
        static template = "o-spreadsheet-DataValidationPanel";
        static components = { DataValidationPreview, DataValidationEditor };
        state = owl.useState({ mode: "list", activeRule: undefined });
        onPreviewClick(id) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const rule = this.env.model.getters.getDataValidationRule(sheetId, id);
            if (rule) {
                this.state.mode = "edit";
                this.state.activeRule = rule;
            }
        }
        addDataValidationRule() {
            this.state.mode = "edit";
            this.state.activeRule = undefined;
        }
        onExitEditMode() {
            this.state.mode = "list";
            this.state.activeRule = undefined;
        }
        localizeDVRule(rule) {
            if (!rule)
                return rule;
            const locale = this.env.model.getters.getLocale();
            return localizeDataValidationRule(rule, locale);
        }
        get validationRules() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            return this.env.model.getters.getDataValidationRules(sheetId);
        }
    }
    DataValidationPanel.props = {
        onCloseSidePanel: Function,
    };

    const sidePanelRegistry = new Registry();
    sidePanelRegistry.add("ConditionalFormatting", {
        title: _t("Conditional formatting"),
        Body: ConditionalFormattingPanel,
    });
    sidePanelRegistry.add("ChartPanel", {
        title: _t("Chart"),
        Body: ChartPanel,
    });
    sidePanelRegistry.add("FindAndReplace", {
        title: _t("Find and Replace"),
        Body: FindAndReplacePanel,
    });
    sidePanelRegistry.add("CustomCurrency", {
        title: _t("Custom currency format"),
        Body: CustomCurrencyPanel,
    });
    sidePanelRegistry.add("SplitToColumns", {
        title: _t("Split text into columns"),
        Body: SplitIntoColumnsPanel,
    });
    sidePanelRegistry.add("Settings", {
        title: _t("Spreadsheet settings"),
        Body: SettingsPanel,
    });
    sidePanelRegistry.add("RemoveDuplicates", {
        title: _t("Remove duplicates"),
        Body: RemoveDuplicatesPanel,
    });
    sidePanelRegistry.add("DataValidation", {
        title: _t("Data validation"),
        Body: DataValidationPanel,
    });
    sidePanelRegistry.add("MoreFormats", {
        title: _t("More date formats"),
        Body: MoreFormatsPanel,
    });

    class TopBarComponentRegistry extends Registry {
        mapping = {};
        uuidGenerator = new UuidGenerator();
        add(name, value) {
            const component = { ...value, id: this.uuidGenerator.uuidv4() };
            return super.add(name, component);
        }
    }
    const topbarComponentRegistry = new TopBarComponentRegistry();

    // -----------------------------------------------------------------------------
    // STYLE
    // -----------------------------------------------------------------------------
    const ANCHOR_SIZE = 8;
    const BORDER_WIDTH = 1;
    const ACTIVE_BORDER_WIDTH = 2;
    css /*SCSS*/ `
  div.o-figure {
    box-sizing: border-box;
    position: absolute;
    width: 100%;
    height: 100%;
    user-select: none;

    &:focus {
      outline: none;
    }
  }

  div.o-figure-border {
    box-sizing: border-box;
    z-index: 1;
  }

  .o-figure-wrapper {
    position: absolute;
    box-sizing: content-box;

    .o-fig-anchor {
      z-index: ${ComponentsImportance.FigureAnchor};
      position: absolute;
      width: ${ANCHOR_SIZE}px;
      height: ${ANCHOR_SIZE}px;
      background-color: #1a73e8;
      outline: ${BORDER_WIDTH}px solid white;

      &.o-top {
        cursor: n-resize;
      }
      &.o-topRight {
        cursor: ne-resize;
      }
      &.o-right {
        cursor: e-resize;
      }
      &.o-bottomRight {
        cursor: se-resize;
      }
      &.o-bottom {
        cursor: s-resize;
      }
      &.o-bottomLeft {
        cursor: sw-resize;
      }
      &.o-left {
        cursor: w-resize;
      }
      &.o-topLeft {
        cursor: nw-resize;
      }
    }

    .o-figure-menu {
      right: 0px;
      top: 0px;
      display: none;
    }

    .o-figure-menu-item {
      cursor: pointer;
    }

    .o-figure.active:focus,
    .o-figure:hover {
      .o-figure-menu {
        display: flex;
      }
    }
  }
`;
    class FigureComponent extends owl.Component {
        static template = "o-spreadsheet-FigureComponent";
        static components = { Menu };
        static defaultProps = {
            onFigureDeleted: () => { },
            onMouseDown: () => { },
            onClickAnchor: () => { },
        };
        menuState = owl.useState({ isOpen: false, position: null, menuItems: [] });
        figureRef = owl.useRef("figure");
        menuButtonRef = owl.useRef("menuButton");
        menuButtonRect = useAbsoluteBoundingRect(this.menuButtonRef);
        borderWidth;
        get isSelected() {
            return this.env.model.getters.getSelectedFigureId() === this.props.figure.id;
        }
        get figureRegistry() {
            return figureRegistry;
        }
        getBorderWidth() {
            if (this.env.isDashboard())
                return 0;
            return this.isSelected ? ACTIVE_BORDER_WIDTH : this.borderWidth;
        }
        get borderStyle() {
            const borderWidth = this.getBorderWidth();
            const borderColor = this.isSelected ? SELECTION_BORDER_COLOR : FIGURE_BORDER_COLOR;
            return `border: ${borderWidth}px solid ${borderColor};`;
        }
        get wrapperStyle() {
            const { x, y, width, height } = this.props.figure;
            return cssPropertiesToCss({
                left: `${x}px`,
                top: `${y}px`,
                width: `${width}px`,
                height: `${height}px`,
                "z-index": String(ComponentsImportance.Figure + (this.isSelected ? 1 : 0)),
            });
        }
        getResizerPosition(resizer) {
            const anchorCenteringOffset = (ANCHOR_SIZE - ACTIVE_BORDER_WIDTH) / 2;
            let style = {};
            if (resizer.includes("top")) {
                style.top = `${-anchorCenteringOffset}px`;
            }
            else if (resizer.includes("bottom")) {
                style.bottom = `${-anchorCenteringOffset}px`;
            }
            else {
                style.bottom = `calc(50% - ${anchorCenteringOffset}px)`;
            }
            if (resizer.includes("left")) {
                style.left = `${-anchorCenteringOffset}px`;
            }
            else if (resizer.includes("right")) {
                style.right = `${-anchorCenteringOffset}px`;
            }
            else {
                style.right = `calc(50% - ${anchorCenteringOffset}px)`;
            }
            return cssPropertiesToCss(style);
        }
        setup() {
            const borderWidth = figureRegistry.get(this.props.figure.tag).borderWidth;
            this.borderWidth = borderWidth !== undefined ? borderWidth : BORDER_WIDTH;
            owl.useEffect((selectedFigureId, thisFigureId, el) => {
                if (selectedFigureId === thisFigureId) {
                    /** Scrolling on a newly inserted figure that overflows outside the viewport
                     * will break the whole layout.
                     * NOTE: `preventScroll`does not work on mobile but then again,
                     * mobile is not really supported ATM.
                     *
                     * TODO: When implementing proper mobile, we will need to scroll the viewport
                     * correctly (and render?) before focusing the element.
                     */
                    el?.focus({ preventScroll: true });
                }
            }, () => [this.env.model.getters.getSelectedFigureId(), this.props.figure.id, this.figureRef.el]);
        }
        clickAnchor(dirX, dirY, ev) {
            this.props.onClickAnchor(dirX, dirY, ev);
        }
        onMouseDown(ev) {
            this.props.onMouseDown(ev);
        }
        onKeyDown(ev) {
            const figure = this.props.figure;
            switch (ev.key) {
                case "Delete":
                    this.env.model.dispatch("DELETE_FIGURE", {
                        sheetId: this.env.model.getters.getActiveSheetId(),
                        id: figure.id,
                    });
                    this.props.onFigureDeleted();
                    ev.stopPropagation();
                    ev.preventDefault();
                    break;
                case "ArrowDown":
                case "ArrowLeft":
                case "ArrowRight":
                case "ArrowUp":
                    const deltaMap = {
                        ArrowDown: [0, 1],
                        ArrowLeft: [-1, 0],
                        ArrowRight: [1, 0],
                        ArrowUp: [0, -1],
                    };
                    const delta = deltaMap[ev.key];
                    this.env.model.dispatch("UPDATE_FIGURE", {
                        sheetId: this.env.model.getters.getActiveSheetId(),
                        id: figure.id,
                        x: figure.x + delta[0],
                        y: figure.y + delta[1],
                    });
                    ev.stopPropagation();
                    ev.preventDefault();
                    break;
            }
        }
        onContextMenu(ev) {
            if (this.env.isDashboard())
                return;
            const position = {
                x: ev.clientX,
                y: ev.clientY,
            };
            this.openContextMenu(position);
        }
        showMenu() {
            const { x, y, width } = this.menuButtonRect;
            const menuPosition = {
                x: x >= MENU_WIDTH ? x - MENU_WIDTH : x + width,
                y: y,
            };
            this.openContextMenu(menuPosition);
        }
        openContextMenu(position) {
            this.menuState.isOpen = true;
            this.menuState.position = position;
            this.menuState.menuItems = figureRegistry
                .get(this.props.figure.tag)
                .menuBuilder(this.props.figure.id, this.props.onFigureDeleted, this.env);
        }
    }
    FigureComponent.props = {
        figure: Object,
        style: { type: String, optional: true },
        onFigureDeleted: { type: Function, optional: true },
        onMouseDown: { type: Function, optional: true },
        onClickAnchor: { type: Function, optional: true },
    };

    const ToggleGroupInteractiveContent = {
        CannotHideAllRows: _t("Cannot hide all the rows of a sheet."),
        CannotHideAllColumns: _t("Cannot hide all the columns of a sheet."),
    };
    function interactiveToggleGroup(env, sheetId, dimension, start, end) {
        const group = env.model.getters.getHeaderGroup(sheetId, dimension, start, end);
        if (!group) {
            return;
        }
        const command = group.isFolded ? "UNFOLD_HEADER_GROUP" : "FOLD_HEADER_GROUP";
        const result = env.model.dispatch(command, {
            sheetId,
            dimension,
            start: group.start,
            end: group.end,
        });
        if (!result.isSuccessful) {
            if (result.isCancelledBecause("NotEnoughElements" /* CommandResult.NotEnoughElements */)) {
                const errorMessage = dimension === "ROW"
                    ? ToggleGroupInteractiveContent.CannotHideAllRows
                    : ToggleGroupInteractiveContent.CannotHideAllColumns;
                env.raiseError(errorMessage);
            }
        }
    }

    function createHeaderGroupContainerContextMenu(sheetId, dimension) {
        return createActions([
            {
                id: "unfold_all",
                name: dimension === "ROW" ? _t("Expand all row groups") : _t("Expand all column groups"),
                execute: (env) => {
                    env.model.dispatch("UNFOLD_ALL_HEADER_GROUPS", { sheetId, dimension });
                },
            },
            {
                id: "fold_all",
                name: dimension === "ROW" ? _t("Collapse all row groups") : _t("Collapse all column groups"),
                execute: (env) => {
                    env.model.dispatch("FOLD_ALL_HEADER_GROUPS", { sheetId, dimension });
                },
            },
        ]);
    }
    function getHeaderGroupContextMenu(sheetId, dimension, start, end) {
        const groupActions = createActions([
            {
                id: "toggle_group",
                name: (env) => {
                    const sheetId = env.model.getters.getActiveSheetId();
                    const groupIsFolded = env.model.getters.isGroupFolded(sheetId, dimension, start, end);
                    if (groupIsFolded) {
                        return dimension === "ROW" ? _t("Expand row group") : _t("Expand column group");
                    }
                    else {
                        return dimension === "ROW" ? _t("Collapse row group") : _t("Collapse column group");
                    }
                },
                execute: (env) => {
                    const sheetId = env.model.getters.getActiveSheetId();
                    interactiveToggleGroup(env, sheetId, dimension, start, end);
                },
            },
            {
                id: "remove_group",
                name: dimension === "ROW" ? _t("Remove row group") : _t("Remove column group"),
                execute: (env) => {
                    const sheetId = env.model.getters.getActiveSheetId();
                    env.model.dispatch("UNGROUP_HEADERS", { sheetId, dimension, start, end });
                },
                separator: true,
            },
        ]);
        return [...groupActions, ...createHeaderGroupContainerContextMenu(sheetId, dimension)];
    }
    const groupHeadersMenuRegistry = new MenuItemRegistry();
    groupHeadersMenuRegistry
        .add("group_columns", {
        sequence: 10,
        ...groupColumns,
        isVisible: () => true,
        isEnabled: groupColumns.isVisible,
    })
        .add("group_rows", {
        sequence: 20,
        ...groupRows,
        isVisible: () => true,
        isEnabled: groupRows.isVisible,
    });
    const unGroupHeadersMenuRegistry = new MenuItemRegistry();
    unGroupHeadersMenuRegistry
        .add("ungroup_columns", {
        sequence: 10,
        ...ungroupColumns,
        isEnabled: (env) => canUngroupHeaders(env, "COL"),
    })
        .add("ungroup_rows", {
        sequence: 20,
        ...ungroupRows,
        isEnabled: (env) => canUngroupHeaders(env, "ROW"),
    });

    // -----------------------------------------------------------------------------
    // Autofill
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-autofill {
    position: absolute;
    height: ${AUTOFILL_EDGE_LENGTH}px;
    width: ${AUTOFILL_EDGE_LENGTH}px;
    border: 1px solid white;
    box-sizing: border-box !important;
    background-color: #1a73e8;
  }

  .o-autofill-handler {
    position: absolute;
    height: ${AUTOFILL_EDGE_LENGTH}px;
    width: ${AUTOFILL_EDGE_LENGTH}px;
    &:hover {
      cursor: crosshair;
    }
  }

  .o-autofill-nextvalue {
    position: absolute;
    background-color: #ffffff;
    border: 1px solid black;
    padding: 5px;
    font-size: 12px;
    pointer-events: none;
    white-space: nowrap;
  }
`;
    class Autofill extends owl.Component {
        static template = "o-spreadsheet-Autofill";
        state = owl.useState({
            position: { left: 0, top: 0 },
            handler: false,
        });
        get style() {
            const { left, top } = this.props.position;
            return cssPropertiesToCss({
                top: `${top}px`,
                left: `${left}px`,
                visibility: this.props.isVisible ? "visible" : "hidden",
            });
        }
        get handlerStyle() {
            const { left, top } = this.state.handler ? this.state.position : this.props.position;
            return cssPropertiesToCss({
                top: `${top}px`,
                left: `${left}px`,
            });
        }
        get styleNextValue() {
            const { left, top } = this.state.position;
            return cssPropertiesToCss({
                top: `${top + 5}px`,
                left: `${left + 15}px`,
            });
        }
        getTooltip() {
            const tooltip = this.env.model.getters.getAutofillTooltip();
            if (tooltip && !tooltip.component) {
                tooltip.component = TooltipComponent;
            }
            return tooltip;
        }
        onMouseDown(ev) {
            this.state.handler = true;
            let lastCol;
            let lastRow;
            const start = {
                left: ev.clientX - this.props.position.left,
                top: ev.clientY - this.props.position.top,
            };
            const onMouseUp = () => {
                this.state.handler = false;
                this.state.position = { ...this.props.position };
                this.env.model.dispatch("AUTOFILL");
            };
            const onMouseMove = (col, row, ev) => {
                this.state.position = {
                    left: ev.clientX - start.left,
                    top: ev.clientY - start.top,
                };
                if (lastCol !== col || lastRow !== row) {
                    const activeSheetId = this.env.model.getters.getActiveSheetId();
                    const numberOfCols = this.env.model.getters.getNumberCols(activeSheetId);
                    const numberOfRows = this.env.model.getters.getNumberRows(activeSheetId);
                    lastCol = col === -1 ? lastCol : clip(col, 0, numberOfCols);
                    lastRow = row === -1 ? lastRow : clip(row, 0, numberOfRows);
                    if (lastCol !== undefined && lastRow !== undefined) {
                        this.env.model.dispatch("AUTOFILL_SELECT", { col: lastCol, row: lastRow });
                    }
                }
            };
            dragAndDropBeyondTheViewport(this.env, onMouseMove, onMouseUp);
        }
        onDblClick() {
            this.env.model.dispatch("AUTOFILL_AUTO");
        }
    }
    Autofill.props = {
        position: Object,
        isVisible: Boolean,
    };
    class TooltipComponent extends owl.Component {
        static template = owl.xml /* xml */ `
    <div t-esc="props.content"/>
  `;
    }
    TooltipComponent.props = {
        content: String,
    };

    css /* scss */ `
  .o-client-tag {
    position: absolute;
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
    font-size: ${DEFAULT_FONT_SIZE};
    color: white;
    pointer-events: none;
  }
`;
    class ClientTag extends owl.Component {
        static template = "o-spreadsheet-ClientTag";
        get tagStyle() {
            const { col, row, color } = this.props;
            const { height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
            const { x, y } = this.env.model.getters.getVisibleRect({
                left: col,
                top: row,
                right: col,
                bottom: row,
            });
            return cssPropertiesToCss({
                bottom: `${height - y + 15}px`,
                left: `${x - 1}px`,
                border: `1px solid ${color}`,
                "background-color": color,
            });
        }
    }
    ClientTag.props = {
        active: Boolean,
        name: String,
        color: String,
        col: Number,
        row: Number,
    };

    function getHtmlContentFromPattern(pattern, value, highlightColor, className) {
        const pendingHtmlContent = [];
        pattern = pattern.toLowerCase();
        for (const patternChar of pattern) {
            const index = value.toLocaleLowerCase().indexOf(patternChar);
            if (index === -1) {
                continue;
            }
            pendingHtmlContent.push({ value: value.slice(0, index), color: "" }, { value: value[index], color: highlightColor, class: className });
            value = value.slice(index + 1);
        }
        pendingHtmlContent.push({ value });
        const htmlContent = pendingHtmlContent.filter((content) => content.value);
        return htmlContent;
    }

    css /* scss */ `
  .o-autocomplete-dropdown {
    pointer-events: auto;
    cursor: pointer;
    background-color: #fff;
    max-width: 400px;

    .o-autocomplete-value-focus {
      background-color: #f2f2f2;
    }

    & > div {
      padding: 1px 5px 5px 5px;
      .o-autocomplete-description {
        padding-left: 5px;
        font-size: 11px;
      }
    }
  }
`;
    class TextValueProvider extends owl.Component {
        static template = "o-spreadsheet-TextValueProvider";
        autoCompleteListRef = owl.useRef("autoCompleteList");
        setup() {
            owl.useEffect(() => {
                const selectedIndex = this.props.selectedIndex;
                if (selectedIndex === undefined) {
                    return;
                }
                const selectedElement = this.autoCompleteListRef.el?.children[selectedIndex];
                selectedElement?.scrollIntoView?.({ block: "nearest" });
            }, () => [this.props.selectedIndex, this.autoCompleteListRef.el]);
        }
    }
    TextValueProvider.props = {
        values: Array,
        selectedIndex: { type: Number, optional: true },
        getHtmlContent: Function,
        onValueSelected: Function,
        onValueHovered: Function,
    };

    class ContentEditableHelper {
        // todo make el private and expose dedicated methods
        el;
        constructor(el) {
            this.el = el;
        }
        updateEl(el) {
            this.el = el;
        }
        /**
         * select the text at position start to end, no matter the children
         */
        selectRange(start, end) {
            let selection = window.getSelection();
            const { start: currentStart, end: currentEnd } = this.getCurrentSelection();
            if (currentStart === start && currentEnd === end) {
                return;
            }
            const currentRange = selection.getRangeAt(0);
            let range;
            if (this.el.contains(currentRange.startContainer)) {
                range = currentRange;
            }
            else {
                range = document.createRange();
                selection.removeAllRanges();
                selection.addRange(range);
            }
            if (start === end && start === 0) {
                range.setStart(this.el, 0);
                range.setEnd(this.el, 0);
            }
            else {
                const textLength = this.getText().length;
                if (start < 0 || end > textLength) {
                    console.warn(`wrong selection asked start ${start}, end ${end}, text content length ${textLength}`);
                    if (start < 0)
                        start = 0;
                    if (end > textLength)
                        end = textLength;
                    if (start > textLength)
                        start = textLength;
                }
                let startNode = this.findChildAtCharacterIndex(start);
                let endNode = this.findChildAtCharacterIndex(end);
                range.setStart(startNode.node, startNode.offset);
                selection.extend(endNode.node, endNode.offset);
            }
        }
        /**
         * finds the dom element that contains the character at `offset`
         */
        findChildAtCharacterIndex(offset) {
            let it = iterateChildren(this.el);
            let current, previous;
            let usedCharacters = offset;
            let isFirstParagraph = true;
            do {
                current = it.next();
                if (!current.done && !current.value.hasChildNodes()) {
                    if (current.value.textContent && current.value.textContent.length < usedCharacters) {
                        usedCharacters -= current.value.textContent.length;
                    }
                    else if (current.value.textContent &&
                        current.value.textContent.length >= usedCharacters) {
                        it.return(current.value);
                    }
                    previous = current.value;
                }
                // One new paragraph = one new line character, except for the first paragraph
                if (!current.done && current.value.nodeName === "P") {
                    if (isFirstParagraph) {
                        isFirstParagraph = false;
                    }
                    else {
                        usedCharacters--;
                    }
                }
            } while (!current.done && usedCharacters);
            if (current.value) {
                return { node: current.value, offset: usedCharacters };
            }
            return { node: previous, offset: usedCharacters };
        }
        /**
         * Sets (or Replaces all) the text inside the root element in the form of distinctive paragraphs and
         * span for each element provided in `contents`.
         *
         * The function will apply the diff between the current content and the new content to avoid the systematic
         * destruction of DOM elements which interferes with IME[1]
         *
         * Each line of text will be encapsulated in a paragraph element.
         * Each span will have its own fontcolor and specific class if provided in the HtmlContent object.
         *
         * [1] https://developer.mozilla.org/en-US/docs/Glossary/Input_method_editor
         */
        setText(contents) {
            if (contents.length === 0) {
                this.removeAll();
                return;
            }
            const childElements = Array.from(this.el.childNodes);
            const contentLength = contents.length;
            for (let i = 0; i < contentLength; i++) {
                const line = contents[i];
                const childElement = childElements[i];
                let newChild = false;
                let p;
                if (childElement && childElement.nodeName === "P") {
                    p = childElement;
                }
                else {
                    newChild = true;
                    p = document.createElement("p");
                }
                const lineLength = line.length;
                const existingChildren = Array.from(p.childNodes);
                for (let j = 0; j < lineLength; j++) {
                    const content = line[j];
                    const child = existingChildren[j];
                    // child nodes can be multiple types of nodes: Span, Text, Div, etc...
                    // We can only modify a node in place if it has the same type as the content
                    // that we would insert, which are spans.
                    // Otherwise, it means that the node has been input by the user, through the keyboard or a copy/paste
                    // @ts-ignore (somehow required because jest does not like child.tagName despite the prior check)
                    const childIsSpan = child && "tagName" in child && child.tagName === "SPAN";
                    if (childIsSpan && compareContentToSpanElement(content, child)) {
                        continue;
                    }
                    // this is an empty line in the content
                    if (!content.value && !content.class) {
                        if (child)
                            p.removeChild(child);
                        continue;
                    }
                    const span = document.createElement("span");
                    span.innerText = content.value;
                    span.style.color = content.color || "";
                    if (content.class) {
                        span.classList.add(content.class);
                    }
                    if (child) {
                        p.replaceChild(span, child);
                    }
                    else {
                        p.appendChild(span);
                    }
                }
                if (existingChildren.length > lineLength) {
                    for (let i = lineLength; i < existingChildren.length; i++) {
                        p.removeChild(existingChildren[i]);
                    }
                }
                // Empty line
                if (!p.hasChildNodes()) {
                    const span = document.createElement("span");
                    span.appendChild(document.createElement("br"));
                    p.appendChild(span);
                }
                // replace p if necessary
                if (newChild) {
                    if (childElement) {
                        this.el.replaceChild(p, childElement);
                    }
                    else {
                        this.el.appendChild(p);
                    }
                }
            }
            if (childElements.length > contentLength) {
                for (let i = contentLength; i < childElements.length; i++) {
                    this.el.removeChild(childElements[i]);
                }
            }
        }
        scrollSelectionIntoView() {
            const focusedNode = document.getSelection()?.focusNode;
            if (!focusedNode || !this.el.contains(focusedNode))
                return;
            const element = focusedNode instanceof HTMLElement ? focusedNode : focusedNode.parentElement;
            element?.scrollIntoView({ block: "nearest" });
        }
        /**
         * remove the current selection of the user
         * */
        removeSelection() {
            let selection = window.getSelection();
            selection.removeAllRanges();
        }
        removeAll() {
            if (this.el) {
                while (this.el.firstChild) {
                    this.el.removeChild(this.el.firstChild);
                }
            }
        }
        /**
         * finds the indexes of the current selection.
         * */
        getCurrentSelection() {
            let { startElement, endElement, startSelectionOffset, endSelectionOffset } = this.getStartAndEndSelection();
            let startSizeBefore = this.findSelectionIndex(startElement, startSelectionOffset);
            let endSizeBefore = this.findSelectionIndex(endElement, endSelectionOffset);
            return {
                start: startSizeBefore,
                end: endSizeBefore,
            };
        }
        /**
         * Computes the text 'index' inside this.el based on the currently selected node and its offset.
         * The selected node is either a Text node or an Element node.
         *
         * case 1 -Text node:
         * the offset is the number of characters from the start of the node. We have to add this offset to the
         * content length of all previous nodes.
         *
         * case 2 - Element node:
         * the offset is the number of child nodes before the selected node. We have to add the content length of
         * all the bnodes prior to the selected node as well as the content of the child node before the offset.
         *
         * See the MDN documentation for more details.
         * https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset
         * https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset
         *
         */
        findSelectionIndex(nodeToFind, nodeOffset) {
            let usedCharacters = 0;
            let it = iterateChildren(this.el);
            let current = it.next();
            let isFirstParagraph = true;
            while (!current.done && current.value !== nodeToFind) {
                if (!current.value.hasChildNodes()) {
                    if (current.value.textContent) {
                        usedCharacters += current.value.textContent.length;
                    }
                }
                // One new paragraph = one new line character, except for the first paragraph
                if (current.value.nodeName === "P" ||
                    (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
                ) {
                    if (isFirstParagraph) {
                        isFirstParagraph = false;
                    }
                    else {
                        usedCharacters++;
                    }
                }
                current = it.next();
            }
            if (current.value !== nodeToFind) {
                /** This situation can happen if the code is called while the selection is not currently on the ContentEditableHelper.
                 * In this case, we return 0 because we don't know the size of the text before the selection.
                 *
                 * A known occurence is triggered since the introduction of commit d4663158 (PR #2038).
                 *
                 * FIXME: find a way to test eventhough the selection API is not available in jsDOM.
                 */
                return 0;
            }
            else {
                if (!current.value.hasChildNodes()) {
                    usedCharacters += nodeOffset;
                }
                else {
                    const children = [...current.value.childNodes].slice(0, nodeOffset);
                    usedCharacters += children.reduce((acc, child, index) => {
                        if (child.textContent !== null) {
                            // need to account for paragraph nodes that implicitely add a new line
                            // except for the last paragraph
                            let chars = child.textContent.length;
                            if (child.nodeName === "P" && index !== children.length - 1) {
                                chars++;
                            }
                            return acc + chars;
                        }
                        else {
                            return acc;
                        }
                    }, 0);
                }
            }
            if (nodeToFind.nodeName === "P" && !isFirstParagraph && nodeToFind.textContent === "") {
                usedCharacters++;
            }
            return usedCharacters;
        }
        getStartAndEndSelection() {
            const selection = document.getSelection();
            return {
                startElement: selection.anchorNode || this.el,
                startSelectionOffset: selection.anchorOffset,
                endElement: selection.focusNode || this.el,
                endSelectionOffset: selection.focusOffset,
            };
        }
        getText() {
            let text = "";
            let it = iterateChildren(this.el);
            let current = it.next();
            let isFirstParagraph = true;
            while (!current.done) {
                if (!current.value.hasChildNodes()) {
                    text += current.value.textContent;
                }
                if (current.value.nodeName === "P" ||
                    (current.value.nodeName === "DIV" && current.value !== this.el) // On paste, the HTML may contain <div> instead of <p>
                ) {
                    if (isFirstParagraph) {
                        isFirstParagraph = false;
                    }
                    else {
                        text += NEWLINE;
                    }
                }
                current = it.next();
            }
            return text;
        }
    }
    function compareContentToSpanElement(content, node) {
        const contentColor = content.color ? toHex(content.color) : "";
        const nodeColor = node.style?.color ? toHex(node.style.color) : "";
        const sameColor = contentColor === nodeColor;
        const sameClass = deepEquals([content.class], [...node.classList]);
        const sameContent = node.innerText === content.value;
        return sameColor && sameClass && sameContent;
    }

    // -----------------------------------------------------------------------------
    // Formula Assistant component
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-formula-assistant {
    .o-formula-assistant-head {
      background-color: #f2f2f2;
      padding: 10px;
    }
    .o-formula-assistant-core {
      border-bottom: 1px solid gray;
    }
    .o-formula-assistant-arg-description {
      font-size: 85%;
    }
    .o-formula-assistant-focus {
      div:first-child,
      span {
        color: ${COMPOSER_ASSISTANT_COLOR};
        text-shadow: 0px 0px 1px ${COMPOSER_ASSISTANT_COLOR};
      }
      div:last-child {
        color: black;
      }
    }
    .o-formula-assistant-gray {
      color: gray;
    }
  }
`;
    class FunctionDescriptionProvider extends owl.Component {
        static template = "o-spreadsheet-FunctionDescriptionProvider";
        assistantState = owl.useState({
            allowCellSelectionBehind: false,
        });
        timeOutId = 0;
        setup() {
            owl.onWillUnmount(() => {
                if (this.timeOutId) {
                    clearTimeout(this.timeOutId);
                }
            });
        }
        getContext() {
            return this.props;
        }
        onMouseMove() {
            this.assistantState.allowCellSelectionBehind = true;
            if (this.timeOutId) {
                clearTimeout(this.timeOutId);
            }
            this.timeOutId = setTimeout(() => {
                this.assistantState.allowCellSelectionBehind = false;
            }, 2000);
        }
    }
    FunctionDescriptionProvider.props = {
        functionName: String,
        functionDescription: Object,
        argToFocus: Number,
    };

    const functions$2 = functionRegistry.content;
    const ASSISTANT_WIDTH = 300;
    const AUTOCOMPLETE_ENTRIES = 10;
    const selectionIndicatorClass = "selector-flag";
    const selectionIndicatorColor = "#a9a9a9";
    const selectionIndicator = "␣";
    const functionColor = "#4a4e4d";
    const operatorColor = "#3da4ab";
    const tokenColors = {
        OPERATOR: operatorColor,
        NUMBER: "#02c39a",
        STRING: "#00a82d",
        FUNCTION: functionColor,
        DEBUGGER: operatorColor,
        LEFT_PAREN: functionColor,
        RIGHT_PAREN: functionColor,
        ARG_SEPARATOR: functionColor,
        MATCHING_PAREN: "#000000",
    };
    css /* scss */ `
  .o-composer-container {
    .o-composer {
      overflow-y: auto;
      overflow-x: hidden;
      word-break: break-all;
      padding-right: 2px;

      box-sizing: border-box;
      font-family: ${DEFAULT_FONT};

      caret-color: black;
      padding-left: 3px;
      padding-right: 3px;
      outline: none;

      p {
        margin-bottom: 0px;

        span {
          white-space: pre-wrap;
          &.${selectionIndicatorClass}:after {
            content: "${selectionIndicator}";
            color: ${selectionIndicatorColor};
          }
        }
      }
    }

    .o-composer-assistant {
      position: absolute;
      margin: 1px 4px;
      pointer-events: none;
      overflow: auto;

      .o-semi-bold {
        /** FIXME: to remove in favor of Bootstrap
        * 'fw-semibold' when we upgrade to Bootstrap 5.2
        */
        font-weight: 600 !important;
      }
    }
  }
`;
    class Composer extends owl.Component {
        static template = "o-spreadsheet-Composer";
        static components = { TextValueProvider, FunctionDescriptionProvider };
        static defaultProps = {
            inputStyle: "",
            isDefaultFocus: false,
        };
        composerRef = owl.useRef("o_composer");
        contentHelper = new ContentEditableHelper(this.composerRef.el);
        composerState = owl.useState({
            positionStart: 0,
            positionEnd: 0,
        });
        autoCompleteState = owl.useState({
            showProvider: false,
            values: [],
            selectedIndex: undefined,
            type: "function",
            getHtmlContent: () => [],
        });
        functionDescriptionState = owl.useState({
            showDescription: false,
            functionName: "",
            functionDescription: {},
            argToFocus: 0,
        });
        compositionActive = false;
        get assistantStyle() {
            const assistantStyle = {};
            assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
            if (this.autoCompleteState.type === "function") {
                assistantStyle.width = `${ASSISTANT_WIDTH}px`;
            }
            if (this.props.delimitation && this.props.rect) {
                const { x: cellX, y: cellY, height: cellHeight } = this.props.rect;
                const remainingHeight = this.props.delimitation.height - (cellY + cellHeight);
                assistantStyle["max-height"] = `${remainingHeight}px`;
                if (cellY > remainingHeight) {
                    const availableSpaceAbove = cellY;
                    assistantStyle["max-height"] = `${availableSpaceAbove}px`;
                    // render top
                    // We compensate 2 px of margin on the assistant style + 1px for design reasons
                    assistantStyle.top = `-3px`;
                    assistantStyle.transform = `translate(0, -100%)`;
                }
                if (cellX + ASSISTANT_WIDTH > this.props.delimitation.width) {
                    // render left
                    assistantStyle.right = `0px`;
                }
            }
            return cssPropertiesToCss(assistantStyle);
        }
        // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
        shouldProcessInputEvents = false;
        tokens = [];
        keyMapping = {
            ArrowUp: this.processArrowKeys,
            ArrowDown: this.processArrowKeys,
            ArrowLeft: this.processArrowKeys,
            ArrowRight: this.processArrowKeys,
            Enter: this.processEnterKey,
            Escape: this.processEscapeKey,
            F2: () => console.warn("Not implemented"),
            F4: this.processF4Key,
            Tab: (ev) => this.processTabKey(ev),
        };
        keyCodeMapping = {
            NumpadDecimal: this.processNumpadDecimal,
        };
        setup() {
            owl.onMounted(() => {
                const el = this.composerRef.el;
                if (this.props.isDefaultFocus) {
                    this.env.focusableElement.setFocusableElement(el);
                }
                this.contentHelper.updateEl(el);
                this.processTokenAtCursor();
            });
            owl.useEffect(() => {
                this.processContent();
            });
        }
        // ---------------------------------------------------------------------------
        // Handlers
        // ---------------------------------------------------------------------------
        processArrowKeys(ev) {
            if (this.env.model.getters.isSelectingForComposer() ||
                this.env.model.getters.getEditionMode() === "inactive") {
                this.functionDescriptionState.showDescription = false;
                // Prevent the default content editable behavior which moves the cursor
                ev.preventDefault();
                ev.stopPropagation();
                updateSelectionWithArrowKeys(ev, this.env.model.selection);
                return;
            }
            const content = this.env.model.getters.getCurrentContent();
            if (this.props.focus === "cellFocus" &&
                !this.autoCompleteState.showProvider &&
                !content.startsWith("=")) {
                interactiveStopEdition(this.env);
                return;
            }
            // All arrow keys are processed: up and down should move autocomplete, left
            // and right should move the cursor.
            ev.stopPropagation();
            this.handleArrowKeysForAutocomplete(ev);
        }
        handleArrowKeysForAutocomplete(ev) {
            // only for arrow up and down
            if (["ArrowUp", "ArrowDown"].includes(ev.key) && this.autoCompleteState.showProvider) {
                ev.preventDefault();
                if (this.autoCompleteState.selectedIndex === undefined) {
                    this.autoCompleteState.selectedIndex = 0;
                    return;
                }
                if (ev.key === "ArrowUp") {
                    this.autoCompleteState.selectedIndex--;
                    if (this.autoCompleteState.selectedIndex < 0) {
                        this.autoCompleteState.selectedIndex = this.autoCompleteState.values.length - 1;
                    }
                }
                else {
                    this.autoCompleteState.selectedIndex =
                        (this.autoCompleteState.selectedIndex + 1) % this.autoCompleteState.values.length;
                }
            }
        }
        processTabKey(ev) {
            ev.preventDefault();
            ev.stopPropagation();
            if (this.env.model.getters.getEditionMode() !== "inactive") {
                const state = this.autoCompleteState;
                if (state.showProvider && state.selectedIndex !== undefined) {
                    const autoCompleteValue = this.autoCompleteState.values[state.selectedIndex]?.text;
                    if (autoCompleteValue) {
                        this.autoComplete(autoCompleteValue);
                        return;
                    }
                }
                interactiveStopEdition(this.env);
            }
            const direction = ev.shiftKey ? "left" : "right";
            this.env.model.selection.moveAnchorCell(direction, 1);
        }
        processEnterKey(ev) {
            ev.preventDefault();
            ev.stopPropagation();
            if (ev.altKey || ev.ctrlKey) {
                this.composerRef.el?.dispatchEvent(new InputEvent("input", { inputType: "insertParagraph", bubbles: true, isComposing: false }));
                return;
            }
            const state = this.autoCompleteState;
            if (state.showProvider && state.selectedIndex !== undefined) {
                const autoCompleteValue = this.autoCompleteState.values[state.selectedIndex]?.text;
                if (autoCompleteValue) {
                    this.autoComplete(autoCompleteValue);
                    return;
                }
            }
            interactiveStopEdition(this.env);
            const direction = ev.shiftKey ? "up" : "down";
            this.env.model.selection.moveAnchorCell(direction, 1);
        }
        processEscapeKey() {
            this.env.model.dispatch("CANCEL_EDITION");
        }
        processF4Key() {
            this.env.model.dispatch("CYCLE_EDITION_REFERENCES");
            this.processContent();
        }
        processNumpadDecimal(ev) {
            ev.stopPropagation();
            ev.preventDefault();
            const locale = this.env.model.getters.getLocale();
            const selection = this.contentHelper.getCurrentSelection();
            const currentContent = this.env.model.getters.getCurrentContent();
            const content = currentContent.slice(0, selection.start) +
                locale.decimalSeparator +
                currentContent.slice(selection.end);
            // Update composer even by hand rather than dispatching an InputEvent because untrusted inputs
            // events aren't handled natively by contentEditable
            this.env.model.dispatch("SET_CURRENT_CONTENT", {
                content,
                selection: { start: selection.start + 1, end: selection.start + 1 },
            });
            // We need to do the process content here in case there is no render between the keyDown and the
            // keyUp event
            this.processContent();
        }
        onCompositionStart() {
            this.compositionActive = true;
        }
        onCompositionEnd() {
            this.compositionActive = false;
        }
        onKeydown(ev) {
            if (this.env.model.getters.getEditionMode() === "inactive") {
                return;
            }
            let handler = this.keyMapping[ev.key] || this.keyCodeMapping[ev.code];
            if (handler) {
                handler.call(this, ev);
            }
            else {
                ev.stopPropagation();
            }
        }
        onPaste(ev) {
            if (this.env.model.getters.getEditionMode() !== "inactive") {
                ev.stopPropagation();
            }
        }
        /*
         * Triggered automatically by the content-editable between the keydown and key up
         * */
        onInput(ev) {
            if (!this.shouldProcessInputEvents) {
                return;
            }
            if (ev.inputType === "insertFromPaste" &&
                this.env.model.getters.getEditionMode() === "inactive") {
                return;
            }
            ev.stopPropagation();
            let content;
            if (this.env.model.getters.getEditionMode() === "inactive") {
                content = ev.data || "";
            }
            else {
                content = this.contentHelper.getText();
            }
            if (this.props.focus === "inactive") {
                return this.props.onComposerCellFocused?.(content);
            }
            let selection = this.contentHelper.getCurrentSelection();
            if (ev.inputType === "insertParagraph") {
                const start = Math.min(selection.start, selection.end);
                const end = Math.max(selection.start, selection.end);
                content = content.slice(0, start) + NEWLINE + content.slice(end);
                selection = { start: start + 1, end: start + 1 };
            }
            this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
            this.env.model.dispatch("SET_CURRENT_CONTENT", {
                content,
                selection,
            });
            this.processTokenAtCursor();
        }
        onKeyup(ev) {
            if (this.contentHelper.el === document.activeElement) {
                if (this.autoCompleteState.showProvider && ["ArrowUp", "ArrowDown"].includes(ev.key)) {
                    return;
                }
                if (this.env.model.getters.isSelectingForComposer() && ev.key?.startsWith("Arrow")) {
                    return;
                }
                const { start: oldStart, end: oldEnd } = this.env.model.getters.getComposerSelection();
                const { start, end } = this.contentHelper.getCurrentSelection();
                if (start !== oldStart || end !== oldEnd) {
                    this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", { start, end });
                }
                this.processTokenAtCursor();
            }
        }
        showFunctionAutocomplete(searchTerm) {
            const searchTermUpperCase = searchTerm.toUpperCase();
            if (!this.env.model.getters.getCurrentContent().startsWith("=") ||
                searchTermUpperCase === "TRUE" ||
                searchTermUpperCase === "FALSE") {
                return;
            }
            this.autoCompleteState.showProvider = true;
            this.autoCompleteState.type = "function";
            let values = Object.entries(functionRegistry.content)
                .filter(([_, { hidden }]) => !hidden)
                .map(([text, { description }]) => {
                return {
                    text,
                    description,
                };
            })
                .sort((a, b) => {
                return a.text.length - b.text.length || a.text.localeCompare(b.text);
            });
            if (searchTerm) {
                values = fuzzyLookup(searchTerm, values, (t) => t.text).slice(0, AUTOCOMPLETE_ENTRIES);
            }
            this.autoCompleteState.values = values.slice(0, AUTOCOMPLETE_ENTRIES);
            this.autoCompleteState.getHtmlContent = (value) => getHtmlContentFromPattern(searchTerm, value, COMPOSER_ASSISTANT_COLOR, "o-semi-bold");
            this.autoCompleteState.selectedIndex = 0;
        }
        updateAutoCompleteIndex(index) {
            this.autoCompleteState.selectedIndex = clip(0, index, 10);
        }
        /**
         * This is required to ensure the content helper selection is
         * properly updated on "onclick" events. Depending on the browser,
         * the callback onClick from the composer will be executed before
         * the selection was updated in the dom, which means we capture an
         * wrong selection which is then forced upon the content helper on
         * processContent.
         */
        onMousedown(ev) {
            if (ev.button > 0) {
                // not main button, probably a context menu
                return;
            }
            this.contentHelper.removeSelection();
        }
        onClick() {
            if (this.env.model.getters.isReadonly()) {
                return;
            }
            const newSelection = this.contentHelper.getCurrentSelection();
            this.env.model.dispatch("STOP_COMPOSER_RANGE_SELECTION");
            this.props.onComposerContentFocused();
            if (this.props.focus === "inactive") ;
            this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", newSelection);
            this.processTokenAtCursor();
        }
        onDblClick() {
            if (this.env.model.getters.isReadonly()) {
                return;
            }
            const composerContent = this.env.model.getters.getCurrentContent();
            const isValidFormula = composerContent.startsWith("=");
            if (isValidFormula) {
                const tokens = this.env.model.getters.getCurrentTokens();
                const currentSelection = this.contentHelper.getCurrentSelection();
                if (currentSelection.start === currentSelection.end)
                    return;
                const currentSelectedText = composerContent.substring(currentSelection.start, currentSelection.end);
                const token = tokens.filter((token) => token.value.includes(currentSelectedText) &&
                    token.start <= currentSelection.start &&
                    token.end >= currentSelection.end)[0];
                if (!token) {
                    return;
                }
                if (token.type === "REFERENCE") {
                    this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", {
                        start: token.start,
                        end: token.end,
                    });
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        processContent() {
            if (this.compositionActive) {
                return;
            }
            this.shouldProcessInputEvents = false;
            if (this.props.focus !== "inactive") {
                this.contentHelper.el.focus();
            }
            const content = this.getContentLines();
            this.contentHelper.setText(content);
            if (content.length !== 0 && content.length[0] !== 0) {
                if (this.props.focus !== "inactive") {
                    // Put the cursor back where it was before the rendering
                    const { start, end } = this.env.model.getters.getComposerSelection();
                    this.contentHelper.selectRange(start, end);
                }
                this.contentHelper.scrollSelectionIntoView();
            }
            this.shouldProcessInputEvents = true;
        }
        /**
         * Get the HTML content corresponding to the current composer token, divided by lines.
         */
        getContentLines() {
            let value = this.env.model.getters.getCurrentContent();
            const isValidFormula = value.startsWith("=");
            if (value === "") {
                return [];
            }
            else if (isValidFormula && this.props.focus !== "inactive") {
                return this.splitHtmlContentIntoLines(this.getColoredTokens());
            }
            return this.splitHtmlContentIntoLines([{ value }]);
        }
        getColoredTokens() {
            const tokens = this.env.model.getters.getCurrentTokens();
            const tokenAtCursor = this.env.model.getters.getTokenAtCursor();
            const result = [];
            const { end, start } = this.env.model.getters.getComposerSelection();
            for (const token of tokens) {
                switch (token.type) {
                    case "OPERATOR":
                    case "NUMBER":
                    case "ARG_SEPARATOR":
                    case "STRING":
                        result.push({ value: token.value, color: tokenColors[token.type] || "#000" });
                        break;
                    case "REFERENCE":
                        const { xc, sheetName } = splitReference(token.value);
                        result.push({ value: token.value, color: this.rangeColor(xc, sheetName) || "#000" });
                        break;
                    case "SYMBOL":
                        const value = token.value;
                        const upperCaseValue = value.toUpperCase();
                        if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
                            result.push({ value: token.value, color: tokenColors.NUMBER });
                        }
                        else if (upperCaseValue in functionRegistry.content) {
                            result.push({ value: token.value, color: tokenColors.FUNCTION });
                        }
                        else {
                            result.push({ value: token.value, color: "#000" });
                        }
                        break;
                    case "LEFT_PAREN":
                    case "RIGHT_PAREN":
                        // Compute the matching parenthesis
                        if (tokenAtCursor &&
                            ["LEFT_PAREN", "RIGHT_PAREN"].includes(tokenAtCursor.type) &&
                            tokenAtCursor.parenIndex &&
                            tokenAtCursor.parenIndex === token.parenIndex) {
                            result.push({ value: token.value, color: tokenColors.MATCHING_PAREN  });
                        }
                        else {
                            result.push({ value: token.value, color: tokenColors[token.type] || "#000" });
                        }
                        break;
                    default:
                        result.push({ value: token.value, color: "#000" });
                        break;
                }
                if (this.env.model.getters.showSelectionIndicator() && end === start && end === token.end) {
                    result[result.length - 1].class = selectionIndicatorClass;
                }
            }
            return result;
        }
        /**
         * Split an array of HTMLContents into lines. Each NEWLINE character encountered will create a new
         * line. Contents can be split into multiple parts if they contain multiple NEWLINE characters.
         */
        splitHtmlContentIntoLines(contents) {
            const contentSplitInLines = [];
            let currentLine = [];
            for (const content of contents) {
                if (content.value.includes(NEWLINE)) {
                    const lines = content.value.split(NEWLINE);
                    const lastLine = lines.pop();
                    for (const line of lines) {
                        currentLine.push({ color: content.color, value: line }); // don't copy class, only last line should keep it
                        contentSplitInLines.push(currentLine);
                        currentLine = [];
                    }
                    currentLine.push({ ...content, value: lastLine });
                }
                else {
                    currentLine.push(content);
                }
            }
            if (currentLine.length) {
                contentSplitInLines.push(currentLine);
            }
            // Remove useless empty contents
            const filteredLines = [];
            for (const line of contentSplitInLines) {
                if (line.every(this.isContentEmpty)) {
                    filteredLines.push([line[0]]);
                }
                else {
                    filteredLines.push(line.filter((content) => !this.isContentEmpty(content)));
                }
            }
            return filteredLines;
        }
        isContentEmpty(content) {
            return !(content.value || content.class);
        }
        rangeColor(xc, sheetName) {
            if (this.props.focus === "inactive") {
                return undefined;
            }
            const highlights = this.env.model.getters.getHighlights();
            const refSheet = sheetName
                ? this.env.model.getters.getSheetIdByName(sheetName)
                : this.env.model.getters.getCurrentEditedCell()?.sheetId;
            const highlight = highlights.find((highlight) => {
                if (highlight.sheetId !== refSheet)
                    return false;
                const range = this.env.model.getters.getRangeFromSheetXC(refSheet, xc);
                let zone = range.zone;
                zone = getZoneArea(zone) === 1 ? this.env.model.getters.expandZone(refSheet, zone) : zone;
                return isEqual(zone, highlight.zone);
            });
            return highlight && highlight.color ? highlight.color : undefined;
        }
        /**
         * Compute the state of the composer from the tokenAtCursor.
         * If the token is a function or symbol (that isn't a cell/range reference) we have to initialize
         * the autocomplete engine otherwise we initialize the formula assistant.
         */
        processTokenAtCursor() {
            let content = this.env.model.getters.getCurrentContent();
            this.autoCompleteState.showProvider = false;
            this.functionDescriptionState.showDescription = false;
            const dataValidationAutocompleteValues = this.env.model.getters.getAutoCompleteDataValidationValues();
            if (!content.startsWith("=") && dataValidationAutocompleteValues.length) {
                this.showDataValidationAutocomplete(dataValidationAutocompleteValues);
            }
            if (content.startsWith("=")) {
                const token = this.env.model.getters.getTokenAtCursor();
                if (!token) {
                    return;
                }
                if (token.type === "SYMBOL") {
                    // initialize Autocomplete Dropdown
                    this.showFunctionAutocomplete(token.value);
                    return;
                }
                const tokenContext = token.functionContext;
                const parentFunction = tokenContext?.parent.toUpperCase();
                if (tokenContext &&
                    parentFunction &&
                    parentFunction in functions$2 &&
                    token.type !== "UNKNOWN") {
                    // initialize Formula Assistant
                    const description = functions$2[parentFunction];
                    const argPosition = tokenContext.argPosition;
                    this.functionDescriptionState.functionName = parentFunction;
                    this.functionDescriptionState.functionDescription = description;
                    this.functionDescriptionState.argToFocus = description.getArgToFocus(argPosition + 1) - 1;
                    this.functionDescriptionState.showDescription = true;
                }
            }
        }
        autoComplete(value) {
            if (!value) {
                return;
            }
            if (this.autoCompleteState.type === "function") {
                const tokenAtCursor = this.env.model.getters.getTokenAtCursor();
                if (tokenAtCursor) {
                    let start = tokenAtCursor.end;
                    let end = tokenAtCursor.end;
                    // shouldn't it be REFERENCE ?
                    if (["SYMBOL", "FUNCTION"].includes(tokenAtCursor.type)) {
                        start = tokenAtCursor.start;
                    }
                    const tokens = this.env.model.getters.getCurrentTokens();
                    if (tokens.length) {
                        value += "(";
                        const currentTokenIndex = tokens.map((token) => token.start).indexOf(tokenAtCursor.start);
                        if (currentTokenIndex + 1 < tokens.length) {
                            const nextToken = tokens[currentTokenIndex + 1];
                            if (nextToken.type === "LEFT_PAREN") {
                                end++;
                            }
                        }
                    }
                    this.env.model.dispatch("CHANGE_COMPOSER_CURSOR_SELECTION", { start, end });
                }
                this.env.model.dispatch("REPLACE_COMPOSER_CURSOR_SELECTION", { text: value });
            }
            else {
                this.env.model.dispatch("SET_CURRENT_CONTENT", { content: value });
                this.env.model.dispatch("STOP_EDITION");
            }
            this.processTokenAtCursor();
        }
        showDataValidationAutocomplete(values) {
            this.autoCompleteState.showProvider = true;
            this.autoCompleteState.type = "dataValidation";
            this.autoCompleteState.selectedIndex = undefined;
            this.autoCompleteState.values = values.map((value) => ({ text: value, description: "" }));
            this.autoCompleteState.getHtmlContent = (value) => [{ value }];
        }
    }
    Composer.props = {
        inputStyle: { type: String, optional: true },
        rect: { type: Object, optional: true },
        delimitation: { type: Object, optional: true },
        focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
        onComposerCellFocused: { type: Function, optional: true },
        onComposerContentFocused: Function,
        isDefaultFocus: { type: Boolean, optional: true },
    };

    const COMPOSER_BORDER_WIDTH = 3 * 0.4 * window.devicePixelRatio || 1;
    const GRID_CELL_REFERENCE_TOP_OFFSET = 28;
    css /* scss */ `
  div.o-grid-composer {
    z-index: ${ComponentsImportance.GridComposer};
    box-sizing: border-box;
    position: absolute;
    border: ${COMPOSER_BORDER_WIDTH}px solid ${SELECTION_BORDER_COLOR};

    display: flex;
    align-items: center;
  }

  div.o-cell-reference {
    position: absolute;
    z-index: ${ComponentsImportance.GridComposer};
    background: ${SELECTION_BORDER_COLOR};
    color: white;
    font-size: 12px;
    line-height: 14px;
    padding: 6px 7px;
    border-radius: 4px;
  }
`;
    /**
     * This component is a composer which positions itself on the grid at the anchor cell.
     * It also applies the style of the cell to the composer input.
     */
    class GridComposer extends owl.Component {
        static template = "o-spreadsheet-GridComposer";
        static components = { Composer };
        rect = this.defaultRect;
        isEditing = false;
        isCellReferenceVisible;
        get defaultRect() {
            return { x: 0, y: 0, width: 0, height: 0 };
        }
        setup() {
            owl.onWillUpdateProps(() => {
                this.updateComponentPosition();
                this.updateCellReferenceVisibility();
            });
        }
        get shouldDisplayCellReference() {
            return this.isCellReferenceVisible;
        }
        get cellReference() {
            if (!this.env.model.getters.getCurrentEditedCell()) {
                return "";
            }
            const { col, row, sheetId } = this.env.model.getters.getCurrentEditedCell();
            const prefixSheet = sheetId !== this.env.model.getters.getActiveSheetId();
            return `${prefixSheet ? getCanonicalSheetName(this.env.model.getters.getSheetName(sheetId)) + "!" : ""}${toXC(col, row)}`;
        }
        get cellReferenceStyle() {
            const { x: left, y: top } = this.rect;
            return cssPropertiesToCss({
                left: `${left - COMPOSER_BORDER_WIDTH}px`,
                top: `${top - GRID_CELL_REFERENCE_TOP_OFFSET}px`,
            });
        }
        get composerProps() {
            const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
            return {
                rect: { ...this.rect },
                delimitation: {
                    width,
                    height,
                },
                focus: this.props.focus,
                isDefaultFocus: true,
                onComposerContentFocused: this.props.onComposerContentFocused,
                onComposerCellFocused: this.props.onComposerCellFocused,
            };
        }
        get containerStyle() {
            if (this.env.model.getters.getEditionMode() === "inactive" || !this.rect) {
                return `
        position: absolute;
        z-index: -1000;
      `;
            }
            const isFormula = this.env.model.getters.getCurrentContent().startsWith("=");
            const cell = this.env.model.getters.getActiveCell();
            const position = this.env.model.getters.getActivePosition();
            const style = this.env.model.getters.getCellComputedStyle(position);
            // position style
            const { x: left, y: top, width, height } = this.rect;
            // color style
            const background = (!isFormula && style.fillColor) || "#ffffff";
            const color = (!isFormula && style.textColor) || "#000000";
            // font style
            const fontSize = (!isFormula && style.fontSize) || 10;
            const fontWeight = !isFormula && style.bold ? "bold" : undefined;
            const fontStyle = !isFormula && style.italic ? "italic" : "normal";
            const textDecoration = !isFormula ? getTextDecoration(style) : "none";
            // align style
            let textAlign = "left";
            if (!isFormula) {
                textAlign = style.align || cell.defaultAlign;
            }
            const maxHeight = this.props.gridDims.height - this.rect.y;
            const maxWidth = this.props.gridDims.width - this.rect.x;
            /**
             * min-size is on the container, not the composer element, because we want to have the same size as the cell by default,
             * including all the paddings/margins of the composer
             *
             * The +-1 are there to include cell borders in the composer sizing/positioning
             */
            return cssPropertiesToCss({
                left: `${left - 1}px`,
                top: `${top}px`,
                "min-width": `${width + 1}px`,
                "min-height": `${height + 1}px`,
                "max-width": `${maxWidth}px`,
                "max-height": `${maxHeight}px`,
                background,
                color,
                "font-size": `${fontSizeInPixels(fontSize)}px`,
                "font-weight": fontWeight,
                "font-style": fontStyle,
                "text-decoration": textDecoration,
                "text-align": textAlign,
            });
        }
        updateComponentPosition() {
            const isEditing = this.env.model.getters.getEditionMode() !== "inactive";
            if (this.isEditing !== isEditing) {
                this.isEditing = isEditing;
                if (!isEditing) {
                    this.rect = this.defaultRect;
                    this.env.focusableElement.focus();
                    return;
                }
                const position = this.env.model.getters.getActivePosition();
                const zone = this.env.model.getters.expandZone(position.sheetId, positionToZone(position));
                this.rect = this.env.model.getters.getVisibleRect(zone);
            }
        }
        updateCellReferenceVisibility() {
            if (this.isCellReferenceVisible || this.env.model.getters.getEditionMode() === "inactive") {
                return;
            }
            const sheetId = this.env.model.getters.getActiveSheetId();
            const zone = this.env.model.getters.getSelectedZone();
            const rect = this.env.model.getters.getVisibleRect(zone);
            if (!deepEquals(rect, this.rect) ||
                sheetId !== this.env.model.getters.getCurrentEditedCell().sheetId) {
                this.isCellReferenceVisible = true;
            }
        }
    }
    GridComposer.props = {
        focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
        onComposerContentFocused: Function,
        gridDims: Object,
        onComposerCellFocused: Function,
    };

    const CSS$1 = css /* scss */ `
  .o-grid-cell-icon {
    width: ${GRID_ICON_EDGE_LENGTH}px;
    height: ${GRID_ICON_EDGE_LENGTH}px;
  }
`;
    class GridCellIcon extends owl.Component {
        static style = CSS$1;
        static template = "o-spreadsheet-GridCellIcon";
        get iconStyle() {
            const x = this.getIconHorizontalPosition();
            const y = this.getIconVerticalPosition();
            return cssPropertiesToCss({
                top: `${y + (this.props.offset?.y || 0)}px`,
                left: `${x + (this.props.offset?.x || 0)}px`,
            });
        }
        getIconVerticalPosition() {
            const { sheetId, row } = this.props.cellPosition;
            const merge = this.env.model.getters.getMerge(this.props.cellPosition);
            const start = this.env.model.getters.getRowDimensionsInViewport(sheetId, row).start;
            const end = this.env.model.getters.getRowDimensionsInViewport(sheetId, merge ? merge.bottom : row).end;
            const cell = this.env.model.getters.getCell(this.props.cellPosition);
            const align = this.props.verticalAlign || cell?.style?.verticalAlign || DEFAULT_VERTICAL_ALIGN;
            switch (align) {
                case "bottom":
                    return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
                case "top":
                    return start + GRID_ICON_MARGIN;
                default:
                    const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
                    return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
            }
        }
        getIconHorizontalPosition() {
            const { sheetId, col } = this.props.cellPosition;
            const merge = this.env.model.getters.getMerge(this.props.cellPosition);
            const start = this.env.model.getters.getColDimensionsInViewport(sheetId, col).start;
            const end = this.env.model.getters.getColDimensionsInViewport(sheetId, merge ? merge.right : col).end;
            const cell = this.env.model.getters.getCell(this.props.cellPosition);
            const evaluatedCell = this.env.model.getters.getEvaluatedCell(this.props.cellPosition);
            const align = this.props.horizontalAlign || cell?.style?.align || evaluatedCell.defaultAlign;
            switch (align) {
                case "right":
                    return end - GRID_ICON_MARGIN - GRID_ICON_EDGE_LENGTH;
                case "left":
                    return start + GRID_ICON_MARGIN;
                default:
                    const centeringOffset = Math.floor((end - start - GRID_ICON_EDGE_LENGTH) / 2);
                    return end - GRID_ICON_EDGE_LENGTH - centeringOffset;
            }
        }
        isPositionVisible(position) {
            const rect = this.env.model.getters.getVisibleRect(positionToZone(position));
            return !(rect.width === 0 || rect.height === 0);
        }
    }
    GridCellIcon.props = {
        cellPosition: Object,
        horizontalAlign: { type: String, optional: true },
        verticalAlign: { type: String, optional: true },
        offset: { type: Object, optional: true },
        slots: Object,
    };

    const CSS = css /* scss */ `
  .o-filter-icon {
    color: ${FILTERS_COLOR};
    display: flex;
    align-items: center;
    justify-content: center;
    width: ${GRID_ICON_EDGE_LENGTH}px;
    height: ${GRID_ICON_EDGE_LENGTH}px;
  }
  .o-filter-icon:hover {
    background: ${FILTERS_COLOR};
    color: #fff;
  }
`;
    class FilterIcon extends owl.Component {
        static style = CSS;
        static template = "o-spreadsheet-FilterIcon";
        onClick() {
            const position = this.props.cellPosition;
            const activePopoverType = this.env.model.getters.getPersistentPopoverTypeAtPosition(position);
            if (activePopoverType && activePopoverType === "FilterMenu") {
                this.env.model.dispatch("CLOSE_CELL_POPOVER");
                return;
            }
            const { col, row } = position;
            this.env.model.dispatch("OPEN_CELL_POPOVER", {
                col,
                row,
                popoverType: "FilterMenu",
            });
        }
        get isFilterActive() {
            return this.env.model.getters.isFilterActive(this.props.cellPosition);
        }
    }
    FilterIcon.props = {
        cellPosition: Object,
    };

    class FilterIconsOverlay extends owl.Component {
        static template = "o-spreadsheet-FilterIconsOverlay";
        static components = {
            GridCellIcon,
            FilterIcon,
        };
        static defaultProps = {
            gridPosition: { x: 0, y: 0 },
        };
        getFilterHeadersPositions() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const headerPositions = this.env.model.getters.getFilterHeaders(sheetId);
            return headerPositions.map((position) => ({ sheetId, ...position }));
        }
    }
    FilterIconsOverlay.props = {
        gridPosition: { type: Object, optional: true },
    };

    const CHECKBOX_WIDTH = 15;
    const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
    css /* scss */ `
  .o-dv-checkbox {
    box-sizing: border-box !important;
    width: ${CHECKBOX_WIDTH}px;
    height: ${CHECKBOX_WIDTH}px;
    accent-color: #808080;
    margin: ${MARGIN}px;
  }
`;
    class DataValidationCheckbox extends owl.Component {
        static template = "o-spreadsheet-DataValidationCheckbox";
        onCheckboxChange(ev) {
            const newValue = ev.target.checked;
            const { sheetId, col, row } = this.props.cellPosition;
            const cellContent = newValue ? "TRUE" : "FALSE";
            this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
        }
        get checkBoxValue() {
            return !!this.env.model.getters.getEvaluatedCell(this.props.cellPosition).value;
        }
        get isDisabled() {
            const cell = this.env.model.getters.getCell(this.props.cellPosition);
            return !!cell?.isFormula;
        }
    }
    DataValidationCheckbox.props = {
        cellPosition: Object,
    };

    const ICON_WIDTH = 13;
    css /* scss */ `
  .o-dv-list-icon {
    color: #808080;
    border-radius: 1px;
    height: ${GRID_ICON_EDGE_LENGTH}px;
    width: ${GRID_ICON_EDGE_LENGTH}px;

    &:hover {
      color: #ffffff;
      background-color: #808080;
    }

    svg {
      width: ${ICON_WIDTH}px;
      height: ${ICON_WIDTH}px;
    }
  }
`;
    class DataValidationListIcon extends owl.Component {
        static template = "o-spreadsheet-DataValidationListIcon";
        onClick() {
            const { col, row } = this.props.cellPosition;
            this.env.model.selection.selectCell(col, row);
            this.env.startCellEdition();
        }
    }
    DataValidationListIcon.props = {
        cellPosition: Object,
    };

    class DataValidationOverlay extends owl.Component {
        static template = "o-spreadsheet-DataValidationOverlay";
        static components = { GridCellIcon, DataValidationCheckbox, DataValidationListIcon };
        get checkBoxCellPositions() {
            return this.env.model.getters.getDataValidationCheckBoxCellPositions();
        }
        get listIconsCellPositions() {
            return this.env.model.getters.isReadonly()
                ? []
                : this.env.model.getters.getDataValidationListCellsPositions();
        }
    }
    DataValidationOverlay.props = {};

    /**
     * Transform a figure with coordinates from the model, to coordinates as they are shown on the screen,
     * taking into account the scroll position of the active sheet and the frozen panes.
     */
    function internalFigureToScreen(getters, fig) {
        return { ...fig, ...internalToScreenCoordinates(getters, { x: fig.x, y: fig.y }) };
    }
    /**
     * Transform a figure with coordinates as they are shown on the screen, to coordinates as they are in the model,
     * taking into account the scroll position of the active sheet and the frozen panes.
     *
     * Note that this isn't  exactly the reverse operation as internalFigureToScreen, because the figure will always be on top
     * of the frozen panes.
     */
    function screenFigureToInternal(getters, fig) {
        return { ...fig, ...screenCoordinatesToInternal(getters, { x: fig.x, y: fig.y }) };
    }
    function internalToScreenCoordinates(getters, { x, y }) {
        const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates();
        const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
        x = x < viewportX ? x : x - scrollX;
        y = y < viewportY ? y : y - scrollY;
        return { x, y };
    }
    function screenCoordinatesToInternal(getters, { x, y }) {
        const { x: viewportX, y: viewportY } = getters.getMainViewportCoordinates();
        const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
        x = viewportX && x < viewportX ? x : x + scrollX;
        y = viewportY && y < viewportY ? y : y + scrollY;
        return { x, y };
    }

    function dragFigureForMove({ x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, initialFigure, { x: viewportX, y: viewportY }, { maxX, maxY }, { scrollX, scrollY }) {
        const minX = viewportX ? 0 : -scrollX;
        const minY = viewportY ? 0 : -scrollY;
        const deltaX = mouseX - mouseInitialX;
        const newX = clamp(initialFigure.x + deltaX, minX, maxX - initialFigure.width - scrollX);
        const deltaY = mouseY - mouseInitialY;
        const newY = clamp(initialFigure.y + deltaY, minY, maxY - initialFigure.height - scrollY);
        return { ...initialFigure, x: newX, y: newY };
    }
    function clamp(value, min, max) {
        return Math.min(Math.max(value, min), max);
    }
    function dragFigureForResize(initialFigure, dirX, dirY, { x: mouseX, y: mouseY }, { x: mouseInitialX, y: mouseInitialY }, keepRatio, minFigSize) {
        let { x, y, width, height } = initialFigure;
        if (keepRatio && dirX != 0 && dirY != 0) {
            const deltaX = Math.min(dirX * (mouseInitialX - mouseX), initialFigure.width - minFigSize);
            const deltaY = Math.min(dirY * (mouseInitialY - mouseY), initialFigure.height - minFigSize);
            const fraction = Math.min(deltaX / initialFigure.width, deltaY / initialFigure.height);
            width = initialFigure.width * (1 - fraction);
            height = initialFigure.height * (1 - fraction);
            if (dirX < 0) {
                x = initialFigure.x + initialFigure.width * fraction;
            }
            if (dirY < 0) {
                y = initialFigure.y + initialFigure.height * fraction;
            }
        }
        else {
            const deltaX = Math.max(dirX * (mouseX - mouseInitialX), minFigSize - initialFigure.width);
            const deltaY = Math.max(dirY * (mouseY - mouseInitialY), minFigSize - initialFigure.height);
            width = initialFigure.width + deltaX;
            height = initialFigure.height + deltaY;
            if (dirX < 0) {
                x = initialFigure.x - deltaX;
            }
            if (dirY < 0) {
                y = initialFigure.y - deltaY;
            }
        }
        // Restrict resizing if x or y reaches header boundaries
        if (x < 0) {
            width += x;
            x = 0;
        }
        if (y < 0) {
            height += y;
            y = 0;
        }
        return { ...initialFigure, x, y, width, height };
    }

    const SNAP_MARGIN = 5;
    /**
     * Try to snap the given figure to other figures when moving the figure, and return the snapped
     * figure and the possible snap lines, if any were found
     */
    function snapForMove(getters, figureToSnap, otherFigures) {
        const snappedFigure = { ...figureToSnap };
        const verticalSnapLine = getSnapLine(getters, snappedFigure, ["hCenter", "right", "left"], otherFigures, ["hCenter", "right", "left"]);
        const horizontalSnapLine = getSnapLine(getters, snappedFigure, ["vCenter", "bottom", "top"], otherFigures, ["vCenter", "bottom", "top"]);
        const { y: viewportY, x: viewportX } = getters.getMainViewportCoordinates();
        const { scrollY, scrollX } = getters.getActiveSheetScrollInfo();
        // If the snap cause the figure to change pane, we need to also apply the scroll as an offset
        if (horizontalSnapLine) {
            snappedFigure.y -= horizontalSnapLine.snapOffset;
            const isBaseFigFrozenY = figureToSnap.y < viewportY;
            const isSnappedFrozenY = snappedFigure.y < viewportY;
            if (isBaseFigFrozenY && !isSnappedFrozenY)
                snappedFigure.y += scrollY;
            else if (!isBaseFigFrozenY && isSnappedFrozenY)
                snappedFigure.y -= scrollY;
        }
        if (verticalSnapLine) {
            snappedFigure.x -= verticalSnapLine.snapOffset;
            const isBaseFigFrozenX = figureToSnap.x < viewportX;
            const isSnappedFrozenX = snappedFigure.x < viewportX;
            if (isBaseFigFrozenX && !isSnappedFrozenX)
                snappedFigure.x += scrollX;
            else if (!isBaseFigFrozenX && isSnappedFrozenX)
                snappedFigure.x -= scrollX;
        }
        return { snappedFigure, verticalSnapLine, horizontalSnapLine };
    }
    /**
     * Try to snap the given figure to the other figures when resizing the figure, and return the snapped
     * figure and the possible snap lines, if any were found
     */
    function snapForResize(getters, resizeDirX, resizeDirY, figureToSnap, otherFigures) {
        const snappedFigure = { ...figureToSnap };
        // Vertical snap line
        const verticalSnapLine = getSnapLine(getters, snappedFigure, [resizeDirX === -1 ? "left" : "right"], otherFigures, ["right", "left"]);
        if (verticalSnapLine) {
            if (resizeDirX === 1) {
                snappedFigure.width -= verticalSnapLine.snapOffset;
            }
            else if (resizeDirX === -1) {
                snappedFigure.x -= verticalSnapLine.snapOffset;
                snappedFigure.width += verticalSnapLine.snapOffset;
            }
        }
        // Horizontal snap line
        const horizontalSnapLine = getSnapLine(getters, snappedFigure, [resizeDirY === -1 ? "top" : "bottom"], otherFigures, ["bottom", "top"]);
        if (horizontalSnapLine) {
            if (resizeDirY === 1) {
                snappedFigure.height -= horizontalSnapLine.snapOffset;
            }
            else if (resizeDirY === -1) {
                snappedFigure.y -= horizontalSnapLine.snapOffset;
                snappedFigure.height += horizontalSnapLine.snapOffset;
            }
        }
        snappedFigure.x = Math.round(snappedFigure.x);
        snappedFigure.y = Math.round(snappedFigure.y);
        snappedFigure.height = Math.round(snappedFigure.height);
        snappedFigure.width = Math.round(snappedFigure.width);
        return { snappedFigure, verticalSnapLine, horizontalSnapLine };
    }
    /**
     * Get the position of snap axes for the given figure
     *
     * @param figure the figure
     * @param axesTypes the list of axis types to return the positions of
     */
    function getVisibleAxes(getters, figure, axesTypes) {
        const axes = axesTypes.map((axisType) => getAxis(figure, axisType));
        return axes
            .filter((axis) => isAxisVisible(getters, figure, axis))
            .map((axis) => getAxisScreenPosition(getters, figure, axis));
    }
    /**
     * We need two positions for the figure axis :
     *  - the position (core) of the axis in the figure. This is used to know whether or not the axis is
     *      displayed, or is hidden by the scroll/the frozen panes
     *  - the position in the screen, which is used to find snap matches. We cannot use the core position for this,
     *      because figures partially in frozen panes aren't displayed at their actual coordinates
     */
    function getAxisScreenPosition(getters, figure, figureAxis) {
        const screenFigure = internalFigureToScreen(getters, figure);
        return getAxis(screenFigure, figureAxis.axisType);
    }
    function isAxisVisible(getters, figure, axis) {
        const { x: mainViewportX, y: mainViewportY } = getters.getMainViewportCoordinates();
        const axisStartEndPositions = [];
        switch (axis.axisType) {
            case "top":
            case "bottom":
            case "vCenter":
                if (figure.y < mainViewportY)
                    return true;
                axisStartEndPositions.push({ x: figure.x, y: axis.position });
                axisStartEndPositions.push({ x: figure.x + figure.width, y: axis.position });
                break;
            case "left":
            case "right":
            case "hCenter":
                if (figure.x < mainViewportX)
                    return true;
                axisStartEndPositions.push({ x: axis.position, y: figure.y });
                axisStartEndPositions.push({ x: axis.position, y: figure.y + figure.height });
                break;
        }
        return axisStartEndPositions.some(getters.isPositionVisible);
    }
    /**
     * Get a snap line for the given figure, if the figure can snap to any other figure
     *
     * @param figureToSnap figure to get the snap line for
     * @param figAxesTypes figure axes of the given figure to be considered to find a snap line
     * @param otherFigures figures to match against the snapped figure to find a snap line
     * @param otherAxesTypes figure axes of the other figures to be considered to find a snap line
     */
    function getSnapLine(getters, figureToSnap, figAxesTypes, otherFigures, otherAxesTypes) {
        const axesOfFigure = getVisibleAxes(getters, figureToSnap, figAxesTypes);
        let closestMatch = undefined;
        for (const otherFigure of otherFigures) {
            const axesOfOtherFig = getVisibleAxes(getters, otherFigure, otherAxesTypes);
            for (const axisOfFigure of axesOfFigure) {
                for (const axisOfOtherFig of axesOfOtherFig) {
                    if (!canSnap(axisOfFigure.position, axisOfOtherFig.position))
                        continue;
                    const snapOffset = axisOfFigure.position - axisOfOtherFig.position;
                    if (closestMatch && snapOffset === closestMatch.snapOffset) {
                        closestMatch.matchedFigIds.push(otherFigure.id);
                    }
                    else if (!closestMatch || Math.abs(snapOffset) <= Math.abs(closestMatch.snapOffset)) {
                        closestMatch = {
                            matchedFigIds: [otherFigure.id],
                            snapOffset,
                            snappedAxisType: axisOfFigure.axisType,
                            position: axisOfOtherFig.position,
                        };
                    }
                }
            }
        }
        return closestMatch;
    }
    /** Check if two axes are close enough to snap */
    function canSnap(axisPosition1, axisPosition2) {
        return Math.abs(axisPosition1 - axisPosition2) <= SNAP_MARGIN;
    }
    function getAxis(fig, axisType) {
        let position = 0;
        switch (axisType) {
            case "top":
                position = fig.y;
                break;
            case "bottom":
                position = fig.y + fig.height - FIGURE_BORDER_WIDTH;
                break;
            case "vCenter":
                position = fig.y + Math.floor(fig.height / 2) - FIGURE_BORDER_WIDTH;
                break;
            case "left":
                position = fig.x;
                break;
            case "right":
                position = fig.x + fig.width - FIGURE_BORDER_WIDTH;
                break;
            case "hCenter":
                position = fig.x + Math.floor(fig.width / 2) - FIGURE_BORDER_WIDTH;
                break;
        }
        return { position, axisType: axisType };
    }

    css /*SCSS*/ `
  .o-figure-snap-line {
    position: relative;
    z-index: ${ComponentsImportance.FigureSnapLine};
    &.vertical {
      width: 0px;
      border-left: 1px dashed black;
    }
    &.horizontal {
      border-top: 1px dashed black;
      height: 0px;
    }
  }
`;
    /**
     * Each figure ⭐ is positioned inside a container `div` placed and sized
     * according to the split pane the figure is part of, or a separate container for the figure
     * currently drag & dropped. Any part of the figure outside of the container is hidden
     * thanks to its `overflow: hidden` property.
     *
     * Additionally, the figure is placed inside a "inverse viewport" `div` 🟥.
     * Its position represents the viewport position in the grid: its top/left
     * corner represents the top/left corner of the grid.
     *
     * It allows to position the figure inside this div regardless of the
     * (possibly freezed) viewports and the scrolling position.
     *
     * --: container limits
     * 🟥: inverse viewport
     * ⭐: figure top/left position
     *
     *                     container
     *                         ↓
     * |🟥--------------------------------------------
     * |  \                                          |
     * |   \                                         |
     * |    \                                        |
     * |     \          visible area                 |  no scroll
     * |      ⭐                                     |
     * |                                             |
     * |                                             |
     * -----------------------------------------------
     *
     * the scrolling of the pane is applied as an inverse offset
     * to the div which will in turn move the figure up and down
     * inside the container.
     * Hence, once the figure position is (resp. partly) out of
     * the container dimensions, it will be (resp. partly) hidden.
     *
     * The same reasoning applies to the horizontal axis.
     *
     *  🟥 ························
     *    \                       ↑
     *     \                      |
     *      \                     | inverse viewport = -1 * scroll of pane
     *       \                    |
     *        ⭐ <- not visible   |
     *                            ↓
     * -----------------------------------------------
     * |                                             |
     * |                                             |
     * |                                             |
     * |               visible area                  |
     * |                                             |
     * |                                             |
     * |                                             |
     * -----------------------------------------------
     *
     * In the case the d&d figure container, the container is the same as the "topLeft" container for
     * frozen pane (unaffected by scroll and always visible). The figure coordinates are transformed
     * for this container at the start of the d&d, and transformed back at the end to adapt to the scroll
     * that occurred during the drag & drop, and to position the figure on the correct pane.
     *
     */
    class FiguresContainer extends owl.Component {
        static template = "o-spreadsheet-FiguresContainer";
        static components = { FigureComponent };
        dnd = owl.useState({
            draggedFigure: undefined,
            horizontalSnap: undefined,
            verticalSnap: undefined,
        });
        setup() {
            owl.onMounted(() => {
                // horrible, but necessary
                // the following line ensures that we render the figures with the correct
                // viewport.  The reason is that whenever we initialize the grid
                // component, we do not know yet the actual size of the viewport, so the
                // first owl rendering is done with an empty viewport.  Only then we can
                // compute which figures should be displayed, so we have to force a
                // new rendering
                this.render();
            });
        }
        getVisibleFigures() {
            const visibleFigures = this.env.model.getters.getVisibleFigures();
            if (this.dnd.draggedFigure &&
                !visibleFigures.some((figure) => figure.id === this.dnd.draggedFigure?.id)) {
                visibleFigures.push(this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id));
            }
            return visibleFigures;
        }
        get containers() {
            const visibleFigures = this.getVisibleFigures();
            const containers = [];
            for (const containerType of [
                "topLeft",
                "topRight",
                "bottomLeft",
                "bottomRight",
            ]) {
                const containerFigures = visibleFigures.filter((figure) => this.getFigureContainer(figure) === containerType);
                if (containerFigures.length > 0) {
                    containers.push({
                        type: containerType,
                        figures: containerFigures,
                        style: this.getContainerStyle(containerType),
                        inverseViewportStyle: this.getInverseViewportPositionStyle(containerType),
                    });
                }
            }
            if (this.dnd.draggedFigure) {
                containers.push({
                    type: "dnd",
                    figures: [this.getDndFigure()],
                    style: this.getContainerStyle("dnd"),
                    inverseViewportStyle: this.getInverseViewportPositionStyle("dnd"),
                });
            }
            return containers;
        }
        getContainerStyle(container) {
            return this.rectToCss(this.getContainerRect(container));
        }
        rectToCss(rect) {
            return cssPropertiesToCss({
                left: `${rect.x}px`,
                top: `${rect.y}px`,
                width: `${rect.width}px`,
                height: `${rect.height}px`,
            });
        }
        getContainerRect(container) {
            const { width: viewWidth, height: viewHeight } = this.env.model.getters.getMainViewportRect();
            const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
            const x = ["bottomRight", "topRight"].includes(container) ? viewportX : 0;
            const width = viewWidth - x;
            const y = ["bottomRight", "bottomLeft"].includes(container) ? viewportY : 0;
            const height = viewHeight - y;
            return { x, y, width, height };
        }
        getInverseViewportPositionStyle(container) {
            const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
            const left = ["bottomRight", "topRight"].includes(container) ? -(viewportX + scrollX) : 0;
            const top = ["bottomRight", "bottomLeft"].includes(container) ? -(viewportY + scrollY) : 0;
            return cssPropertiesToCss({
                left: `${left}px`,
                top: `${top}px`,
            });
        }
        getFigureContainer(figure) {
            const { x: viewportX, y: viewportY } = this.env.model.getters.getMainViewportCoordinates();
            if (figure.id === this.dnd.draggedFigure?.id) {
                return "dnd";
            }
            else if (figure.x < viewportX && figure.y < viewportY) {
                return "topLeft";
            }
            else if (figure.x < viewportX) {
                return "bottomLeft";
            }
            else if (figure.y < viewportY) {
                return "topRight";
            }
            else {
                return "bottomRight";
            }
        }
        startDraggingFigure(figure, ev) {
            if (ev.button > 0 || this.env.model.getters.isReadonly()) {
                // not main button, probably a context menu and no d&d in readonly mode
                return;
            }
            const selectResult = this.env.model.dispatch("SELECT_FIGURE", { id: figure.id });
            if (!selectResult.isSuccessful) {
                return;
            }
            const sheetId = this.env.model.getters.getActiveSheetId();
            const initialMousePosition = { x: ev.clientX, y: ev.clientY };
            const maxDimensions = {
                maxX: this.env.model.getters.getColDimensions(sheetId, this.env.model.getters.getNumberCols(sheetId) - 1).end,
                maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end,
            };
            const { x, y } = internalFigureToScreen(this.env.model.getters, figure);
            const initialFig = { ...figure, x, y };
            const onMouseMove = (ev) => {
                const getters = this.env.model.getters;
                const currentMousePosition = { x: ev.clientX, y: ev.clientY };
                const draggedFigure = dragFigureForMove(currentMousePosition, initialMousePosition, initialFig, this.env.model.getters.getMainViewportCoordinates(), maxDimensions, getters.getActiveSheetScrollInfo());
                const otherFigures = this.getOtherFigures(figure.id);
                const internalDragged = screenFigureToInternal(getters, draggedFigure);
                const snapResult = snapForMove(getters, internalDragged, otherFigures);
                this.dnd.draggedFigure = internalFigureToScreen(getters, snapResult.snappedFigure);
                this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);
                this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);
            };
            const onMouseUp = (ev) => {
                if (!this.dnd.draggedFigure) {
                    return;
                }
                let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure);
                this.dnd.draggedFigure = undefined;
                this.dnd.horizontalSnap = undefined;
                this.dnd.verticalSnap = undefined;
                this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
            };
            startDnd(onMouseMove, onMouseUp);
        }
        /**
         * Initialize the resize of a figure with mouse movements
         *
         * @param dirX X direction of the resize. -1 : resize from the left border of the figure, 0 : no resize in X, 1 :
         * resize from the right border of the figure
         * @param dirY Y direction of the resize. -1 : resize from the top border of the figure, 0 : no resize in Y, 1 :
         * resize from the bottom border of the figure
         * @param ev Mouse Event
         */
        startResize(figure, dirX, dirY, ev) {
            ev.stopPropagation();
            const initialMousePosition = { x: ev.clientX, y: ev.clientY };
            const { x, y } = internalFigureToScreen(this.env.model.getters, figure);
            const initialFig = { ...figure, x, y };
            const keepRatio = figureRegistry.get(figure.tag).keepRatio || false;
            const minFigSize = figureRegistry.get(figure.tag).minFigSize || MIN_FIG_SIZE;
            const onMouseMove = (ev) => {
                const currentMousePosition = { x: ev.clientX, y: ev.clientY };
                const draggedFigure = dragFigureForResize(initialFig, dirX, dirY, currentMousePosition, initialMousePosition, keepRatio, minFigSize);
                const otherFigures = this.getOtherFigures(figure.id);
                const snapResult = snapForResize(this.env.model.getters, dirX, dirY, draggedFigure, otherFigures);
                this.dnd.draggedFigure = snapResult.snappedFigure;
                this.dnd.horizontalSnap = this.getSnap(snapResult.horizontalSnapLine);
                this.dnd.verticalSnap = this.getSnap(snapResult.verticalSnapLine);
            };
            const onMouseUp = (ev) => {
                if (!this.dnd.draggedFigure) {
                    return;
                }
                let { x, y } = screenFigureToInternal(this.env.model.getters, this.dnd.draggedFigure);
                const update = { x, y };
                if (dirX) {
                    update.width = this.dnd.draggedFigure.width;
                }
                if (dirY) {
                    update.height = this.dnd.draggedFigure.height;
                }
                this.env.model.dispatch("UPDATE_FIGURE", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    id: figure.id,
                    ...update,
                });
                this.dnd.draggedFigure = undefined;
                this.dnd.horizontalSnap = undefined;
                this.dnd.verticalSnap = undefined;
            };
            startDnd(onMouseMove, onMouseUp);
        }
        getOtherFigures(figId) {
            return this.getVisibleFigures().filter((f) => f.id !== figId);
        }
        getDndFigure() {
            const figure = this.getVisibleFigures().find((fig) => fig.id === this.dnd.draggedFigure?.id);
            if (!figure)
                throw new Error("Dnd figure not found");
            return {
                ...figure,
                ...this.dnd.draggedFigure,
            };
        }
        getFigureStyle(figure) {
            if (figure.id !== this.dnd.draggedFigure?.id)
                return "";
            return cssPropertiesToCss({
                opacity: "0.9",
                cursor: "grabbing",
            });
        }
        getSnap(snapLine) {
            if (!snapLine || !this.dnd.draggedFigure)
                return undefined;
            const figureVisibleRects = snapLine.matchedFigIds
                .map((id) => this.getVisibleFigures().find((fig) => fig.id === id))
                .filter(isDefined$1)
                .map((fig) => {
                const figOnSCreen = internalFigureToScreen(this.env.model.getters, fig);
                const container = this.getFigureContainer(fig);
                return rectIntersection(figOnSCreen, this.getContainerRect(container));
            })
                .filter(isDefined$1);
            const containerRect = rectUnion(this.dnd.draggedFigure, ...figureVisibleRects);
            return {
                line: snapLine,
                containerStyle: this.rectToCss(containerRect),
                lineStyle: this.getSnapLineStyle(snapLine, containerRect),
            };
        }
        getSnapLineStyle(snapLine, containerRect) {
            if (!snapLine)
                return "";
            if (["top", "vCenter", "bottom"].includes(snapLine.snappedAxisType)) {
                return cssPropertiesToCss({
                    top: `${snapLine.position - containerRect.y}px`,
                    left: `0px`,
                    width: `100%`,
                });
            }
            else {
                return cssPropertiesToCss({
                    top: `0px`,
                    left: `${snapLine.position - containerRect.x}px`,
                    height: `100%`,
                });
            }
        }
    }
    FiguresContainer.props = {
        onFigureDeleted: Function,
    };

    css /* scss */ `
  .o-grid-add-rows {
    input {
      box-sizing: border-box;
      width: 60px;
      height: 30px;
    }

    .o-validation-error {
      display: inline-block !important;
      margin-top: 0;
      margin-left: 8px;
    }
  }
`;
    class GridAddRowsFooter extends owl.Component {
        static template = "o-spreadsheet-GridAddRowsFooter";
        static components = { ValidationMessages };
        inputRef = owl.useRef("inputRef");
        state = owl.useState({
            inputValue: "100",
            errorFlag: false,
        });
        setup() {
            owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
        }
        get addRowsPosition() {
            const activeSheetId = this.env.model.getters.getActiveSheetId();
            const { numberOfRows } = this.env.model.getters.getSheetSize(activeSheetId);
            const { scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            const rowDimensions = this.env.model.getters.getRowDimensions(activeSheetId, numberOfRows - 1);
            const top = rowDimensions.end - scrollY;
            return cssPropertiesToCss({
                top: `${top}px`,
            });
        }
        get errorMessages() {
            return [_t("Please enter a number between 0 and 10000.")];
        }
        onKeydown(ev) {
            if (ev.key.toUpperCase() === "ESCAPE") {
                this.props.focusGrid();
            }
            else if (ev.key.toUpperCase() === "ENTER") {
                this.onConfirm();
            }
        }
        onInput(ev) {
            const value = ev.target.value;
            this.state.inputValue = value;
            const quantity = Number(value);
            this.state.errorFlag = Number.isNaN(quantity) || quantity <= 0 || quantity > 10000;
        }
        onConfirm() {
            if (this.state.errorFlag) {
                return;
            }
            const quantity = Number(this.state.inputValue);
            const activeSheetId = this.env.model.getters.getActiveSheetId();
            const rowNumber = this.env.model.getters.getNumberRows(activeSheetId);
            this.env.model.dispatch("ADD_COLUMNS_ROWS", {
                sheetId: activeSheetId,
                position: "after",
                base: rowNumber - 1,
                quantity,
                dimension: "ROW",
            });
            this.props.focusGrid();
            // After adding new rows, scroll down to the new last row
            const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();
            const { end } = this.env.model.getters.getRowDimensions(activeSheetId, rowNumber + quantity - 1);
            this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
                offsetX: scrollX,
                offsetY: end,
            });
        }
        onExternalClick(ev) {
            if (this.inputRef.el !== document.activeElement || ev.target === this.inputRef.el) {
                return;
            }
            this.props.focusGrid();
        }
    }
    GridAddRowsFooter.props = {
        focusGrid: Function,
    };

    /**
     * Manages an event listener on a ref. Useful for hooks that want to manage
     * event listeners, especially more than one. Prefer using t-on directly in
     * components. If your hook only needs a single event listener, consider simply
     * returning it from the hook and letting the user attach it with t-on.
     *
     * Adapted from Odoo Community - See https://github.com/odoo/odoo/blob/saas-16.2/addons/web/static/src/core/utils/hooks.js
     */
    function useRefListener(ref, ...listener) {
        owl.useEffect((el) => {
            el?.addEventListener(...listener);
            return () => el?.removeEventListener(...listener);
        }, () => [ref.el]);
    }

    /**
     * Repeatedly calls a callback function with a time delay between calls.
     */
    function useInterval(callback, delay) {
        let intervalId;
        const { setInterval, clearInterval } = window;
        owl.useEffect(() => {
            intervalId = setInterval(callback, delay);
            return () => clearInterval(intervalId);
        }, () => [delay]);
        return {
            pause: () => {
                clearInterval(intervalId);
                intervalId = undefined;
            },
            resume: () => {
                if (intervalId === undefined) {
                    intervalId = setInterval(callback, delay);
                }
            },
        };
    }

    const CURSOR_SVG = /*xml*/ `
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="16"><path d="M6.5.4c1.3-.8 2.9-.1 3.8 1.4l2.9 5.1c.2.4.9 1.6-.4 2.3l-1.6.9 1.8 3.1c.2.4.1 1-.2 1.2l-1.6 1c-.3.1-.9 0-1.1-.4l-1.8-3.1-1.6 1c-.6.4-1.7 0-2.2-.8L0 4.3"/><path fill="#fff" d="M9.1 2a1.4 1.1 60 0 0-1.7-.6L5.5 2.5l.9 1.6-1 .6-.9-1.6-.6.4 1.8 3.1-1.3.7-1.8-3.1-1 .6 3.8 6.6 6.8-3.98M3.9 8.8 10.82 5l.795 1.4-6.81 3.96"/></svg>
`;
    css /* scss */ `
  .o-paint-format-cursor {
    cursor: url("data:image/svg+xml,${encodeURIComponent(CURSOR_SVG)}"), auto;
  }
`;
    function useCellHovered(env, gridRef, callback) {
        let hoveredPosition = {
            col: undefined,
            row: undefined,
        };
        const { Date } = window;
        let x = 0;
        let y = 0;
        let lastMoved = 0;
        function getPosition() {
            const col = env.model.getters.getColIndex(x);
            const row = env.model.getters.getRowIndex(y);
            return { col, row };
        }
        const { pause, resume } = useInterval(checkTiming, 200);
        function checkTiming() {
            const { col, row } = getPosition();
            const delta = Date.now() - lastMoved;
            if (delta > 300 && (col !== hoveredPosition.col || row !== hoveredPosition.row)) {
                setPosition(undefined, undefined);
            }
            if (delta > 300) {
                if (col < 0 || row < 0) {
                    return;
                }
                setPosition(col, row);
            }
        }
        function updateMousePosition(e) {
            if (gridRef.el === e.target) {
                x = e.offsetX;
                y = e.offsetY;
                lastMoved = Date.now();
            }
        }
        function recompute() {
            const { col, row } = getPosition();
            if (col !== hoveredPosition.col || row !== hoveredPosition.row) {
                setPosition(undefined, undefined);
            }
        }
        function onMouseLeave(e) {
            const x = e.offsetX;
            const y = e.offsetY;
            const gridRect = getBoundingRectAsPOJO(gridRef.el);
            if (y < 0 || y > gridRect.height || x < 0 || x > gridRect.width) {
                return updateMousePosition(e);
            }
            else {
                return pause();
            }
        }
        useRefListener(gridRef, "mousemove", updateMousePosition);
        useRefListener(gridRef, "mouseleave", onMouseLeave);
        useRefListener(gridRef, "mouseenter", resume);
        useRefListener(gridRef, "mousedown", recompute);
        owl.useExternalListener(window, "click", handleGlobalClick);
        function handleGlobalClick(e) {
            const target = e.target;
            const grid = gridRef.el;
            if (!grid.contains(target)) {
                setPosition(undefined, undefined);
            }
        }
        function setPosition(col, row) {
            if (col !== hoveredPosition.col || row !== hoveredPosition.row) {
                hoveredPosition.col = col;
                hoveredPosition.row = row;
                callback({ col, row });
            }
        }
        return hoveredPosition;
    }
    function useTouchMove(gridRef, handler, canMoveUp) {
        let x = null;
        let y = null;
        function onTouchStart(ev) {
            if (ev.touches.length !== 1)
                return;
            x = ev.touches[0].clientX;
            y = ev.touches[0].clientY;
        }
        function onTouchEnd() {
            x = null;
            y = null;
        }
        function onTouchMove(ev) {
            if (ev.touches.length !== 1)
                return;
            // On mobile browsers, swiping down is often associated with "pull to refresh".
            // We only want this behavior if the grid is already at the top.
            // Otherwise we only want to move the canvas up, without triggering any refresh.
            if (canMoveUp()) {
                ev.preventDefault();
                ev.stopPropagation();
            }
            const currentX = ev.touches[0].clientX;
            const currentY = ev.touches[0].clientY;
            handler(x - currentX, y - currentY);
            x = currentX;
            y = currentY;
        }
        useRefListener(gridRef, "touchstart", onTouchStart);
        useRefListener(gridRef, "touchend", onTouchEnd);
        useRefListener(gridRef, "touchmove", onTouchMove);
    }
    class GridOverlay extends owl.Component {
        static template = "o-spreadsheet-GridOverlay";
        static components = { FiguresContainer, DataValidationOverlay, GridAddRowsFooter };
        static defaultProps = {
            onCellHovered: () => { },
            onCellDoubleClicked: () => { },
            onCellClicked: () => { },
            onCellRightClicked: () => { },
            onGridResized: () => { },
            onFigureDeleted: () => { },
        };
        gridOverlay = owl.useRef("gridOverlay");
        gridOverlayRect = useAbsoluteBoundingRect(this.gridOverlay);
        setup() {
            useCellHovered(this.env, this.gridOverlay, this.props.onCellHovered);
            const resizeObserver = new ResizeObserver(() => {
                const boundingRect = this.gridOverlayEl.getBoundingClientRect();
                this.props.onGridResized({
                    x: boundingRect.left,
                    y: boundingRect.top,
                    height: this.gridOverlayEl.clientHeight,
                    width: this.gridOverlayEl.clientWidth,
                });
            });
            owl.onMounted(() => {
                resizeObserver.observe(this.gridOverlayEl);
            });
            owl.onWillUnmount(() => {
                resizeObserver.disconnect();
            });
            useTouchMove(this.gridOverlay, this.props.onGridMoved, () => {
                const { scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();
                return scrollY > 0;
            });
        }
        get gridOverlayEl() {
            if (!this.gridOverlay.el) {
                throw new Error("GridOverlay el is not defined.");
            }
            return this.gridOverlay.el;
        }
        get style() {
            return this.props.gridOverlayDimensions;
        }
        get isPaintingFormat() {
            return this.env.model.getters.isPaintingFormat();
        }
        onMouseDown(ev) {
            if (ev.button > 0) {
                // not main button, probably a context menu
                return;
            }
            const [col, row] = this.getCartesianCoordinates(ev);
            this.props.onCellClicked(col, row, {
                expandZone: ev.shiftKey,
                addZone: isCtrlKey(ev),
            });
        }
        onDoubleClick(ev) {
            const [col, row] = this.getCartesianCoordinates(ev);
            this.props.onCellDoubleClicked(col, row);
        }
        onContextMenu(ev) {
            const [col, row] = this.getCartesianCoordinates(ev);
            this.props.onCellRightClicked(col, row, { x: ev.clientX, y: ev.clientY });
        }
        getCartesianCoordinates(ev) {
            const x = ev.clientX - this.gridOverlayRect.x;
            const y = ev.clientY - this.gridOverlayRect.y;
            const colIndex = this.env.model.getters.getColIndex(x);
            const rowIndex = this.env.model.getters.getRowIndex(y);
            return [colIndex, rowIndex];
        }
    }
    GridOverlay.props = {
        onCellHovered: { type: Function, optional: true },
        onCellDoubleClicked: { type: Function, optional: true },
        onCellClicked: { type: Function, optional: true },
        onCellRightClicked: { type: Function, optional: true },
        onGridResized: { type: Function, optional: true },
        onFigureDeleted: { type: Function, optional: true },
        onGridMoved: Function,
        gridOverlayDimensions: String,
    };

    class GridPopover extends owl.Component {
        static template = "o-spreadsheet-GridPopover";
        static components = { Popover };
        zIndex = ComponentsImportance.GridPopover;
        get cellPopover() {
            const popover = this.env.model.getters.getCellPopover(this.props.hoveredCell);
            if (!popover.isOpen) {
                return { isOpen: false };
            }
            const anchorRect = popover.anchorRect;
            return {
                ...popover,
                // transform from the "canvas coordinate system" to the "body coordinate system"
                anchorRect: {
                    ...anchorRect,
                    x: anchorRect.x + this.props.gridRect.x,
                    y: anchorRect.y + this.props.gridRect.y,
                },
            };
        }
    }
    GridPopover.props = {
        hoveredCell: Object,
        onClosePopover: Function,
        onMouseWheel: Function,
        gridRect: Object,
    };

    class AbstractResizer extends owl.Component {
        PADDING = 0;
        MAX_SIZE_MARGIN = 0;
        MIN_ELEMENT_SIZE = 0;
        lastSelectedElementIndex = null;
        state = owl.useState({
            resizerIsActive: false,
            isResizing: false,
            isMoving: false,
            isSelecting: false,
            waitingForMove: false,
            activeElement: 0,
            draggerLinePosition: 0,
            draggerShadowPosition: 0,
            draggerShadowThickness: 0,
            delta: 0,
            base: 0,
            position: "before",
        });
        _computeHandleDisplay(ev) {
            const position = this._getEvOffset(ev);
            const elementIndex = this._getElementIndex(position);
            if (elementIndex < 0) {
                return;
            }
            const dimensions = this._getDimensionsInViewport(elementIndex);
            if (position - dimensions.start < this.PADDING && elementIndex !== this._getViewportOffset()) {
                this.state.resizerIsActive = true;
                this.state.draggerLinePosition = dimensions.start;
                this.state.activeElement = this._getPreviousVisibleElement(elementIndex);
            }
            else if (dimensions.end - position < this.PADDING) {
                this.state.resizerIsActive = true;
                this.state.draggerLinePosition = dimensions.end;
                this.state.activeElement = elementIndex;
            }
            else {
                this.state.resizerIsActive = false;
            }
        }
        _computeGrabDisplay(ev) {
            const index = this._getElementIndex(this._getEvOffset(ev));
            const activeElements = this._getActiveElements();
            const selectedZoneStart = this._getSelectedZoneStart();
            const selectedZoneEnd = this._getSelectedZoneEnd();
            if (activeElements.has(selectedZoneStart)) {
                if (selectedZoneStart <= index && index <= selectedZoneEnd) {
                    this.state.waitingForMove = true;
                    return;
                }
            }
            this.state.waitingForMove = false;
        }
        onMouseMove(ev) {
            if (this.state.isResizing || this.state.isMoving || this.state.isSelecting) {
                return;
            }
            this._computeHandleDisplay(ev);
            this._computeGrabDisplay(ev);
        }
        onMouseLeave() {
            this.state.resizerIsActive = this.state.isResizing;
            this.state.waitingForMove = false;
        }
        onDblClick(ev) {
            this._fitElementSize(this.state.activeElement);
            this.state.isResizing = false;
            this._computeHandleDisplay(ev);
            this._computeGrabDisplay(ev);
        }
        onMouseDown(ev) {
            this.state.isResizing = true;
            this.state.delta = 0;
            const initialPosition = this._getClientPosition(ev);
            const styleValue = this.state.draggerLinePosition;
            const size = this._getElementSize(this.state.activeElement);
            const minSize = styleValue - size + this.MIN_ELEMENT_SIZE;
            const maxSize = this._getMaxSize();
            const onMouseUp = (ev) => {
                this.state.isResizing = false;
                if (this.state.delta !== 0) {
                    this._updateSize();
                }
            };
            const onMouseMove = (ev) => {
                this.state.delta = this._getClientPosition(ev) - initialPosition;
                this.state.draggerLinePosition = styleValue + this.state.delta;
                if (this.state.draggerLinePosition < minSize) {
                    this.state.draggerLinePosition = minSize;
                    this.state.delta = this.MIN_ELEMENT_SIZE - size;
                }
                if (this.state.draggerLinePosition > maxSize) {
                    this.state.draggerLinePosition = maxSize;
                    this.state.delta = maxSize - styleValue;
                }
            };
            startDnd(onMouseMove, onMouseUp);
        }
        select(ev) {
            if (ev.button > 0) {
                // not main button, probably a context menu
                return;
            }
            const index = this._getElementIndex(this._getEvOffset(ev));
            if (index < 0) {
                return;
            }
            if (this.state.waitingForMove === true) {
                if (!this.env.model.getters.isGridSelectionActive()) {
                    this._selectElement(index, false);
                }
                else {
                    // FIXME: Consider reintroducing this feature for all type of selection if we find
                    // a way to have the grid selection follow the other selections evolution
                    this.startMovement(ev);
                }
                return;
            }
            if (this.env.model.getters.getEditionMode() === "editing") {
                this.env.model.selection.getBackToDefault();
            }
            this.startSelection(ev, index);
        }
        startMovement(ev) {
            this.state.waitingForMove = false;
            this.state.isMoving = true;
            const startDimensions = this._getDimensionsInViewport(this._getSelectedZoneStart());
            const endDimensions = this._getDimensionsInViewport(this._getSelectedZoneEnd());
            const defaultPosition = startDimensions.start;
            this.state.draggerLinePosition = defaultPosition;
            this.state.base = this._getSelectedZoneStart();
            this.state.draggerShadowPosition = defaultPosition;
            this.state.draggerShadowThickness = endDimensions.end - startDimensions.start;
            const mouseMoveMovement = (col, row) => {
                let elementIndex = this._getType() === "COL" ? col : row;
                if (elementIndex >= 0) {
                    // define draggerLinePosition
                    const dimensions = this._getDimensionsInViewport(elementIndex);
                    if (elementIndex <= this._getSelectedZoneStart()) {
                        this.state.draggerLinePosition = dimensions.start;
                        this.state.draggerShadowPosition = dimensions.start;
                        this.state.base = elementIndex;
                        this.state.position = "before";
                    }
                    else if (this._getSelectedZoneEnd() < elementIndex) {
                        this.state.draggerLinePosition = dimensions.end;
                        this.state.draggerShadowPosition = dimensions.end - this.state.draggerShadowThickness;
                        this.state.base = elementIndex;
                        this.state.position = "after";
                    }
                    else {
                        this.state.draggerLinePosition = startDimensions.start;
                        this.state.draggerShadowPosition = startDimensions.start;
                        this.state.base = this._getSelectedZoneStart();
                    }
                }
            };
            const mouseUpMovement = () => {
                this.state.isMoving = false;
                if (this.state.base !== this._getSelectedZoneStart()) {
                    this._moveElements();
                }
                this._computeGrabDisplay(ev);
            };
            dragAndDropBeyondTheViewport(this.env, mouseMoveMovement, mouseUpMovement);
        }
        startSelection(ev, index) {
            this.state.isSelecting = true;
            if (ev.shiftKey) {
                this._increaseSelection(index);
            }
            else {
                this._selectElement(index, isCtrlKey(ev));
            }
            this.lastSelectedElementIndex = index;
            const mouseMoveSelect = (col, row) => {
                let newIndex = this._getType() === "COL" ? col : row;
                if (newIndex !== this.lastSelectedElementIndex && newIndex !== -1) {
                    this._increaseSelection(newIndex);
                    this.lastSelectedElementIndex = newIndex;
                }
            };
            const mouseUpSelect = () => {
                this.state.isSelecting = false;
                this.lastSelectedElementIndex = null;
                this._computeGrabDisplay(ev);
            };
            dragAndDropBeyondTheViewport(this.env, mouseMoveSelect, mouseUpSelect);
        }
        onMouseUp(ev) {
            this.lastSelectedElementIndex = null;
        }
        onContextMenu(ev) {
            ev.preventDefault();
            const index = this._getElementIndex(this._getEvOffset(ev));
            if (index < 0)
                return;
            if (!this._getActiveElements().has(index)) {
                this._selectElement(index, false);
            }
            const type = this._getType();
            this.props.onOpenContextMenu(type, ev.clientX, ev.clientY);
        }
    }
    css /* scss */ `
  .o-col-resizer {
    position: absolute;
    top: 0;
    left: ${HEADER_WIDTH}px;
    right: 0;
    height: ${HEADER_HEIGHT}px;
    &.o-dragging {
      cursor: grabbing;
    }
    &.o-grab {
      cursor: grab;
    }
    .dragging-col-line {
      top: ${HEADER_HEIGHT}px;
      position: absolute;
      width: 2px;
      height: 10000px;
      background-color: black;
    }
    .dragging-col-shadow {
      top: ${HEADER_HEIGHT}px;
      position: absolute;
      height: 10000px;
      background-color: black;
      opacity: 0.1;
    }
    .o-handle {
      position: absolute;
      height: ${HEADER_HEIGHT}px;
      width: 4px;
      cursor: e-resize;
      background-color: ${SELECTION_BORDER_COLOR};
    }
    .dragging-resizer {
      top: ${HEADER_HEIGHT}px;
      position: absolute;
      margin-left: 2px;
      width: 1px;
      height: 10000px;
      background-color: ${SELECTION_BORDER_COLOR};
    }
    .o-unhide {
      width: ${UNHIDE_ICON_EDGE_LENGTH}px;
      height: ${UNHIDE_ICON_EDGE_LENGTH}px;
      position: absolute;
      overflow: hidden;
      border-radius: 2px;
      top: calc(${HEADER_HEIGHT}px / 2 - ${UNHIDE_ICON_EDGE_LENGTH}px / 2);
    }
    .o-unhide:hover {
      z-index: ${ComponentsImportance.Grid + 1};
      background-color: lightgrey;
    }
    .o-unhide > svg {
      position: relative;
      top: calc(${UNHIDE_ICON_EDGE_LENGTH}px / 2 - ${ICON_EDGE_LENGTH}px / 2);
    }
  }
`;
    AbstractResizer.props = {
        onOpenContextMenu: Function,
    };
    class ColResizer extends AbstractResizer {
        static template = "o-spreadsheet-ColResizer";
        colResizerRef;
        setup() {
            super.setup();
            this.colResizerRef = owl.useRef("colResizer");
            this.PADDING = 15;
            this.MAX_SIZE_MARGIN = 90;
            this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
        }
        _getEvOffset(ev) {
            return ev.offsetX;
        }
        _getViewportOffset() {
            return this.env.model.getters.getActiveMainViewport().left;
        }
        _getClientPosition(ev) {
            return ev.clientX;
        }
        _getElementIndex(position) {
            return this.env.model.getters.getColIndex(position);
        }
        _getSelectedZoneStart() {
            return this.env.model.getters.getSelectedZone().left;
        }
        _getSelectedZoneEnd() {
            return this.env.model.getters.getSelectedZone().right;
        }
        _getEdgeScroll(position) {
            return this.env.model.getters.getEdgeScrollCol(position, position, position);
        }
        _getDimensionsInViewport(index) {
            return this.env.model.getters.getColDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
        }
        _getElementSize(index) {
            return this.env.model.getters.getColSize(this.env.model.getters.getActiveSheetId(), index);
        }
        _getMaxSize() {
            return this.colResizerRef.el.clientWidth;
        }
        _updateSize() {
            const index = this.state.activeElement;
            const size = this.state.delta + this._getElementSize(index);
            const cols = this.env.model.getters.getActiveCols();
            this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
                dimension: "COL",
                sheetId: this.env.model.getters.getActiveSheetId(),
                elements: cols.has(index) ? [...cols] : [index],
                size,
            });
        }
        _moveElements() {
            const elements = [];
            const start = this._getSelectedZoneStart();
            const end = this._getSelectedZoneEnd();
            for (let colIndex = start; colIndex <= end; colIndex++) {
                elements.push(colIndex);
            }
            const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                dimension: "COL",
                base: this.state.base,
                elements,
                position: this.state.position,
            });
            if (!result.isSuccessful && result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
                this.env.raiseError(MergeErrorMessage);
            }
        }
        _selectElement(index, addDistinctHeader) {
            this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
        }
        _increaseSelection(index) {
            this.env.model.selection.selectColumn(index, "updateAnchor");
        }
        _fitElementSize(index) {
            const cols = this.env.model.getters.getActiveCols();
            this.env.model.dispatch("AUTORESIZE_COLUMNS", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                cols: cols.has(index) ? [...cols] : [index],
            });
        }
        _getType() {
            return "COL";
        }
        _getActiveElements() {
            return this.env.model.getters.getActiveCols();
        }
        _getPreviousVisibleElement(index) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            let row;
            for (row = index - 1; row >= 0; row--) {
                if (!this.env.model.getters.isColHidden(sheetId, row)) {
                    break;
                }
            }
            return row;
        }
        unhide(hiddenElements) {
            this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                elements: hiddenElements,
                dimension: "COL",
            });
        }
        unhideStyleValue(hiddenIndex) {
            return this._getDimensionsInViewport(hiddenIndex).start;
        }
    }
    css /* scss */ `
  .o-row-resizer {
    position: absolute;
    top: ${HEADER_HEIGHT}px;
    left: 0;
    right: 0;
    width: ${HEADER_WIDTH}px;
    height: 100%;
    &.o-dragging {
      cursor: grabbing;
    }
    &.o-grab {
      cursor: grab;
    }
    .dragging-row-line {
      left: ${HEADER_WIDTH}px;
      position: absolute;
      width: 10000px;
      height: 2px;
      background-color: black;
    }
    .dragging-row-shadow {
      left: ${HEADER_WIDTH}px;
      position: absolute;
      width: 10000px;
      background-color: black;
      opacity: 0.1;
    }
    .o-handle {
      position: absolute;
      height: 4px;
      width: ${HEADER_WIDTH}px;
      cursor: n-resize;
      background-color: ${SELECTION_BORDER_COLOR};
    }
    .dragging-resizer {
      left: ${HEADER_WIDTH}px;
      position: absolute;
      margin-top: 2px;
      width: 10000px;
      height: 1px;
      background-color: ${SELECTION_BORDER_COLOR};
    }
    .o-unhide {
      width: ${UNHIDE_ICON_EDGE_LENGTH}px;
      height: ${UNHIDE_ICON_EDGE_LENGTH}px;
      position: absolute;
      overflow: hidden;
      border-radius: 2px;
      left: calc(${HEADER_WIDTH}px - ${UNHIDE_ICON_EDGE_LENGTH}px - 2px);
    }
    .o-unhide > svg {
      position: relative;
      left: calc(${UNHIDE_ICON_EDGE_LENGTH}px / 2 - ${ICON_EDGE_LENGTH}px / 2);
      top: calc(${UNHIDE_ICON_EDGE_LENGTH}px / 2 - ${ICON_EDGE_LENGTH}px / 2);
    }
    .o-unhide:hover {
      z-index: ${ComponentsImportance.Grid + 1};
      background-color: lightgrey;
    }
  }
`;
    ColResizer.props = {
        onOpenContextMenu: Function,
    };
    class RowResizer extends AbstractResizer {
        static template = "o-spreadsheet-RowResizer";
        setup() {
            super.setup();
            this.rowResizerRef = owl.useRef("rowResizer");
            this.PADDING = 5;
            this.MAX_SIZE_MARGIN = 60;
            this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
        }
        rowResizerRef;
        _getEvOffset(ev) {
            return ev.offsetY;
        }
        _getViewportOffset() {
            return this.env.model.getters.getActiveMainViewport().top;
        }
        _getClientPosition(ev) {
            return ev.clientY;
        }
        _getElementIndex(position) {
            return this.env.model.getters.getRowIndex(position);
        }
        _getSelectedZoneStart() {
            return this.env.model.getters.getSelectedZone().top;
        }
        _getSelectedZoneEnd() {
            return this.env.model.getters.getSelectedZone().bottom;
        }
        _getEdgeScroll(position) {
            return this.env.model.getters.getEdgeScrollRow(position, position, position);
        }
        _getDimensionsInViewport(index) {
            return this.env.model.getters.getRowDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
        }
        _getElementSize(index) {
            return this.env.model.getters.getRowSize(this.env.model.getters.getActiveSheetId(), index);
        }
        _getMaxSize() {
            return this.rowResizerRef.el.clientHeight;
        }
        _updateSize() {
            const index = this.state.activeElement;
            const size = this.state.delta + this._getElementSize(index);
            const rows = this.env.model.getters.getActiveRows();
            this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
                dimension: "ROW",
                sheetId: this.env.model.getters.getActiveSheetId(),
                elements: rows.has(index) ? [...rows] : [index],
                size,
            });
        }
        _moveElements() {
            const elements = [];
            const start = this._getSelectedZoneStart();
            const end = this._getSelectedZoneEnd();
            for (let rowIndex = start; rowIndex <= end; rowIndex++) {
                elements.push(rowIndex);
            }
            const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                dimension: "ROW",
                base: this.state.base,
                elements,
                position: this.state.position,
            });
            if (!result.isSuccessful && result.reasons.includes("WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */)) {
                this.env.raiseError(MergeErrorMessage);
            }
        }
        _selectElement(index, addDistinctHeader) {
            this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
        }
        _increaseSelection(index) {
            this.env.model.selection.selectRow(index, "updateAnchor");
        }
        _fitElementSize(index) {
            const rows = this.env.model.getters.getActiveRows();
            this.env.model.dispatch("AUTORESIZE_ROWS", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                rows: rows.has(index) ? [...rows] : [index],
            });
        }
        _getType() {
            return "ROW";
        }
        _getActiveElements() {
            return this.env.model.getters.getActiveRows();
        }
        _getPreviousVisibleElement(index) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            let row;
            for (row = index - 1; row >= 0; row--) {
                if (!this.env.model.getters.isRowHidden(sheetId, row)) {
                    break;
                }
            }
            return row;
        }
        unhide(hiddenElements) {
            this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                dimension: "ROW",
                elements: hiddenElements,
            });
        }
        unhideStyleValue(hiddenIndex) {
            return this._getDimensionsInViewport(hiddenIndex).start;
        }
    }
    css /* scss */ `
  .o-overlay {
    .all {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      width: ${HEADER_WIDTH}px;
      height: ${HEADER_HEIGHT}px;
    }
  }
`;
    RowResizer.props = {
        onOpenContextMenu: Function,
    };
    class HeadersOverlay extends owl.Component {
        static template = "o-spreadsheet-HeadersOverlay";
        static components = { ColResizer, RowResizer };
        selectAll() {
            this.env.model.selection.selectAll();
        }
    }
    HeadersOverlay.props = {
        onOpenContextMenu: Function,
    };

    function useGridDrawing(refName, model, canvasSize) {
        const canvasRef = owl.useRef(refName);
        owl.useEffect(drawGrid);
        function drawGrid() {
            const canvas = canvasRef.el;
            const dpr = window.devicePixelRatio || 1;
            const ctx = canvas.getContext("2d", { alpha: false });
            const thinLineWidth = 0.4 * dpr;
            const renderingContext = {
                ctx,
                dpr,
                thinLineWidth,
            };
            const { width, height } = canvasSize();
            canvas.style.width = `${width}px`;
            canvas.style.height = `${height}px`;
            canvas.width = width * dpr;
            canvas.height = height * dpr;
            canvas.setAttribute("style", `width:${width}px;height:${height}px;`);
            // Imagine each pixel as a large square. The whole-number coordinates (0, 1, 2…)
            // are the edges of the squares. If you draw a one-unit-wide line between whole-number
            // coordinates, it will overlap opposite sides of the pixel square, and the resulting
            // line will be drawn two pixels wide. To draw a line that is only one pixel wide,
            // you need to shift the coordinates by 0.5 perpendicular to the line's direction.
            // http://diveintohtml5.info/canvas.html#pixel-madness
            ctx.translate(-CANVAS_SHIFT, -CANVAS_SHIFT);
            ctx.scale(dpr, dpr);
            model.drawGrid(renderingContext);
        }
    }

    function useWheelHandler(handler) {
        function normalize(val, deltaMode) {
            return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);
        }
        const onMouseWheel = (ev) => {
            const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode);
            const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode);
            handler(deltaX, deltaY);
        };
        return onMouseWheel;
    }

    css /* scss */ `
  .o-border {
    position: absolute;
    &:hover {
      cursor: grab;
    }
  }
  .o-moving {
    cursor: grabbing;
  }
`;
    class Border extends owl.Component {
        static template = "o-spreadsheet-Border";
        get style() {
            const isTop = ["n", "w", "e"].includes(this.props.orientation);
            const isLeft = ["n", "w", "s"].includes(this.props.orientation);
            const isHorizontal = ["n", "s"].includes(this.props.orientation);
            const isVertical = ["w", "e"].includes(this.props.orientation);
            const z = this.props.zone;
            const margin = 2;
            const rect = this.env.model.getters.getVisibleRect(z);
            const left = rect.x;
            const right = rect.x + rect.width - 2 * margin;
            const top = rect.y;
            const bottom = rect.y + rect.height - 2 * margin;
            const lineWidth = 4;
            const leftValue = isLeft ? left : right;
            const topValue = isTop ? top : bottom;
            const widthValue = isHorizontal ? right - left : lineWidth;
            const heightValue = isVertical ? bottom - top : lineWidth;
            return cssPropertiesToCss({
                left: `${leftValue}px`,
                top: `${topValue}px`,
                width: `${widthValue}px`,
                height: `${heightValue}px`,
            });
        }
        onMouseDown(ev) {
            this.props.onMoveHighlight(ev.clientX, ev.clientY);
        }
    }
    Border.props = {
        zone: Object,
        orientation: String,
        isMoving: Boolean,
        onMoveHighlight: Function,
    };

    css /* scss */ `
  .o-corner {
    position: absolute;
    height: 6px;
    width: 6px;
    border: 1px solid white;
  }
  .o-corner-nw,
  .o-corner-se {
    &:hover {
      cursor: nwse-resize;
    }
  }
  .o-corner-ne,
  .o-corner-sw {
    &:hover {
      cursor: nesw-resize;
    }
  }
  .o-resizing {
    cursor: grabbing;
  }
`;
    class Corner extends owl.Component {
        static template = "o-spreadsheet-Corner";
        isTop = this.props.orientation[0] === "n";
        isLeft = this.props.orientation[1] === "w";
        get style() {
            const z = this.props.zone;
            const col = this.isLeft ? z.left : z.right;
            const row = this.isTop ? z.top : z.bottom;
            const rect = this.env.model.getters.getVisibleRect({
                left: col,
                right: col,
                top: row,
                bottom: row,
            });
            // Don't show if not visible in the viewport
            if (rect.width * rect.height === 0) {
                return `display:none`;
            }
            const leftValue = this.isLeft ? rect.x : rect.x + rect.width;
            const topValue = this.isTop ? rect.y : rect.y + rect.height;
            return cssPropertiesToCss({
                left: `${leftValue - AUTOFILL_EDGE_LENGTH / 2}px`,
                top: `${topValue - AUTOFILL_EDGE_LENGTH / 2}px`,
                "background-color": this.props.color,
            });
        }
        onMouseDown(ev) {
            this.props.onResizeHighlight(this.isLeft, this.isTop);
        }
    }
    Corner.props = {
        zone: Object,
        color: String,
        orientation: String,
        isResizing: Boolean,
        onResizeHighlight: Function,
    };

    css /*SCSS*/ `
  .o-highlight {
    z-index: ${ComponentsImportance.Highlight};
  }
`;
    class Highlight extends owl.Component {
        static template = "o-spreadsheet-Highlight";
        static components = {
            Corner,
            Border,
        };
        highlightState = owl.useState({
            shiftingMode: "none",
        });
        onResizeHighlight(isLeft, isTop) {
            const activeSheetId = this.env.model.getters.getActiveSheetId();
            this.highlightState.shiftingMode = "isResizing";
            const z = this.props.zone;
            const pivotCol = isLeft ? z.right : z.left;
            const pivotRow = isTop ? z.bottom : z.top;
            let lastCol = isLeft ? z.left : z.right;
            let lastRow = isTop ? z.top : z.bottom;
            let currentZone = z;
            this.env.model.dispatch("START_CHANGE_HIGHLIGHT", { zone: currentZone });
            const mouseMove = (col, row) => {
                if (lastCol !== col || lastRow !== row) {
                    lastCol = clip(col === -1 ? lastCol : col, 0, this.env.model.getters.getNumberCols(activeSheetId) - 1);
                    lastRow = clip(row === -1 ? lastRow : row, 0, this.env.model.getters.getNumberRows(activeSheetId) - 1);
                    let newZone = {
                        left: Math.min(pivotCol, lastCol),
                        top: Math.min(pivotRow, lastRow),
                        right: Math.max(pivotCol, lastCol),
                        bottom: Math.max(pivotRow, lastRow),
                    };
                    if (!isEqual(newZone, currentZone)) {
                        this.env.model.selection.selectZone({
                            cell: { col: newZone.left, row: newZone.top },
                            zone: newZone,
                        }, { unbounded: true });
                        currentZone = newZone;
                    }
                }
            };
            const mouseUp = () => {
                this.highlightState.shiftingMode = "none";
            };
            dragAndDropBeyondTheViewport(this.env, mouseMove, mouseUp);
        }
        onMoveHighlight(clientX, clientY) {
            this.highlightState.shiftingMode = "isMoving";
            const z = this.props.zone;
            const position = gridOverlayPosition();
            const activeSheetId = this.env.model.getters.getActiveSheetId();
            const initCol = this.env.model.getters.getColIndex(clientX - position.left);
            const initRow = this.env.model.getters.getRowIndex(clientY - position.top);
            const deltaColMin = -z.left;
            const deltaColMax = this.env.model.getters.getNumberCols(activeSheetId) - z.right - 1;
            const deltaRowMin = -z.top;
            const deltaRowMax = this.env.model.getters.getNumberRows(activeSheetId) - z.bottom - 1;
            let currentZone = z;
            this.env.model.dispatch("START_CHANGE_HIGHLIGHT", { zone: currentZone });
            let lastCol = initCol;
            let lastRow = initRow;
            const mouseMove = (col, row) => {
                if (lastCol !== col || lastRow !== row) {
                    lastCol = col === -1 ? lastCol : col;
                    lastRow = row === -1 ? lastRow : row;
                    const deltaCol = clip(lastCol - initCol, deltaColMin, deltaColMax);
                    const deltaRow = clip(lastRow - initRow, deltaRowMin, deltaRowMax);
                    let newZone = {
                        left: z.left + deltaCol,
                        top: z.top + deltaRow,
                        right: z.right + deltaCol,
                        bottom: z.bottom + deltaRow,
                    };
                    if (!isEqual(newZone, currentZone)) {
                        this.env.model.selection.selectZone({
                            cell: { col: newZone.left, row: newZone.top },
                            zone: newZone,
                        }, { unbounded: true });
                        currentZone = newZone;
                    }
                }
            };
            const mouseUp = () => {
                this.highlightState.shiftingMode = "none";
            };
            dragAndDropBeyondTheViewport(this.env, mouseMove, mouseUp);
        }
    }
    Highlight.props = {
        zone: Object,
        color: String,
    };

    let ScrollBar$1 = class ScrollBar {
        direction;
        el;
        constructor(el, direction) {
            this.el = el;
            this.direction = direction;
        }
        get scroll() {
            return this.direction === "horizontal" ? this.el.scrollLeft : this.el.scrollTop;
        }
        set scroll(value) {
            if (this.direction === "horizontal") {
                this.el.scrollLeft = value;
            }
            else {
                this.el.scrollTop = value;
            }
        }
    };

    css /* scss */ `
  .o-scrollbar {
    position: absolute;
    overflow: auto;
    z-index: ${ComponentsImportance.ScrollBar};
    background-color: ${BACKGROUND_GRAY_COLOR};

    &.corner {
      right: 0px;
      bottom: 0px;
      height: ${SCROLLBAR_WIDTH}px;
      width: ${SCROLLBAR_WIDTH}px;
      border-top: 1px solid #e2e3e3;
      border-left: 1px solid #e2e3e3;
    }
  }
`;
    class ScrollBar extends owl.Component {
        static template = owl.xml /*xml*/ `
    <div
        t-attf-class="o-scrollbar {{props.direction}}"
        t-on-scroll="onScroll"
        t-ref="scrollbar"
        t-att-style="positionCss">
      <div t-att-style="sizeCss"/>
    </div>
  `;
        static defaultProps = {
            width: 1,
            height: 1,
        };
        scrollbarRef;
        scrollbar;
        setup() {
            this.scrollbarRef = owl.useRef("scrollbar");
            this.scrollbar = new ScrollBar$1(this.scrollbarRef.el, this.props.direction);
            owl.onMounted(() => {
                this.scrollbar.el = this.scrollbarRef.el;
            });
            // TODO improve useEffect dependencies typing in owl
            owl.useEffect(() => {
                if (this.scrollbar.scroll !== this.props.offset) {
                    this.scrollbar.scroll = this.props.offset;
                }
            }, () => [this.scrollbar.scroll, this.props.offset]);
        }
        get sizeCss() {
            return cssPropertiesToCss({
                width: `${this.props.width}px`,
                height: `${this.props.height}px`,
            });
        }
        get positionCss() {
            return cssPropertiesToCss(this.props.position);
        }
        onScroll(ev) {
            if (this.props.offset !== this.scrollbar.scroll) {
                this.props.onScroll(this.scrollbar.scroll);
            }
        }
    }
    ScrollBar.props = {
        width: { type: Number, optional: true },
        height: { type: Number, optional: true },
        direction: String,
        position: Object,
        offset: Number,
        onScroll: Function,
    };

    class HorizontalScrollBar extends owl.Component {
        static components = { ScrollBar };
        static template = owl.xml /*xml*/ `
      <ScrollBar
        t-if="isDisplayed"
        width="width"
        position="position"
        offset="offset"
        direction="'horizontal'"
        onScroll.bind="onScroll"
      />`;
        static defaultProps = {
            leftOffset: 0,
        };
        get offset() {
            return this.env.model.getters.getActiveSheetDOMScrollInfo().scrollX;
        }
        get width() {
            return this.env.model.getters.getMainViewportRect().width;
        }
        get isDisplayed() {
            const { xRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());
            return xRatio < 1;
        }
        get position() {
            const { x } = this.env.model.getters.getMainViewportRect();
            return {
                left: `${this.props.leftOffset + x}px`,
                bottom: "0px",
                height: `${SCROLLBAR_WIDTH}px`,
                right: `0px`,
            };
        }
        onScroll(offset) {
            const { scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();
            this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
                offsetX: offset,
                offsetY: scrollY, // offsetY is the same
            });
        }
    }
    HorizontalScrollBar.props = {
        leftOffset: { type: Number, optional: true },
    };

    class VerticalScrollBar extends owl.Component {
        static components = { ScrollBar };
        static template = owl.xml /*xml*/ `
    <ScrollBar
      t-if="isDisplayed"
      height="height"
      position="position"
      offset="offset"
      direction="'vertical'"
      onScroll.bind="onScroll"
    />`;
        static defaultProps = {
            topOffset: 0,
        };
        get offset() {
            return this.env.model.getters.getActiveSheetDOMScrollInfo().scrollY;
        }
        get height() {
            return this.env.model.getters.getMainViewportRect().height;
        }
        get isDisplayed() {
            const { yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());
            return yRatio < 1;
        }
        get position() {
            const { y } = this.env.model.getters.getMainViewportRect();
            return {
                top: `${this.props.topOffset + y}px`,
                right: "0px",
                width: `${SCROLLBAR_WIDTH}px`,
                bottom: `0px`,
            };
        }
        onScroll(offset) {
            const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();
            this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
                offsetX: scrollX,
                offsetY: offset,
            });
        }
    }
    VerticalScrollBar.props = {
        topOffset: { type: Number, optional: true },
    };

    const registries$1 = {
        ROW: rowMenuRegistry,
        COL: colMenuRegistry,
        CELL: cellMenuRegistry,
        GROUP_HEADERS: groupHeadersMenuRegistry,
        UNGROUP_HEADERS: unGroupHeadersMenuRegistry,
    };
    const MODIFIER_KEYS = ["Shift", "Control", "Alt", "Meta"];
    // -----------------------------------------------------------------------------
    // JS
    // -----------------------------------------------------------------------------
    class Grid extends owl.Component {
        static template = "o-spreadsheet-Grid";
        static components = {
            GridComposer,
            GridOverlay,
            GridPopover,
            HeadersOverlay,
            Menu,
            Autofill,
            ClientTag,
            Highlight,
            Popover,
            VerticalScrollBar,
            HorizontalScrollBar,
            FilterIconsOverlay,
        };
        HEADER_HEIGHT = HEADER_HEIGHT;
        HEADER_WIDTH = HEADER_WIDTH;
        menuState;
        gridRef;
        onMouseWheel;
        canvasPosition;
        hoveredCell;
        setup() {
            this.menuState = owl.useState({
                isOpen: false,
                position: null,
                menuItems: [],
            });
            this.gridRef = owl.useRef("grid");
            this.canvasPosition = useAbsoluteBoundingRect(this.gridRef);
            this.hoveredCell = owl.useState({ col: undefined, row: undefined });
            owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
            owl.useExternalListener(document.body, "cut", this.copy.bind(this, true));
            owl.useExternalListener(document.body, "copy", this.copy.bind(this, false));
            owl.useExternalListener(document.body, "paste", this.paste);
            owl.onMounted(() => this.focusDefaultElement());
            this.props.exposeFocus(() => this.focusDefaultElement());
            useGridDrawing("canvas", this.env.model, () => this.env.model.getters.getSheetViewDimensionWithHeaders());
            owl.useEffect(() => this.focusDefaultElement(), () => [this.env.model.getters.getActiveSheetId()]);
            this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {
                this.moveCanvas(deltaX, deltaY);
                this.hoveredCell.col = undefined;
                this.hoveredCell.row = undefined;
            });
        }
        onCellHovered({ col, row }) {
            this.hoveredCell.col = col;
            this.hoveredCell.row = row;
        }
        get gridOverlayDimensions() {
            return cssPropertiesToCss({
                top: `${HEADER_HEIGHT}px`,
                left: `${HEADER_WIDTH}px`,
                height: `calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px)`,
                width: `calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}px)`,
            });
        }
        onClosePopover() {
            if (this.env.model.getters.hasOpenedPopover()) {
                this.closeOpenedPopover();
            }
            this.focusDefaultElement();
        }
        // this map will handle most of the actions that should happen on key down. The arrow keys are managed in the key
        // down itself
        keyDownMapping = {
            ENTER: () => {
                const cell = this.env.model.getters.getActiveCell();
                cell.type === CellValueType.empty
                    ? this.props.onGridComposerCellFocused()
                    : this.props.onComposerContentFocused();
            },
            TAB: () => this.env.model.selection.moveAnchorCell("right", 1),
            "SHIFT+TAB": () => this.env.model.selection.moveAnchorCell("left", 1),
            F2: () => {
                const cell = this.env.model.getters.getActiveCell();
                cell.type === CellValueType.empty
                    ? this.props.onGridComposerCellFocused()
                    : this.props.onComposerContentFocused();
            },
            DELETE: () => {
                this.env.model.dispatch("DELETE_CONTENT", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    target: this.env.model.getters.getSelectedZones(),
                });
            },
            BACKSPACE: () => {
                this.env.model.dispatch("DELETE_CONTENT", {
                    sheetId: this.env.model.getters.getActiveSheetId(),
                    target: this.env.model.getters.getSelectedZones(),
                });
            },
            ESCAPE: () => {
                /** TODO: Clean once we introduce proper focus on sub components. Grid should not have to handle all this logic */
                if (this.env.model.getters.hasOpenedPopover()) {
                    this.closeOpenedPopover();
                }
                else if (this.menuState.isOpen) {
                    this.closeMenu();
                }
                else if (this.env.model.getters.isPaintingFormat()) {
                    this.env.model.dispatch("CANCEL_PAINT_FORMAT");
                }
                else {
                    this.env.model.dispatch("CLEAN_CLIPBOARD_HIGHLIGHT");
                }
            },
            "CTRL+A": () => this.env.model.selection.loopSelection(),
            "CTRL+Z": () => this.env.model.dispatch("REQUEST_UNDO"),
            "CTRL+Y": () => this.env.model.dispatch("REQUEST_REDO"),
            F4: () => this.env.model.dispatch("REQUEST_REDO"),
            "CTRL+B": () => this.env.model.dispatch("SET_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                style: { bold: !this.env.model.getters.getCurrentStyle().bold },
            }),
            "CTRL+I": () => this.env.model.dispatch("SET_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                style: { italic: !this.env.model.getters.getCurrentStyle().italic },
            }),
            "CTRL+U": () => this.env.model.dispatch("SET_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                style: { underline: !this.env.model.getters.getCurrentStyle().underline },
            }),
            "CTRL+O": () => CREATE_IMAGE(this.env),
            "ALT+=": () => {
                const sheetId = this.env.model.getters.getActiveSheetId();
                const mainSelectedZone = this.env.model.getters.getSelectedZone();
                const { anchor } = this.env.model.getters.getSelection();
                const sums = this.env.model.getters.getAutomaticSums(sheetId, mainSelectedZone, anchor.cell);
                if (this.env.model.getters.isSingleCellOrMerge(sheetId, mainSelectedZone) ||
                    (this.env.model.getters.isEmpty(sheetId, mainSelectedZone) && sums.length <= 1)) {
                    const zone = sums[0]?.zone;
                    const zoneXc = zone ? this.env.model.getters.zoneToXC(sheetId, sums[0].zone) : "";
                    const formula = `=SUM(${zoneXc})`;
                    this.props.onGridComposerCellFocused(formula, { start: 5, end: 5 + zoneXc.length });
                }
                else {
                    this.env.model.dispatch("SUM_SELECTION");
                }
            },
            "ALT+ENTER": () => {
                const cell = this.env.model.getters.getActiveCell();
                if (cell.link) {
                    openLink(cell.link, this.env);
                }
            },
            "CTRL+HOME": () => {
                const sheetId = this.env.model.getters.getActiveSheetId();
                const { col, row } = this.env.model.getters.getNextVisibleCellPosition({
                    sheetId,
                    col: 0,
                    row: 0,
                });
                this.env.model.selection.selectCell(col, row);
            },
            "CTRL+END": () => {
                const sheetId = this.env.model.getters.getActiveSheetId();
                const col = this.env.model.getters.findVisibleHeader(sheetId, "COL", this.env.model.getters.getNumberCols(sheetId) - 1, 0);
                const row = this.env.model.getters.findVisibleHeader(sheetId, "ROW", this.env.model.getters.getNumberRows(sheetId) - 1, 0);
                this.env.model.selection.selectCell(col, row);
            },
            "SHIFT+ ": () => {
                const sheetId = this.env.model.getters.getActiveSheetId();
                const newZone = {
                    ...this.env.model.getters.getSelectedZone(),
                    left: 0,
                    right: this.env.model.getters.getNumberCols(sheetId) - 1,
                };
                const position = this.env.model.getters.getActivePosition();
                this.env.model.selection.selectZone({ cell: position, zone: newZone });
            },
            "CTRL+ ": () => {
                const sheetId = this.env.model.getters.getActiveSheetId();
                const newZone = {
                    ...this.env.model.getters.getSelectedZone(),
                    top: 0,
                    bottom: this.env.model.getters.getNumberRows(sheetId) - 1,
                };
                const position = this.env.model.getters.getActivePosition();
                this.env.model.selection.selectZone({ cell: position, zone: newZone });
            },
            "CTRL+D": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ABOVE"),
            "CTRL+R": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ON_LEFT"),
            "CTRL+SHIFT+E": () => this.setHorizontalAlign("center"),
            "CTRL+SHIFT+L": () => this.setHorizontalAlign("left"),
            "CTRL+SHIFT+R": () => this.setHorizontalAlign("right"),
            "CTRL+SHIFT+V": () => PASTE_VALUE_ACTION(this.env),
            "CTRL+SHIFT+<": () => this.clearFormatting(),
            "CTRL+<": () => this.clearFormatting(),
            "CTRL+SHIFT+ ": () => {
                this.env.model.selection.selectAll();
            },
            "CTRL+ALT+=": () => {
                const activeCols = this.env.model.getters.getActiveCols();
                const activeRows = this.env.model.getters.getActiveRows();
                const isSingleSelection = this.env.model.getters.getSelectedZones().length === 1;
                const areFullCols = activeCols.size > 0 && isSingleSelection;
                const areFullRows = activeRows.size > 0 && isSingleSelection;
                if (areFullCols && !areFullRows) {
                    INSERT_COLUMNS_BEFORE_ACTION(this.env);
                }
                else if (areFullRows && !areFullCols) {
                    INSERT_ROWS_BEFORE_ACTION(this.env);
                }
            },
            "CTRL+ALT+-": () => {
                const columns = [...this.env.model.getters.getActiveCols()];
                const rows = [...this.env.model.getters.getActiveRows()];
                if (columns.length > 0 && rows.length === 0) {
                    this.env.model.dispatch("REMOVE_COLUMNS_ROWS", {
                        sheetId: this.env.model.getters.getActiveSheetId(),
                        dimension: "COL",
                        elements: columns,
                    });
                }
                else if (rows.length > 0 && columns.length === 0) {
                    this.env.model.dispatch("REMOVE_COLUMNS_ROWS", {
                        sheetId: this.env.model.getters.getActiveSheetId(),
                        dimension: "ROW",
                        elements: rows,
                    });
                }
            },
            "SHIFT+PAGEDOWN": () => {
                this.env.model.dispatch("ACTIVATE_NEXT_SHEET");
            },
            "SHIFT+PAGEUP": () => {
                this.env.model.dispatch("ACTIVATE_PREVIOUS_SHEET");
            },
            PAGEDOWN: () => this.env.model.dispatch("SHIFT_VIEWPORT_DOWN"),
            PAGEUP: () => this.env.model.dispatch("SHIFT_VIEWPORT_UP"),
            "CTRL+K": () => INSERT_LINK(this.env),
            "ALT+SHIFT+ARROWRIGHT": () => this.processHeaderGroupingKey("right"),
            "ALT+SHIFT+ARROWLEFT": () => this.processHeaderGroupingKey("left"),
            "ALT+SHIFT+ARROWUP": () => this.processHeaderGroupingKey("up"),
            "ALT+SHIFT+ARROWDOWN": () => this.processHeaderGroupingKey("down"),
        };
        focusDefaultElement() {
            if (!this.env.model.getters.getSelectedFigureId() &&
                this.env.model.getters.getEditionMode() === "inactive") {
                this.env.focusableElement.focus();
            }
        }
        get gridEl() {
            if (!this.gridRef.el) {
                throw new Error("Grid el is not defined.");
            }
            return this.gridRef.el;
        }
        getAutofillPosition() {
            const zone = this.env.model.getters.getSelectedZone();
            const rect = this.env.model.getters.getVisibleRect(zone);
            return {
                left: rect.x + rect.width - AUTOFILL_EDGE_LENGTH / 2,
                top: rect.y + rect.height - AUTOFILL_EDGE_LENGTH / 2,
            };
        }
        get isAutofillVisible() {
            const zone = this.env.model.getters.getSelectedZone();
            const rect = this.env.model.getters.getVisibleRect({
                left: zone.right,
                right: zone.right,
                top: zone.bottom,
                bottom: zone.bottom,
            });
            return !(rect.width === 0 || rect.height === 0);
        }
        onGridResized({ height, width }) {
            this.env.model.dispatch("RESIZE_SHEETVIEW", {
                width: width,
                height: height,
                gridOffsetX: HEADER_WIDTH,
                gridOffsetY: HEADER_HEIGHT,
            });
        }
        moveCanvas(deltaX, deltaY) {
            const { scrollX, scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();
            this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
                offsetX: scrollX + deltaX,
                offsetY: scrollY + deltaY,
            });
        }
        getClientPositionKey(client) {
            return `${client.id}-${client.position?.sheetId}-${client.position?.col}-${client.position?.row}`;
        }
        isCellHovered(col, row) {
            return this.hoveredCell.col === col && this.hoveredCell.row === row;
        }
        getGridRect() {
            return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() };
        }
        // ---------------------------------------------------------------------------
        // Zone selection with mouse
        // ---------------------------------------------------------------------------
        onCellClicked(col, row, { addZone, expandZone }) {
            if (this.env.model.getters.hasOpenedPopover()) {
                this.closeOpenedPopover();
            }
            if (this.env.model.getters.getEditionMode() === "editing") {
                interactiveStopEdition(this.env);
            }
            if (expandZone) {
                this.env.model.selection.setAnchorCorner(col, row);
            }
            else if (addZone) {
                this.env.model.selection.addCellToSelection(col, row);
            }
            else {
                this.env.model.selection.selectCell(col, row);
            }
            let prevCol = col;
            let prevRow = row;
            const onMouseMove = (col, row, ev) => {
                // When selecting cells during the edition, we don't want to avoid the default
                // browser behaviour that will select the text inside the composer
                // (see related commit msg for more information)
                ev.preventDefault();
                if ((col !== prevCol && col != -1) || (row !== prevRow && row != -1)) {
                    prevCol = col === -1 ? prevCol : col;
                    prevRow = row === -1 ? prevRow : row;
                    this.env.model.selection.setAnchorCorner(prevCol, prevRow);
                }
            };
            const onMouseUp = () => {
                if (this.env.model.getters.isPaintingFormat()) {
                    this.env.model.dispatch("PASTE", {
                        target: this.env.model.getters.getSelectedZones(),
                    });
                }
            };
            dragAndDropBeyondTheViewport(this.env, onMouseMove, onMouseUp);
        }
        onCellDoubleClicked(col, row) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            ({ col, row } = this.env.model.getters.getMainCellPosition({ sheetId, col, row }));
            const cell = this.env.model.getters.getEvaluatedCell({ sheetId, col, row });
            if (cell.type === CellValueType.empty) {
                this.props.onGridComposerCellFocused();
            }
            else {
                this.props.onComposerContentFocused();
            }
        }
        closeOpenedPopover() {
            this.env.model.dispatch("CLOSE_CELL_POPOVER");
        }
        // ---------------------------------------------------------------------------
        // Keyboard interactions
        // ---------------------------------------------------------------------------
        processArrows(ev) {
            ev.preventDefault();
            ev.stopPropagation();
            if (this.env.model.getters.hasOpenedPopover()) {
                this.closeOpenedPopover();
            }
            updateSelectionWithArrowKeys(ev, this.env.model.selection);
            if (this.env.model.getters.isPaintingFormat()) {
                this.env.model.dispatch("PASTE", {
                    target: this.env.model.getters.getSelectedZones(),
                });
            }
        }
        onKeydown(ev) {
            let keyDownString = "";
            if (!MODIFIER_KEYS.includes(ev.key)) {
                if (isCtrlKey(ev))
                    keyDownString += "CTRL+";
                if (ev.altKey)
                    keyDownString += "ALT+";
                if (ev.shiftKey)
                    keyDownString += "SHIFT+";
            }
            keyDownString += ev.key.toUpperCase();
            let handler = this.keyDownMapping[keyDownString];
            if (handler) {
                ev.preventDefault();
                ev.stopPropagation();
                handler();
                return;
            }
            if (ev.key.startsWith("Arrow")) {
                this.processArrows(ev);
                return;
            }
        }
        // ---------------------------------------------------------------------------
        // Context Menu
        // ---------------------------------------------------------------------------
        onInputContextMenu(ev) {
            ev.preventDefault();
            const lastZone = this.env.model.getters.getSelectedZone();
            const { left: col, top: row } = lastZone;
            let type = "CELL";
            interactiveStopEdition(this.env);
            if (this.env.model.getters.getActiveCols().has(col)) {
                type = "COL";
            }
            else if (this.env.model.getters.getActiveRows().has(row)) {
                type = "ROW";
            }
            const { x, y, width } = this.env.model.getters.getVisibleRect(lastZone);
            const gridRect = this.getGridRect();
            this.toggleContextMenu(type, gridRect.x + x + width, gridRect.y + y);
        }
        onCellRightClicked(col, row, { x, y }) {
            const zones = this.env.model.getters.getSelectedZones();
            const lastZone = zones[zones.length - 1];
            let type = "CELL";
            if (!isInside(col, row, lastZone)) {
                this.env.model.selection.getBackToDefault();
                this.env.model.selection.selectCell(col, row);
            }
            else {
                if (this.env.model.getters.getActiveCols().has(col)) {
                    type = "COL";
                }
                else if (this.env.model.getters.getActiveRows().has(row)) {
                    type = "ROW";
                }
            }
            this.toggleContextMenu(type, x, y);
        }
        toggleContextMenu(type, x, y) {
            if (this.env.model.getters.hasOpenedPopover()) {
                this.closeOpenedPopover();
            }
            this.menuState.isOpen = true;
            this.menuState.position = { x, y };
            this.menuState.menuItems = registries$1[type].getMenuItems();
        }
        copy(cut, ev) {
            if (!this.gridEl.contains(document.activeElement)) {
                return;
            }
            const clipboardData = ev.clipboardData;
            if (!clipboardData) {
                this.displayWarningCopyPasteNotSupported();
                return;
            }
            /* If we are currently editing a cell, let the default behavior */
            if (this.env.model.getters.getEditionMode() !== "inactive") {
                return;
            }
            if (cut) {
                interactiveCut(this.env);
            }
            else {
                this.env.model.dispatch("COPY");
            }
            const content = this.env.model.getters.getClipboardContent();
            for (const type in content) {
                clipboardData.setData(type, content[type]);
            }
            ev.preventDefault();
        }
        async paste(ev) {
            if (!this.gridEl.contains(document.activeElement)) {
                return;
            }
            const clipboardData = ev.clipboardData;
            if (!clipboardData) {
                this.displayWarningCopyPasteNotSupported();
                return;
            }
            if (clipboardData.types.indexOf(ClipboardMIMEType.PlainText) > -1) {
                const content = clipboardData.getData(ClipboardMIMEType.PlainText);
                const target = this.env.model.getters.getSelectedZones();
                const clipboardString = this.env.model.getters.getClipboardTextContent();
                const isCutOperation = this.env.model.getters.isCutOperation();
                if (clipboardString === content) {
                    // the paste actually comes from o-spreadsheet itself
                    interactivePaste(this.env, target);
                }
                else {
                    interactivePasteFromOS(this.env, target, content);
                }
                if (isCutOperation) {
                    await this.env.clipboard.write({ [ClipboardMIMEType.PlainText]: "" });
                }
            }
        }
        displayWarningCopyPasteNotSupported() {
            this.env.raiseError(_t("Copy/Paste is not supported in this browser."));
        }
        clearFormatting() {
            this.env.model.dispatch("CLEAR_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
            });
        }
        setHorizontalAlign(align) {
            this.env.model.dispatch("SET_FORMATTING", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                style: { align },
            });
        }
        closeMenu() {
            this.menuState.isOpen = false;
            this.focusDefaultElement();
        }
        processHeaderGroupingKey(direction) {
            if (this.env.model.getters.getSelectedZones().length !== 1) {
                return;
            }
            const selectingRows = this.env.model.getters.getActiveRows().size > 0;
            const selectingCols = this.env.model.getters.getActiveCols().size > 0;
            if (selectingCols && selectingRows) {
                this.processHeaderGroupingEventOnWholeSheet(direction);
            }
            else if (selectingCols) {
                this.processHeaderGroupingEventOnHeaders(direction, "COL");
            }
            else if (selectingRows) {
                this.processHeaderGroupingEventOnHeaders(direction, "ROW");
            }
            else {
                this.processHeaderGroupingEventOnGrid(direction);
            }
        }
        processHeaderGroupingEventOnHeaders(direction, dimension) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const zone = this.env.model.getters.getSelectedZone();
            const start = dimension === "COL" ? zone.left : zone.top;
            const end = dimension === "COL" ? zone.right : zone.bottom;
            switch (direction) {
                case "right":
                    this.env.model.dispatch("GROUP_HEADERS", { sheetId, dimension: dimension, start, end });
                    break;
                case "left":
                    this.env.model.dispatch("UNGROUP_HEADERS", { sheetId, dimension: dimension, start, end });
                    break;
                case "down":
                    this.env.model.dispatch("UNFOLD_HEADER_GROUPS_IN_ZONE", { sheetId, dimension, zone });
                    break;
                case "up":
                    this.env.model.dispatch("FOLD_HEADER_GROUPS_IN_ZONE", { sheetId, dimension, zone });
                    break;
            }
        }
        processHeaderGroupingEventOnWholeSheet(direction) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            if (direction === "up") {
                this.env.model.dispatch("FOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "ROW" });
                this.env.model.dispatch("FOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "COL" });
            }
            else if (direction === "down") {
                this.env.model.dispatch("UNFOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "ROW" });
                this.env.model.dispatch("UNFOLD_ALL_HEADER_GROUPS", { sheetId, dimension: "COL" });
            }
        }
        processHeaderGroupingEventOnGrid(direction) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const zone = this.env.model.getters.getSelectedZone();
            switch (direction) {
                case "down":
                    this.env.model.dispatch("UNFOLD_HEADER_GROUPS_IN_ZONE", {
                        sheetId,
                        dimension: "ROW",
                        zone: zone,
                    });
                    this.env.model.dispatch("UNFOLD_HEADER_GROUPS_IN_ZONE", {
                        sheetId,
                        dimension: "COL",
                        zone: zone,
                    });
                    break;
                case "up":
                    this.env.model.dispatch("FOLD_HEADER_GROUPS_IN_ZONE", {
                        sheetId,
                        dimension: "ROW",
                        zone: zone,
                    });
                    this.env.model.dispatch("FOLD_HEADER_GROUPS_IN_ZONE", {
                        sheetId,
                        dimension: "COL",
                        zone: zone,
                    });
                    break;
                case "right": {
                    const { x, y, width } = this.env.model.getters.getVisibleRect(zone);
                    const gridRect = this.getGridRect();
                    this.toggleContextMenu("GROUP_HEADERS", x + width + gridRect.x, y + gridRect.y);
                    break;
                }
                case "left": {
                    if (!canUngroupHeaders(this.env, "COL") && !canUngroupHeaders(this.env, "ROW")) {
                        return;
                    }
                    const { x, y, width } = this.env.model.getters.getVisibleRect(zone);
                    const gridRect = this.getGridRect();
                    this.toggleContextMenu("UNGROUP_HEADERS", x + width + gridRect.x, y + gridRect.y);
                    break;
                }
            }
        }
    }
    Grid.props = {
        sidePanelIsOpen: Boolean,
        exposeFocus: Function,
        focusComposer: String,
        onComposerContentFocused: Function,
        onGridComposerCellFocused: Function,
    };

    /**
     * Represent a raw XML string
     */
    class XMLString {
        xmlString;
        /**
         * @param xmlString should be a well formed, properly escaped XML string
         */
        constructor(xmlString) {
            this.xmlString = xmlString;
        }
        toString() {
            return this.xmlString;
        }
    }
    const XLSX_CHART_TYPES = [
        "areaChart",
        "area3DChart",
        "lineChart",
        "line3DChart",
        "stockChart",
        "radarChart",
        "scatterChart",
        "pieChart",
        "pie3DChart",
        "doughnutChart",
        "barChart",
        "bar3DChart",
        "ofPieChart",
        "surfaceChart",
        "surface3DChart",
        "bubbleChart",
    ];

    /** In XLSX color format (no #)  */
    const AUTO_COLOR = "000000";
    const XLSX_ICONSET_MAP = {
        arrow: "3Arrows",
        smiley: "3Symbols",
        dot: "3TrafficLights1",
    };
    const NAMESPACE = {
        styleSheet: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
        sst: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
        Relationships: "http://schemas.openxmlformats.org/package/2006/relationships",
        Types: "http://schemas.openxmlformats.org/package/2006/content-types",
        worksheet: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
        workbook: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
        drawing: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
        table: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
        revision: "http://schemas.microsoft.com/office/spreadsheetml/2014/revision",
        revision3: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision3",
        markupCompatibility: "http://schemas.openxmlformats.org/markup-compatibility/2006",
    };
    const DRAWING_NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
    const DRAWING_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
    const CONTENT_TYPES = {
        workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
        sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
        sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
        styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
        drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
        chart: "application/vnd.openxmlformats-officedocument.drawingml.chart+xml",
        themes: "application/vnd.openxmlformats-officedocument.theme+xml",
        table: "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml",
        pivot: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml",
        externalLink: "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml",
    };
    const XLSX_RELATION_TYPE = {
        document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
        sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
        sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
        styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
        drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
        chart: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart",
        theme: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
        table: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table",
        hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
        image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
    };
    const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
    const HEIGHT_FACTOR = 0.75; // 100px => 75 u
    /**
     * Excel says its default column width is 8.43 characters (64px)
     * which makes WIDTH_FACTOR = 0.1317, but it doesn't work well
     * 0.143 is a value from dev's experiments.
     */
    const WIDTH_FACTOR = 0.143;
    /** unit : maximum number of characters a column can hold at the standard font size. What. */
    const EXCEL_DEFAULT_COL_WIDTH = 8.43;
    /** unit : points */
    const EXCEL_DEFAULT_ROW_HEIGHT = 12.75;
    const EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS = 30;
    const EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS = 100;
    const FIRST_NUMFMT_ID = 164;
    const FORCE_DEFAULT_ARGS_FUNCTIONS = {
        FLOOR: [{ type: "NUMBER", value: 1 }],
        CEILING: [{ type: "NUMBER", value: 1 }],
        ROUND: [{ type: "NUMBER", value: 0 }],
        ROUNDUP: [{ type: "NUMBER", value: 0 }],
        ROUNDDOWN: [{ type: "NUMBER", value: 0 }],
    };
    /**
     * This list contains all "future" functions that are not compatible with older versions of Excel
     * For more information, see https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/5d1b6d44-6fc1-4ecd-8fef-0b27406cc2bf
     */
    const NON_RETROCOMPATIBLE_FUNCTIONS = [
        "ACOT",
        "ACOTH",
        "AGGREGATE",
        "ARABIC",
        "BASE",
        "BETA.DIST",
        "BETA.INV",
        "BINOM.DIST",
        "BINOM.DIST.RANGE",
        "BINOM.INV",
        "BITAND",
        "BITLSHIFT",
        "BITOR",
        "BITRSHIFT",
        "BITXOR",
        "CEILING.MATH",
        "CEILING.PRECISE",
        "CHISQ.DIST",
        "CHISQ.DIST.RT",
        "CHISQ.INV",
        "CHISQ.INV.RT",
        "CHISQ.TEST",
        "COMBINA",
        "CONCAT",
        "CONFIDENCE.NORM",
        "CONFIDENCE.T",
        "COT",
        "COTH",
        "COVARIANCE.P",
        "COVARIANCE.S",
        "CSC",
        "CSCH",
        "DAYS",
        "DECIMAL",
        "ERF.PRECISE",
        "ERFC.PRECISE",
        "EXPON.DIST",
        "F.DIST",
        "F.DIST.RT",
        "F.INV",
        "F.INV.RT",
        "F.TEST",
        "FILTERXML",
        "FLOOR.MATH",
        "FLOOR.PRECISE",
        "FORECAST.ETS",
        "FORECAST.ETS.CONFINT",
        "FORECAST.ETS.SEASONALITY",
        "FORECAST.ETS.STAT",
        "FORECAST.LINEAR",
        "FORMULATEXT",
        "GAMMA",
        "GAMMA.DIST",
        "GAMMA.INV",
        "GAMMALN.PRECISE",
        "GAUSS",
        "HYPGEOM.DIST",
        "IFNA",
        "IFS",
        "IMCOSH",
        "IMCOT",
        "IMCSC",
        "IMCSCH",
        "IMSEC",
        "IMSECH",
        "IMSINH",
        "IMTAN",
        "ISFORMULA",
        "ISOWEEKNUM",
        "LOGNORM.DIST",
        "LOGNORM.INV",
        "MAXIFS",
        "MINIFS",
        "MODE.MULT",
        "MODE.SNGL",
        "MUNIT",
        "NEGBINOM.DIST",
        "NORM.DIST",
        "NORM.INV",
        "NORM.S.DIST",
        "NORM.S.INV",
        "NUMBERVALUE",
        "PDURATION",
        "PERCENTILE.EXC",
        "PERCENTILE.INC",
        "PERCENTRANK.EXC",
        "PERCENTRANK.INC",
        "PERMUTATIONA",
        "PHI",
        "POISSON.DIST",
        "QUARTILE.EXC",
        "QUARTILE.INC",
        "QUERYSTRING",
        "RANK.AVG",
        "RANK.EQ",
        "RRI",
        "SEC",
        "SECH",
        "SHEET",
        "SHEETS",
        "SKEW.P",
        "STDEV.P",
        "STDEV.S",
        "SWITCH",
        "T.DIST",
        "T.DIST.2T",
        "T.DIST.RT",
        "T.INV",
        "T.INV.2T",
        "T.TEST",
        "TEXTJOIN",
        "UNICHAR",
        "UNICODE",
        "VAR.P",
        "VAR.S",
        "WEBSERVICE",
        "WEIBULL.DIST",
        "XOR",
        "Z.TEST",
    ];
    const CONTENT_TYPES_FILE = "[Content_Types].xml";

    /**
     * Map of the different types of conversions warnings and their name in error messages
     */
    var WarningTypes;
    (function (WarningTypes) {
        WarningTypes["DiagonalBorderNotSupported"] = "Diagonal Borders";
        WarningTypes["BorderStyleNotSupported"] = "Border style";
        WarningTypes["FillStyleNotSupported"] = "Fill Style";
        WarningTypes["FontNotSupported"] = "Font";
        WarningTypes["HorizontalAlignmentNotSupported"] = "Horizontal Alignment";
        WarningTypes["VerticalAlignmentNotSupported"] = "Vertical Alignments";
        WarningTypes["MultipleRulesCfNotSupported"] = "Multiple rules conditional formats";
        WarningTypes["CfTypeNotSupported"] = "Conditional format type";
        WarningTypes["CfFormatBorderNotSupported"] = "Borders in conditional formats";
        WarningTypes["CfFormatAlignmentNotSupported"] = "Alignment in conditional formats";
        WarningTypes["CfFormatNumFmtNotSupported"] = "Num formats in conditional formats";
        WarningTypes["CfIconSetEmptyIconNotSupported"] = "IconSets with empty icons";
        WarningTypes["BadlyFormattedHyperlink"] = "Badly formatted hyperlink";
        WarningTypes["NumFmtIdNotSupported"] = "Number format";
    })(WarningTypes || (WarningTypes = {}));
    class XLSXImportWarningManager {
        _parsingWarnings = new Set();
        _conversionWarnings = new Set();
        addParsingWarning(warning) {
            this._parsingWarnings.add(warning);
        }
        addConversionWarning(warning) {
            this._conversionWarnings.add(warning);
        }
        get warnings() {
            return [...this._parsingWarnings, ...this._conversionWarnings];
        }
        /**
         * Add a warning "... is not supported" to the manager.
         *
         * @param type the type of the warning to add
         * @param name optional, name of the element that was not supported
         * @param supported optional, list of the supported elements
         */
        generateNotSupportedWarning(type, name, supported) {
            let warning = `${type} ${name ? '"' + name + '" is' : "are"} not yet supported. `;
            if (supported) {
                warning += `Only ${supported.join(", ")} are currently supported.`;
            }
            if (!this._conversionWarnings.has(warning)) {
                this._conversionWarnings.add(warning);
            }
        }
    }

    const SUPPORTED_BORDER_STYLES = ["thin"];
    const SUPPORTED_HORIZONTAL_ALIGNMENTS = [
        "general",
        "left",
        "center",
        "right",
    ];
    const SUPPORTED_VERTICAL_ALIGNMENTS = ["top", "center", "bottom"];
    const SUPPORTED_FONTS = ["Arial"];
    const SUPPORTED_FILL_PATTERNS = ["solid"];
    const SUPPORTED_CF_TYPES = [
        "expression",
        "cellIs",
        "colorScale",
        "iconSet",
        "containsText",
        "notContainsText",
        "beginsWith",
        "endsWith",
        "containsBlanks",
        "notContainsBlanks",
    ];
    /** Map between cell type in XLSX file and human readable cell type  */
    const CELL_TYPE_CONVERSION_MAP = {
        b: "boolean",
        d: "date",
        e: "error",
        inlineStr: "inlineStr",
        n: "number",
        s: "sharedString",
        str: "str",
    };
    /** Conversion map Border Style in XLSX <=> Border style in o_spreadsheet*/
    const BORDER_STYLE_CONVERSION_MAP = {
        dashDot: "thin",
        dashDotDot: "thin",
        dashed: "dashed",
        dotted: "dotted",
        double: "thin",
        hair: "thin",
        medium: "medium",
        mediumDashDot: "thin",
        mediumDashDotDot: "thin",
        mediumDashed: "thin",
        none: undefined,
        slantDashDot: "thin",
        thick: "thick",
        thin: "thin",
    };
    /** Conversion map Horizontal Alignment in XLSX <=> Horizontal Alignment in o_spreadsheet*/
    const H_ALIGNMENT_CONVERSION_MAP = {
        general: undefined,
        left: "left",
        center: "center",
        right: "right",
        fill: "left",
        justify: "left",
        centerContinuous: "center",
        distributed: "center",
    };
    /** Conversion map Vertical Alignment in XLSX => Vertical Alignment in o_spreadsheet */
    const V_ALIGNMENT_CONVERSION_MAP = {
        top: "top",
        center: "middle",
        bottom: "bottom",
        justify: "middle",
        distributed: "middle",
    };
    /** Conversion map Vertical Alignment in o-spreadsheet => Vertical Alignment in XLSX */
    const V_ALIGNMENT_EXPORT_CONVERSION_MAP = {
        top: "top",
        middle: "center",
        bottom: "bottom",
    };
    /** Convert the "CellIs" cf operator.
     * We have all the operators that the xlsx have, but ours begin with a uppercase character */
    function convertCFCellIsOperator(xlsxCfOperator) {
        return (xlsxCfOperator.slice(0, 1).toUpperCase() +
            xlsxCfOperator.slice(1));
    }
    /** Conversion map CF types in XLSX <=> Cf types in o_spreadsheet */
    const CF_TYPE_CONVERSION_MAP = {
        aboveAverage: undefined,
        expression: undefined,
        cellIs: undefined,
        colorScale: undefined,
        dataBar: undefined,
        iconSet: undefined,
        top10: undefined,
        uniqueValues: undefined,
        duplicateValues: undefined,
        containsText: "ContainsText",
        notContainsText: "NotContains",
        beginsWith: "BeginsWith",
        endsWith: "EndsWith",
        containsBlanks: "IsEmpty",
        notContainsBlanks: "IsNotEmpty",
        containsErrors: undefined,
        notContainsErrors: undefined,
        timePeriod: undefined,
    };
    /** Conversion map CF thresholds types in XLSX <=> Cf thresholds types in o_spreadsheet */
    const CF_THRESHOLD_CONVERSION_MAP = {
        num: "number",
        percent: "percentage",
        max: "value",
        min: "value",
        percentile: "percentile",
        formula: "formula",
    };
    /**
     * Conversion map between Excels IconSets and our own IconSets. The string is the key of the iconset in the ICON_SETS constant.
     *
     * NoIcons is undefined instead of an empty string because we don't support it and need to mange it separately.
     */
    const ICON_SET_CONVERSION_MAP = {
        NoIcons: undefined,
        "3Arrows": "arrows",
        "3ArrowsGray": "arrows",
        "3Symbols": "smiley",
        "3Symbols2": "smiley",
        "3Signs": "dots",
        "3Flags": "dots",
        "3TrafficLights1": "dots",
        "3TrafficLights2": "dots",
        "4Arrows": "arrows",
        "4ArrowsGray": "arrows",
        "4RedToBlack": "dots",
        "4Rating": "smiley",
        "4TrafficLights": "dots",
        "5Arrows": "arrows",
        "5ArrowsGray": "arrows",
        "5Rating": "smiley",
        "5Quarters": "dots",
        "3Stars": "smiley",
        "3Triangles": "arrows",
        "5Boxes": "dots",
    };
    /** Map between legend position in XLSX file and human readable position  */
    const DRAWING_LEGEND_POSITION_CONVERSION_MAP = {
        b: "bottom",
        t: "top",
        l: "left",
        r: "right",
        tr: "right",
    };
    /** Conversion map chart types in XLSX <=> Cf chart types o_spreadsheet (undefined for unsupported chart types)*/
    const CHART_TYPE_CONVERSION_MAP = {
        areaChart: undefined,
        area3DChart: undefined,
        lineChart: "line",
        line3DChart: undefined,
        stockChart: undefined,
        radarChart: undefined,
        scatterChart: undefined,
        pieChart: "pie",
        pie3DChart: undefined,
        doughnutChart: "pie",
        barChart: "bar",
        bar3DChart: undefined,
        ofPieChart: undefined,
        surfaceChart: undefined,
        surface3DChart: undefined,
        bubbleChart: undefined,
    };
    /** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/
    const SUBTOTAL_FUNCTION_CONVERSION_MAP = {
        "1": "AVERAGE",
        "2": "COUNT",
        "3": "COUNTA",
        "4": "MAX",
        "5": "MIN",
        "6": "PRODUCT",
        "7": "STDEV",
        "8": "STDEVP",
        "9": "SUM",
        "10": "VAR",
        "11": "VARP",
        "101": "AVERAGE",
        "102": "COUNT",
        "103": "COUNTA",
        "104": "MAX",
        "105": "MIN",
        "106": "PRODUCT",
        "107": "STDEV",
        "108": "STDEVP",
        "109": "SUM",
        "110": "VAR",
        "111": "VARP",
    };
    /** Mapping between Excel format indexes (see XLSX_FORMAT_MAP) and some supported formats  */
    const XLSX_FORMATS_CONVERSION_MAP = {
        0: "",
        1: "0",
        2: "0.00",
        3: "#,#00",
        4: "#,##0.00",
        9: "0%",
        10: "0.00%",
        11: undefined,
        12: undefined,
        13: undefined,
        14: "m/d/yyyy",
        15: "m/d/yyyy",
        16: "m/d/yyyy",
        17: "m/d/yyyy",
        18: "hh:mm:ss a",
        19: "hh:mm:ss a",
        20: "hhhh:mm:ss",
        21: "hhhh:mm:ss",
        22: "m/d/yy h:mm",
        37: undefined,
        38: undefined,
        39: undefined,
        40: undefined,
        45: "hhhh:mm:ss",
        46: "hhhh:mm:ss",
        47: "hhhh:mm:ss",
        48: undefined,
        49: undefined,
    };
    /**
     * Mapping format index to format defined by default
     *
     * OpenXML $18.8.30
     * */
    const XLSX_FORMAT_MAP = {
        "0": 1,
        "0.00": 2,
        "#,#00": 3,
        "#,##0.00": 4,
        "0%": 9,
        "0.00%": 10,
        "0.00E+00": 11,
        "# ?/?": 12,
        "# ??/??": 13,
        "mm-dd-yy": 14,
        "d-mm-yy": 15,
        "mm-yy": 16,
        "mmm-yy": 17,
        "h:mm AM/PM": 18,
        "h:mm:ss AM/PM": 19,
        "h:mm": 20,
        "h:mm:ss": 21,
        "m/d/yy h:mm": 22,
        "#,##0 ;(#,##0)": 37,
        "#,##0 ;[Red](#,##0)": 38,
        "#,##0.00;(#,##0.00)": 39,
        "#,##0.00;[Red](#,##0.00)": 40,
        "mm:ss": 45,
        "[h]:mm:ss": 46,
        "mmss.0": 47,
        "##0.0E+0": 48,
        "@": 49,
        "hh:mm:ss a": 19, // TODO: discuss: this format is not recognized by excel for example (doesn't follow their guidelines I guess)
    };
    /** OpenXML $18.8.27 */
    const XLSX_INDEXED_COLORS = {
        0: "000000",
        1: "FFFFFF",
        2: "FF0000",
        3: "00FF00",
        4: "0000FF",
        5: "FFFF00",
        6: "FF00FF",
        7: "00FFFF",
        8: "000000",
        9: "FFFFFF",
        10: "FF0000",
        11: "00FF00",
        12: "0000FF",
        13: "FFFF00",
        14: "FF00FF",
        15: "00FFFF",
        16: "800000",
        17: "008000",
        18: "000080",
        19: "808000",
        20: "800080",
        21: "008080",
        22: "C0C0C0",
        23: "808080",
        24: "9999FF",
        25: "993366",
        26: "FFFFCC",
        27: "CCFFFF",
        28: "660066",
        29: "FF8080",
        30: "0066CC",
        31: "CCCCFF",
        32: "000080",
        33: "FF00FF",
        34: "FFFF00",
        35: "00FFFF",
        36: "800080",
        37: "800000",
        38: "008080",
        39: "0000FF",
        40: "00CCFF",
        41: "CCFFFF",
        42: "CCFFCC",
        43: "FFFF99",
        44: "99CCFF",
        45: "FF99CC",
        46: "CC99FF",
        47: "FFCC99",
        48: "3366FF",
        49: "33CCCC",
        50: "99CC00",
        51: "FFCC00",
        52: "FF9900",
        53: "FF6600",
        54: "666699",
        55: "969696",
        56: "003366",
        57: "339966",
        58: "003300",
        59: "333300",
        60: "993300",
        61: "993366",
        62: "333399",
        63: "333333",
        64: "000000",
        65: "FFFFFF", // system background
    };
    const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
        "image/avif": "avif",
        "image/bmp": "bmp",
        "image/gif": "gif",
        "image/vnd.microsoft.icon": "ico",
        "image/jpeg": "jpeg",
        "image/png": "png",
        "image/tiff": "tiff",
        "image/webp": "webp",
    };
    const IMAGE_EXTENSION_TO_MIMETYPE_MAPPING = {
        avif: "image/avif",
        bmp: "image/bmp",
        gif: "image/gif",
        ico: "image/vnd.microsoft.icon",
        jpeg: "image/jpeg",
        png: "image/png",
        tiff: "image/tiff",
        webp: "image/webp",
        jpg: "image/jpeg",
    };

    /**
     * Most of the functions could stay private, but are exported for testing purposes
     */
    /**
     *
     * Extract the color referenced inside of an XML element and return it as an hex string #RRGGBBAA (or #RRGGBB
     * if alpha = FF)
     *
     *  The color is an attribute of the element that can be :
     *  - rgb : an rgb string
     *  - theme : a reference to a theme element
     *  - auto : automatic coloring. Return const AUTO_COLOR in constants.ts.
     *  - indexed : a legacy indexing scheme for colors. The only value that should be present in a xlsx is
     *      64 = System Foreground, that we can replace with AUTO_COLOR.
     */
    function convertColor(xlsxColor) {
        if (!xlsxColor) {
            return undefined;
        }
        let rgb;
        if (xlsxColor.rgb) {
            rgb = xlsxColor.rgb;
        }
        else if (xlsxColor.auto) {
            rgb = AUTO_COLOR;
        }
        else if (xlsxColor.indexed) {
            rgb = XLSX_INDEXED_COLORS[xlsxColor.indexed];
        }
        else {
            return undefined;
        }
        rgb = xlsxColorToHEXA(rgb);
        if (xlsxColor.tint) {
            rgb = applyTint(rgb, xlsxColor.tint);
        }
        rgb = rgb.toUpperCase();
        // Remove unnecessary alpha
        if (rgb.length === 9 && rgb.endsWith("FF")) {
            rgb = rgb.slice(0, 7);
        }
        return rgb;
    }
    /**
     * Convert a hex color AARRGGBB (or RRGGBB)(representation inside XLSX Xmls) to a standard js color
     * representation #RRGGBBAA
     */
    function xlsxColorToHEXA(color) {
        if (color.length === 6)
            return "#" + color + "FF";
        return "#" + color.slice(2) + color.slice(0, 2);
    }
    /**
     *  Apply tint to a color (see OpenXml spec §18.3.1.15);
     */
    function applyTint(color, tint) {
        const rgba = colorToRGBA(color);
        const hsla = rgbaToHSLA(rgba);
        if (tint < 0) {
            hsla.l = hsla.l * (1 + tint);
        }
        if (tint > 0) {
            hsla.l = hsla.l * (1 - tint) + (100 - 100 * (1 - tint));
        }
        return rgbaToHex(hslaToRGBA(hsla));
    }
    /**
     * Convert a hex + alpha color string to an integer representation. Also remove the alpha.
     *
     * eg. #FF0000FF => 4278190335
     */
    function hexaToInt(hex) {
        if (hex.length === 9) {
            hex = hex.slice(0, 7);
        }
        return parseInt(hex.replace("#", ""), 16);
    }

    /**
     * Get the relative path between two files
     *
     * Eg.:
     * from "folder1/file1.txt" to "folder2/file2.txt" => "../folder2/file2.txt"
     */
    function getRelativePath(from, to) {
        const fromPathParts = from.split("/");
        const toPathParts = to.split("/");
        let relPath = "";
        let startIndex = 0;
        for (let i = 0; i < fromPathParts.length - 1; i++) {
            if (fromPathParts[i] === toPathParts[i]) {
                startIndex++;
            }
            else {
                relPath += "../";
            }
        }
        relPath += toPathParts.slice(startIndex).join("/");
        return relPath;
    }
    /**
     * Convert an array of element into an object where the objects keys were the elements position in the array.
     * Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.
     *
     * eg. : ["a", "b"] => {0:"a", 1:"b"}
     */
    function arrayToObject(array, indexOffset = 0) {
        const obj = {};
        for (let i = 0; i < array.length; i++) {
            if (array[i]) {
                obj[i + indexOffset] = array[i];
            }
        }
        return obj;
    }
    /**
     * Convert an object whose keys are numbers to an array were the element index was their key in the object.
     *
     * eg. : {0:"a", 2:"b"} => ["a", undefined, "b"]
     */
    function objectToArray(obj) {
        const arr = [];
        for (let key of Object.keys(obj).map(Number)) {
            arr[key] = obj[key];
        }
        return arr;
    }
    /**
     * In xlsx we can have string with unicode characters with the format _x00fa_.
     * Replace with characters understandable by JS
     */
    function fixXlsxUnicode(str) {
        return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {
            return String.fromCharCode(parseInt(code, 16));
        });
    }

    const XLSX_DATE_FORMAT_REGEX = /^(yy|yyyy|m{1,5}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/m|\s|-|\/|\.|:)+$/i;
    /**
     * Convert excel format to o_spreadsheet format
     *
     * Excel format are defined in openXML §18.8.31
     */
    function convertXlsxFormat(numFmtId, formats, warningManager) {
        if (numFmtId === 0) {
            return undefined;
        }
        // Format is either defined in the imported data, or the formatId is defined in openXML §18.8.30
        let format = XLSX_FORMATS_CONVERSION_MAP[numFmtId] || formats.find((f) => f.id === numFmtId)?.format;
        if (format) {
            try {
                let convertedFormat = format.replace(/(.*?);.*/, "$1"); // only take first part of multi-part format
                convertedFormat = convertedFormat.replace(/\[(.*)-[A-Z0-9]{3}\]/g, "[$1]"); // remove currency and locale/date system/number system info (ECMA §18.8.31)
                convertedFormat = convertedFormat.replace(/\[\$\]/g, ""); // remove empty bocks
                // Quotes in format escape sequences of characters. ATM we only support [$...] blocks to escape characters, and only one of them per format
                const numberOfQuotes = convertedFormat.match(/"/g)?.length || 0;
                const numberOfOpenBrackets = convertedFormat.match(/\[/g)?.length || 0;
                if (numberOfQuotes / 2 + numberOfOpenBrackets > 1) {
                    throw new Error("Multiple escaped blocks in format");
                }
                convertedFormat = convertedFormat.replace(/"(.*)"/g, "[$$$1]"); // replace '"..."' by '[$...]'
                convertedFormat = convertedFormat.replace(/_.{1}/g, ""); // _ === ignore width of next char for align purposes. Not supported ATM
                convertedFormat = convertedFormat.replace(/\*.{1}/g, ""); // * === repeat next character enough to fill the line. Not supported ATM
                convertedFormat = convertedFormat.replace(/\\ /g, " "); // unescape spaces
                convertedFormat = convertedFormat.replace(/\\./g, (match) => match[1]); // unescape other characters
                if (isXlsxDateFormat(convertedFormat)) {
                    convertedFormat = convertDateFormat$1(convertedFormat);
                }
                if (isFormatSupported(convertedFormat)) {
                    return convertedFormat;
                }
            }
            catch (e) { }
        }
        warningManager.generateNotSupportedWarning(WarningTypes.NumFmtIdNotSupported, format || `nmFmtId ${numFmtId}`);
        return undefined;
    }
    function isFormatSupported(format) {
        try {
            formatValue(0, { format, locale: DEFAULT_LOCALE });
            return true;
        }
        catch (e) {
            return false;
        }
    }
    function isXlsxDateFormat(format) {
        return XLSX_DATE_FORMAT_REGEX.test(format);
    }
    function convertDateFormat$1(format) {
        // Some of these aren't defined neither in the OpenXML spec not the Xlsx extension of OpenXML,
        // but can still occur and are supported by Excel/Google sheets
        format = format.toLowerCase();
        format = format.replace(/mmmmm/g, "mmm");
        format = format.replace(/am\/pm|a\/m/g, "a");
        format = format.replace(/hhhh/g, "hh");
        format = format.replace(/\bh\b/g, "hh");
        return format;
    }

    function convertBorders(data, warningManager) {
        const borderArray = data.borders.map((border) => {
            addBorderWarnings(border, warningManager);
            const b = {
                top: convertBorderDescr$1(border.top, warningManager),
                bottom: convertBorderDescr$1(border.bottom, warningManager),
                left: convertBorderDescr$1(border.left, warningManager),
                right: convertBorderDescr$1(border.right, warningManager),
            };
            Object.keys(b).forEach((key) => b[key] === undefined && delete b[key]);
            return b;
        });
        return arrayToObject(borderArray, 1);
    }
    function convertBorderDescr$1(borderDescr, warningManager) {
        if (!borderDescr)
            return undefined;
        addBorderDescrWarnings(borderDescr, warningManager);
        const style = BORDER_STYLE_CONVERSION_MAP[borderDescr.style];
        return style ? { style, color: convertColor(borderDescr.color) } : undefined;
    }
    function convertStyles(data, warningManager) {
        const stylesArray = data.styles.map((style) => {
            return convertStyle({
                fontStyle: data.fonts[style.fontId],
                fillStyle: data.fills[style.fillId],
                alignment: style.alignment,
            }, warningManager);
        });
        return arrayToObject(stylesArray, 1);
    }
    function convertStyle(styleStruct, warningManager) {
        addStyleWarnings(styleStruct?.fontStyle, styleStruct?.fillStyle, warningManager);
        addHorizontalAlignmentWarnings(styleStruct?.alignment?.horizontal, warningManager);
        addVerticalAlignmentWarnings(styleStruct?.alignment?.vertical, warningManager);
        return {
            bold: styleStruct.fontStyle?.bold,
            italic: styleStruct.fontStyle?.italic,
            strikethrough: styleStruct.fontStyle?.strike,
            underline: styleStruct.fontStyle?.underline,
            verticalAlign: styleStruct.alignment?.vertical
                ? V_ALIGNMENT_CONVERSION_MAP[styleStruct.alignment.vertical]
                : undefined,
            align: styleStruct.alignment?.horizontal
                ? H_ALIGNMENT_CONVERSION_MAP[styleStruct.alignment.horizontal]
                : undefined,
            // In xlsx fills, bgColor is the color of the fill, and fgColor is the color of the pattern above the background, except in solid fills
            fillColor: styleStruct.fillStyle?.patternType === "solid"
                ? convertColor(styleStruct.fillStyle?.fgColor)
                : convertColor(styleStruct.fillStyle?.bgColor),
            textColor: convertColor(styleStruct.fontStyle?.color),
            fontSize: styleStruct.fontStyle?.size,
            wrapping: styleStruct.alignment?.wrapText ? "wrap" : "overflow",
        };
    }
    function convertFormats(data, warningManager) {
        const formats = [];
        for (let style of data.styles) {
            const format = convertXlsxFormat(style.numFmtId, data.numFmts, warningManager);
            if (format) {
                formats[style.numFmtId] = format;
            }
        }
        return arrayToObject(formats, 1);
    }
    // ---------------------------------------------------------------------------
    // Warnings
    // ---------------------------------------------------------------------------
    function addStyleWarnings(font, fill, warningManager) {
        if (font && font.name && !SUPPORTED_FONTS.includes(font.name)) {
            warningManager.generateNotSupportedWarning(WarningTypes.FontNotSupported, font.name, SUPPORTED_FONTS);
        }
        if (fill && fill.patternType && !SUPPORTED_FILL_PATTERNS.includes(fill.patternType)) {
            warningManager.generateNotSupportedWarning(WarningTypes.FillStyleNotSupported, fill.patternType, SUPPORTED_FILL_PATTERNS);
        }
    }
    function addBorderDescrWarnings(borderDescr, warningManager) {
        if (!SUPPORTED_BORDER_STYLES.includes(borderDescr.style)) {
            warningManager.generateNotSupportedWarning(WarningTypes.BorderStyleNotSupported, borderDescr.style, SUPPORTED_BORDER_STYLES);
        }
    }
    function addBorderWarnings(border, warningManager) {
        if (border.diagonal) {
            warningManager.generateNotSupportedWarning(WarningTypes.DiagonalBorderNotSupported);
        }
    }
    function addHorizontalAlignmentWarnings(alignment, warningManager) {
        if (alignment && !SUPPORTED_HORIZONTAL_ALIGNMENTS.includes(alignment)) {
            warningManager.generateNotSupportedWarning(WarningTypes.HorizontalAlignmentNotSupported, alignment, SUPPORTED_HORIZONTAL_ALIGNMENTS);
        }
    }
    function addVerticalAlignmentWarnings(alignment, warningManager) {
        if (alignment && !SUPPORTED_VERTICAL_ALIGNMENTS.includes(alignment)) {
            warningManager.generateNotSupportedWarning(WarningTypes.VerticalAlignmentNotSupported, alignment, SUPPORTED_VERTICAL_ALIGNMENTS);
        }
    }

    function convertConditionalFormats(xlsxCfs, dxfs, warningManager) {
        const cfs = [];
        let cfId = 1;
        for (let cf of xlsxCfs) {
            if (cf.cfRules.length === 0)
                continue;
            addCfConversionWarnings(cf, dxfs, warningManager);
            const rule = cf.cfRules[0];
            let operator;
            const values = [];
            if (rule.dxfId === undefined && !(rule.type === "colorScale" || rule.type === "iconSet"))
                continue;
            switch (rule.type) {
                case "aboveAverage":
                case "containsErrors":
                case "notContainsErrors":
                case "dataBar":
                case "duplicateValues":
                case "expression":
                case "top10":
                case "uniqueValues":
                case "timePeriod":
                    // Not supported
                    continue;
                case "colorScale":
                    const colorScale = convertColorScale(cfId++, cf);
                    if (colorScale) {
                        cfs.push(colorScale);
                    }
                    continue;
                case "iconSet":
                    const iconSet = convertIconSet(cfId++, cf, warningManager);
                    if (iconSet) {
                        cfs.push(iconSet);
                    }
                    continue;
                case "containsText":
                case "notContainsText":
                case "beginsWith":
                case "endsWith":
                    if (!rule.text)
                        continue;
                    operator = CF_TYPE_CONVERSION_MAP[rule.type];
                    values.push(rule.text);
                    break;
                case "containsBlanks":
                case "notContainsBlanks":
                    operator = CF_TYPE_CONVERSION_MAP[rule.type];
                    break;
                case "cellIs":
                    if (!rule.operator || !rule.formula || rule.formula.length === 0)
                        continue;
                    operator = convertCFCellIsOperator(rule.operator);
                    values.push(rule.formula[0]);
                    if (rule.formula.length === 2) {
                        values.push(rule.formula[1]);
                    }
                    break;
            }
            if (operator && rule.dxfId !== undefined) {
                cfs.push({
                    id: (cfId++).toString(),
                    ranges: cf.sqref,
                    stopIfTrue: rule.stopIfTrue,
                    rule: {
                        type: "CellIsRule",
                        operator: operator,
                        values: values,
                        style: convertStyle({ fontStyle: dxfs[rule.dxfId].font, fillStyle: dxfs[rule.dxfId].fill }, warningManager),
                    },
                });
            }
        }
        return cfs;
    }
    function convertColorScale(id, xlsxCf) {
        const scale = xlsxCf.cfRules[0].colorScale;
        if (!scale ||
            scale.cfvos.length !== scale.colors.length ||
            scale.cfvos.length < 2 ||
            scale.cfvos.length > 3) {
            return undefined;
        }
        const thresholds = [];
        for (let i = 0; i < scale.cfvos.length; i++) {
            thresholds.push({
                color: hexaToInt(convertColor(scale.colors[i]) || "#FFFFFF"),
                type: CF_THRESHOLD_CONVERSION_MAP[scale.cfvos[i].type],
                value: scale.cfvos[i].value,
            });
        }
        const minimum = thresholds[0];
        const maximum = thresholds.length === 2 ? thresholds[1] : thresholds[2];
        const midpoint = thresholds.length === 3 ? thresholds[1] : undefined;
        return {
            id: id.toString(),
            stopIfTrue: xlsxCf.cfRules[0].stopIfTrue,
            ranges: xlsxCf.sqref,
            rule: { type: "ColorScaleRule", minimum, midpoint, maximum },
        };
    }
    /**
     * Convert Icons Sets.
     *
     * In the Xlsx extension of OpenXml, the IconSets can either be simply an IconSet, or a list of Icons
     *  (ie. their respective IconSet and their id in this set).
     *
     * In the case of a list of icons :
     *  - The order of the icons is lower => middle => upper
     *  - The their ids are :  0 : bad, 1 : neutral, 2 : good
     */
    function convertIconSet(id, xlsxCf, warningManager) {
        const xlsxIconSet = xlsxCf.cfRules[0].iconSet;
        if (!xlsxIconSet)
            return undefined;
        let cfVos = xlsxIconSet.cfvos;
        let cfIcons = xlsxIconSet.cfIcons;
        if (cfVos.length < 3 || (cfIcons && cfIcons.length < 3)) {
            return undefined;
        }
        // We don't support icon sets with more than 3 icons, so take the extrema and the middle.
        if (cfVos.length > 3) {
            cfVos = [cfVos[0], cfVos[Math.floor(cfVos.length / 2)], cfVos[cfVos.length - 1]];
        }
        if (cfIcons && cfIcons.length > 3) {
            cfIcons = [cfIcons[0], cfIcons[Math.floor(cfIcons.length / 2)], cfIcons[cfIcons.length - 1]];
        }
        // In xlsx, the thresholds are NOT in the first cfVo, but on the second and third
        const thresholds = [];
        for (let i = 1; i <= 2; i++) {
            const type = CF_THRESHOLD_CONVERSION_MAP[cfVos[i].type];
            if (type === "value") {
                return undefined;
            }
            thresholds.push({
                value: cfVos[i].value || "",
                operator: cfVos[i].gte ? "ge" : "gt",
                type: type,
            });
        }
        let icons = {
            lower: cfIcons
                ? convertIcons(cfIcons[0].iconSet, cfIcons[0].iconId)
                : convertIcons(xlsxIconSet.iconSet, 0),
            middle: cfIcons
                ? convertIcons(cfIcons[1].iconSet, cfIcons[1].iconId)
                : convertIcons(xlsxIconSet.iconSet, 1),
            upper: cfIcons
                ? convertIcons(cfIcons[2].iconSet, cfIcons[2].iconId)
                : convertIcons(xlsxIconSet.iconSet, 2),
        };
        if (xlsxIconSet.reverse) {
            icons = { upper: icons.lower, middle: icons.middle, lower: icons.upper };
        }
        // We don't support empty icons in an IconSet, put a dot icon instead
        for (let key of Object.keys(icons)) {
            if (!icons[key]) {
                warningManager.generateNotSupportedWarning(WarningTypes.CfIconSetEmptyIconNotSupported);
                switch (key) {
                    case "upper":
                        icons[key] = ICON_SETS.dots.good;
                        break;
                    case "middle":
                        icons[key] = ICON_SETS.dots.neutral;
                        break;
                    case "lower":
                        icons[key] = ICON_SETS.dots.bad;
                        break;
                }
            }
        }
        return {
            id: id.toString(),
            stopIfTrue: xlsxCf.cfRules[0].stopIfTrue,
            ranges: xlsxCf.sqref,
            rule: {
                type: "IconSetRule",
                icons: icons,
                upperInflectionPoint: thresholds[1],
                lowerInflectionPoint: thresholds[0],
            },
        };
    }
    /**
     * Convert an icon from a XLSX.
     *
     * The indexes are : 0 : bad, 1 : neutral, 2 : good
     */
    function convertIcons(xlsxIconSet, index) {
        const iconSet = ICON_SET_CONVERSION_MAP[xlsxIconSet];
        if (!iconSet)
            return "";
        return index === 0
            ? ICON_SETS[iconSet].bad
            : index === 1
                ? ICON_SETS[iconSet].neutral
                : ICON_SETS[iconSet].good;
    }
    // ---------------------------------------------------------------------------
    // Warnings
    // ---------------------------------------------------------------------------
    function addCfConversionWarnings(cf, dxfs, warningManager) {
        if (cf.cfRules.length > 1) {
            warningManager.generateNotSupportedWarning(WarningTypes.MultipleRulesCfNotSupported);
        }
        if (!SUPPORTED_CF_TYPES.includes(cf.cfRules[0].type)) {
            warningManager.generateNotSupportedWarning(WarningTypes.CfTypeNotSupported, cf.cfRules[0].type);
        }
        if (cf.cfRules[0].dxfId) {
            const dxf = dxfs[cf.cfRules[0].dxfId];
            if (dxf.border) {
                warningManager.generateNotSupportedWarning(WarningTypes.CfFormatBorderNotSupported);
            }
            if (dxf.alignment) {
                warningManager.generateNotSupportedWarning(WarningTypes.CfFormatAlignmentNotSupported);
            }
            if (dxf.numFmt) {
                warningManager.generateNotSupportedWarning(WarningTypes.CfFormatNumFmtNotSupported);
            }
        }
    }

    // -------------------------------------
    //            CF HELPERS
    // -------------------------------------
    /**
     * Convert the conditional formatting o-spreadsheet operator to
     * the corresponding excel operator.
     * */
    function convertOperator(operator) {
        switch (operator) {
            case "IsNotEmpty":
                return "notContainsBlanks";
            case "IsEmpty":
                return "containsBlanks";
            case "NotContains":
                return "notContainsBlanks";
            default:
                return operator.charAt(0).toLowerCase() + operator.slice(1);
        }
    }
    // -------------------------------------
    //        WORKSHEET HELPERS
    // -------------------------------------
    function convertHeightToExcel(height) {
        return Math.round(HEIGHT_FACTOR * height * 100) / 100;
    }
    function convertWidthToExcel(width) {
        return Math.round(WIDTH_FACTOR * width * 100) / 100;
    }
    function convertHeightFromExcel(height) {
        if (!height)
            return height;
        return Math.round((height / HEIGHT_FACTOR) * 100) / 100;
    }
    function convertWidthFromExcel(width) {
        if (!width)
            return width;
        return Math.round((width / WIDTH_FACTOR) * 100) / 100;
    }
    function convertBorderDescr(descr) {
        if (!descr) {
            return undefined;
        }
        return {
            style: descr.style,
            color: { rgb: descr.color },
        };
    }
    function extractStyle(cell, data) {
        let style = {};
        if (cell.style) {
            style = data.styles[cell.style];
        }
        const format = extractFormat(cell, data);
        const exportedBorder = {};
        if (cell.border) {
            const border = data.borders[cell.border];
            exportedBorder.left = convertBorderDescr(border.left);
            exportedBorder.right = convertBorderDescr(border.right);
            exportedBorder.bottom = convertBorderDescr(border.bottom);
            exportedBorder.top = convertBorderDescr(border.top);
        }
        const styles = {
            font: {
                size: style?.fontSize || DEFAULT_FONT_SIZE,
                color: { rgb: style?.textColor ? style.textColor : "000000" },
                family: 2,
                name: "Arial",
            },
            fill: style?.fillColor
                ? {
                    fgColor: { rgb: style.fillColor },
                }
                : { reservedAttribute: "none" },
            numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,
            border: exportedBorder || {},
            alignment: {
                horizontal: style.align,
                vertical: style.verticalAlign
                    ? V_ALIGNMENT_EXPORT_CONVERSION_MAP[style.verticalAlign]
                    : undefined,
                wrapText: style.wrapping === "wrap",
            },
        };
        styles.font["strike"] = !!style?.strikethrough || undefined;
        styles.font["underline"] = !!style?.underline || undefined;
        styles.font["bold"] = !!style?.bold || undefined;
        styles.font["italic"] = !!style?.italic || undefined;
        return styles;
    }
    function extractFormat(cell, data) {
        if (cell.format) {
            return data.formats[cell.format];
        }
        return undefined;
    }
    function normalizeStyle(construct, styles) {
        const { id: fontId } = pushElement(styles["font"], construct.fonts);
        const { id: fillId } = pushElement(styles["fill"], construct.fills);
        const { id: borderId } = pushElement(styles["border"], construct.borders);
        // Normalize this
        const numFmtId = convertFormat(styles["numFmt"], construct.numFmts);
        const style = {
            fontId,
            fillId,
            borderId,
            numFmtId,
            alignment: {
                vertical: styles.alignment.vertical,
                horizontal: styles.alignment.horizontal,
                wrapText: styles.alignment.wrapText,
            },
        };
        const { id } = pushElement(style, construct.styles);
        return id;
    }
    function convertFormat(format, numFmtStructure) {
        if (!format) {
            return 0;
        }
        let formatId = XLSX_FORMAT_MAP[format.format];
        if (!formatId) {
            const { id } = pushElement(format, numFmtStructure);
            formatId = id + FIRST_NUMFMT_ID;
        }
        return formatId;
    }
    /**
     * Add a relation to the given file and return its id.
     */
    function addRelsToFile(relsFiles, path, rel) {
        let relsFile = relsFiles.find((file) => file.path === path);
        // the id is a one-based int casted as string
        let id;
        if (!relsFile) {
            id = "rId1";
            relsFiles.push({ path, rels: [{ ...rel, id }] });
        }
        else {
            id = `rId${(relsFile.rels.length + 1).toString()}`;
            relsFile.rels.push({
                ...rel,
                id,
            });
        }
        return id;
    }
    function pushElement(property, propertyList) {
        for (let [key, value] of Object.entries(propertyList)) {
            if (JSON.stringify(value) === JSON.stringify(property)) {
                return { id: parseInt(key, 10), list: propertyList };
            }
        }
        let elemId = propertyList.findIndex((elem) => JSON.stringify(elem) === JSON.stringify(property));
        if (elemId === -1) {
            propertyList.push(property);
            elemId = propertyList.length - 1;
        }
        return {
            id: elemId,
            list: propertyList,
        };
    }
    const chartIds = [];
    /**
     * Convert a chart o-spreadsheet id to a xlsx id which
     * are unsigned integers (starting from 1).
     */
    function convertChartId(chartId) {
        const xlsxId = chartIds.findIndex((id) => id === chartId);
        if (xlsxId === -1) {
            chartIds.push(chartId);
            return chartIds.length;
        }
        return xlsxId + 1;
    }
    const imageIds = [];
    /**
     * Convert a image o-spreadsheet id to a xlsx id which
     * are unsigned integers (starting from 1).
     */
    function convertImageId(imageId) {
        const xlsxId = imageIds.findIndex((id) => id === imageId);
        if (xlsxId === -1) {
            imageIds.push(imageId);
            return imageIds.length;
        }
        return xlsxId + 1;
    }
    /**
     * Convert a value expressed in dot to EMU.
     * EMU = English Metrical Unit
     * There are 914400 EMU per inch.
     *
     * /!\ A value expressed in EMU cannot be fractional.
     * See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement
     */
    function convertDotValueToEMU(value) {
        const DPI = 96;
        return Math.round((value * 914400) / DPI);
    }
    function getRangeSize(reference, defaultSheetIndex, data) {
        let xc = reference;
        let sheetName = undefined;
        ({ xc, sheetName } = splitReference(reference));
        let rangeSheetIndex;
        if (sheetName) {
            const index = data.sheets.findIndex((sheet) => sheet.name === sheetName);
            if (index < 0) {
                throw new Error("Unable to find a sheet with the name " + sheetName);
            }
            rangeSheetIndex = index;
        }
        else {
            rangeSheetIndex = Number(defaultSheetIndex);
        }
        const zone = toUnboundedZone(xc);
        if (zone.right === undefined) {
            zone.right = data.sheets[rangeSheetIndex].colNumber;
        }
        if (zone.bottom === undefined) {
            zone.bottom = data.sheets[rangeSheetIndex].rowNumber;
        }
        return (zone.right - zone.left + 1) * (zone.bottom - zone.top + 1);
    }
    function convertEMUToDotValue(value) {
        const DPI = 96;
        return Math.round((value * DPI) / 914400);
    }
    /**
     * Get the position of the start of a column in Excel (in px).
     */
    function getColPosition(colIndex, sheetData) {
        let position = 0;
        for (let i = 0; i < colIndex; i++) {
            const colAtIndex = sheetData.cols.find((col) => i >= col.min && i <= col.max);
            if (colAtIndex?.width) {
                position += colAtIndex.width;
            }
            else if (sheetData.sheetFormat?.defaultColWidth) {
                position += sheetData.sheetFormat.defaultColWidth;
            }
            else {
                position += EXCEL_DEFAULT_COL_WIDTH;
            }
        }
        return position / WIDTH_FACTOR;
    }
    /**
     * Get the position of the start of a row in Excel (in px).
     */
    function getRowPosition(rowIndex, sheetData) {
        let position = 0;
        for (let i = 0; i < rowIndex; i++) {
            const rowAtIndex = sheetData.rows[i];
            if (rowAtIndex?.height) {
                position += rowAtIndex.height;
            }
            else if (sheetData.sheetFormat?.defaultRowHeight) {
                position += sheetData.sheetFormat.defaultRowHeight;
            }
            else {
                position += EXCEL_DEFAULT_ROW_HEIGHT;
            }
        }
        return position / HEIGHT_FACTOR;
    }

    function convertFigures(sheetData) {
        let id = 1;
        return sheetData.figures
            .map((figure) => convertFigure(figure, (id++).toString(), sheetData))
            .filter(isDefined$1);
    }
    function convertFigure(figure, id, sheetData) {
        const x1 = getColPosition(figure.anchors[0].col, sheetData) +
            convertEMUToDotValue(figure.anchors[0].colOffset);
        const x2 = getColPosition(figure.anchors[1].col, sheetData) +
            convertEMUToDotValue(figure.anchors[1].colOffset);
        const y1 = getRowPosition(figure.anchors[0].row, sheetData) +
            convertEMUToDotValue(figure.anchors[0].rowOffset);
        const y2 = getRowPosition(figure.anchors[1].row, sheetData) +
            convertEMUToDotValue(figure.anchors[1].rowOffset);
        const figureData = { id, x: x1, y: y1 };
        if (isChartData(figure.data)) {
            return {
                ...figureData,
                width: x2 - x1,
                height: y2 - y1,
                tag: "chart",
                data: convertChartData(figure.data),
            };
        }
        else if (isImageData(figure.data)) {
            return {
                ...figureData,
                width: convertEMUToDotValue(figure.data.size.cx),
                height: convertEMUToDotValue(figure.data.size.cy),
                tag: "image",
                data: {
                    path: figure.data.imageSrc,
                    mimetype: figure.data.mimetype,
                },
            };
        }
        return undefined;
    }
    function isChartData(data) {
        return "dataSets" in data;
    }
    function isImageData(data) {
        return "imageSrc" in data;
    }
    function convertChartData(chartData) {
        const dataSetsHaveTitle = chartData.dataSets[0].label !== undefined;
        const labelRange = chartData.labelRange
            ? convertExcelRangeToSheetXC(chartData.labelRange, dataSetsHaveTitle)
            : undefined;
        let dataSets = chartData.dataSets.map((data) => convertExcelRangeToSheetXC(data.range, dataSetsHaveTitle));
        // For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset
        if (chartData.type === "pie") {
            dataSets.reverse();
        }
        return {
            dataSets,
            dataSetsHaveTitle,
            labelRange,
            title: chartData.title || "",
            type: chartData.type,
            background: convertColor({ rgb: chartData.backgroundColor }) || "#FFFFFF",
            verticalAxisPosition: chartData.verticalAxisPosition,
            legendPosition: chartData.legendPosition,
            stacked: chartData.stacked || false,
            aggregated: false,
            cumulative: chartData.cumulative || false,
            labelsAsText: false,
        };
    }
    function convertExcelRangeToSheetXC(range, dataSetsHaveTitle) {
        let { sheetName, xc } = splitReference(range);
        if (sheetName) {
            sheetName = getCanonicalSheetName(sheetName) + "!";
        }
        else {
            sheetName = "";
        }
        let zone = toUnboundedZone(xc);
        if (dataSetsHaveTitle && zone.bottom !== undefined && zone.right !== undefined) {
            const height = zone.bottom - zone.top + 1;
            const width = zone.right - zone.left + 1;
            if (height === 1) {
                zone = { ...zone, left: zone.left - 1 };
            }
            else if (width === 1) {
                zone = { ...zone, top: zone.top - 1 };
            }
        }
        const dataXC = zoneToXc(zone);
        return sheetName + dataXC;
    }

    /**
     * Match external reference (ex. '[1]Sheet 3'!$B$4)
     *
     * First match group is the external reference id
     * Second match group is the sheet id
     * Third match group is the reference of the cell
     */
    const externalReferenceRegex = new RegExp(/'?\[([0-9]*)\](.*)'?!(\$?[a-zA-Z]*\$?[0-9]*)/g);
    const subtotalRegex = new RegExp(/SUBTOTAL\(([0-9]*),/g);
    const cellRegex = new RegExp(cellReference.source, "ig");
    function convertFormulasContent(sheet, data) {
        const sfMap = getSharedFormulasMap(sheet);
        for (let cell of sheet.rows.map((row) => row.cells).flat()) {
            if (cell?.formula) {
                cell.formula.content =
                    cell.formula.sharedIndex !== undefined && !cell.formula.content
                        ? "=" + adaptFormula(cell.xc, sfMap[cell.formula.sharedIndex])
                        : "=" + cell.formula.content;
                cell.formula.content = convertFormula(cell.formula.content, data);
            }
        }
    }
    function getSharedFormulasMap(sheet) {
        const formulas = {};
        for (let row of sheet.rows) {
            for (let cell of row.cells) {
                if (cell.formula && cell.formula.sharedIndex !== undefined && cell.formula.content) {
                    formulas[cell.formula.sharedIndex] = { refCellXc: cell.xc, formula: cell.formula.content };
                }
            }
        }
        return formulas;
    }
    /**
     * Convert an XLSX formula into something we can evaluate.
     * - remove _xlfn. flags before function names
     * - convert the SUBTOTAL(index, formula) function to the function given by its index
     * - change #REF! into #REF
     * - convert external references into their value
     */
    function convertFormula(formula, data) {
        formula = formula.replace("_xlfn.", "");
        formula = formula.replace(/#REF!/g, "#REF");
        // SUBOTOTAL function, eg. =SUBTOTAL(3, {formula})
        formula = formula.replace(subtotalRegex, (match, functionId) => {
            const convertedFunction = SUBTOTAL_FUNCTION_CONVERSION_MAP[functionId];
            return convertedFunction ? convertedFunction + "(" : match;
        });
        // External references, eg. ='[1]Sheet 3'!$B$4
        formula = formula.replace(externalReferenceRegex, (match, externalRefId, sheetName, cellRef) => {
            externalRefId = Number(externalRefId) - 1;
            cellRef = cellRef.replace(/\$/g, "");
            const sheetIndex = data.externalBooks[externalRefId].sheetNames.findIndex((name) => name === sheetName);
            if (sheetIndex === -1) {
                return match;
            }
            const externalDataset = data.externalBooks[externalRefId].datasets.find((dataset) => dataset.sheetId === sheetIndex)?.data;
            if (!externalDataset) {
                return match;
            }
            const datasetValue = externalDataset && externalDataset[cellRef];
            const convertedValue = Number(datasetValue) ? datasetValue : `"${datasetValue}"`;
            return convertedValue || match;
        });
        return formula;
    }
    /**
     * Transform a shared formula for the given target.
     *
     * This will compute the offset between the original cell of the shared formula and the target cell,
     * then apply this offset to all the ranges in the formula (taking fixed references into account)
     */
    function adaptFormula(targetCell, sf) {
        const refPosition = toCartesian(sf.refCellXc);
        let newFormula = sf.formula.slice();
        let match;
        do {
            match = cellRegex.exec(newFormula);
            if (match) {
                const formulaPosition = toCartesian(match[0].replace("$", ""));
                const targetPosition = toCartesian(targetCell);
                const rangePart = {
                    colFixed: match[0].startsWith("$"),
                    rowFixed: match[0].includes("$", 1),
                };
                const offset = {
                    col: targetPosition.col - refPosition.col,
                    row: targetPosition.row - refPosition.row,
                };
                const offsettedPosition = {
                    col: rangePart.colFixed ? formulaPosition.col : formulaPosition.col + offset.col,
                    row: rangePart.rowFixed ? formulaPosition.row : formulaPosition.row + offset.row,
                };
                newFormula =
                    newFormula.slice(0, match.index) +
                        toXC(offsettedPosition.col, offsettedPosition.row, rangePart) +
                        newFormula.slice(match.index + match[0].length);
            }
        } while (match);
        return newFormula;
    }

    function convertSheets(data, warningManager) {
        return data.sheets.map((sheet) => {
            convertFormulasContent(sheet, data);
            const sheetDims = getSheetDims(sheet);
            const sheetOptions = sheet.sheetViews[0];
            return {
                id: sheet.sheetName,
                areGridLinesVisible: sheetOptions ? sheetOptions.showGridLines : true,
                name: sheet.sheetName,
                colNumber: sheetDims[0],
                rowNumber: sheetDims[1],
                cells: convertCells(sheet, data, sheetDims, warningManager),
                merges: sheet.merges,
                cols: convertCols(sheet, sheetDims[0]),
                rows: convertRows(sheet, sheetDims[1]),
                conditionalFormats: convertConditionalFormats(sheet.cfs, data.dxfs, warningManager),
                figures: convertFigures(sheet),
                isVisible: sheet.isVisible,
                panes: sheetOptions
                    ? { xSplit: sheetOptions.pane.xSplit, ySplit: sheetOptions.pane.ySplit }
                    : { xSplit: 0, ySplit: 0 },
                filterTables: [],
            };
        });
    }
    function convertCols(sheet, numberOfCols) {
        const cols = {};
        // Excel begins indexes at 1
        for (let i = 1; i < numberOfCols + 1; i++) {
            const col = sheet.cols.find((col) => col.min <= i && i <= col.max);
            let colSize;
            if (col && col.width)
                colSize = col.width;
            else if (sheet.sheetFormat?.defaultColWidth)
                colSize = sheet.sheetFormat.defaultColWidth;
            else
                colSize = EXCEL_DEFAULT_COL_WIDTH;
            cols[i - 1] = { size: convertWidthFromExcel(colSize), isHidden: col?.hidden };
        }
        return cols;
    }
    function convertRows(sheet, numberOfRows) {
        const rows = {};
        // Excel begins indexes at 1
        for (let i = 1; i < numberOfRows + 1; i++) {
            const row = sheet.rows.find((row) => row.index === i);
            let rowSize;
            if (row && row.height)
                rowSize = row.height;
            else if (sheet.sheetFormat?.defaultRowHeight)
                rowSize = sheet.sheetFormat.defaultRowHeight;
            else
                rowSize = EXCEL_DEFAULT_ROW_HEIGHT;
            rows[i - 1] = { size: convertHeightFromExcel(rowSize), isHidden: row?.hidden };
        }
        return rows;
    }
    /** Remove newlines (\n) in shared strings, We do not support them */
    function convertSharedStrings(xlsxSharedStrings) {
        return xlsxSharedStrings.map((str) => str.replace(/\n/g, ""));
    }
    function convertCells(sheet, data, sheetDims, warningManager) {
        const cells = {};
        const sharedStrings = convertSharedStrings(data.sharedStrings);
        const hyperlinkMap = sheet.hyperlinks.reduce((map, link) => {
            map[link.xc] = link;
            return map;
        }, {});
        for (let row of sheet.rows) {
            for (let cell of row.cells) {
                cells[cell.xc] = {
                    content: getCellValue(cell, hyperlinkMap, sharedStrings, warningManager),
                    // + 1 : our indexes for normalized values begin at 1 and not 0
                    style: cell.styleIndex ? cell.styleIndex + 1 : undefined,
                    border: cell.styleIndex ? data.styles[cell.styleIndex].borderId + 1 : undefined,
                    format: cell.styleIndex ? data.styles[cell.styleIndex].numFmtId + 1 : undefined,
                };
            }
        }
        // Apply row style
        for (let row of sheet.rows.filter((row) => row.styleIndex)) {
            for (let colIndex = 1; colIndex <= sheetDims[0]; colIndex++) {
                const xc = toXC(colIndex - 1, row.index - 1); // Excel indexes start at 1
                let cell = cells[xc];
                if (!cell) {
                    cell = {};
                    cells[xc] = cell;
                }
                cell.style = cell.style ? cell.style : row.styleIndex + 1;
                cell.border = cell.border ? cell.border : data.styles[row.styleIndex].borderId + 1;
                cell.format = cell.format ? cell.format : data.styles[row.styleIndex].numFmtId + 1;
            }
        }
        // Apply col style
        for (let col of sheet.cols.filter((col) => col.styleIndex)) {
            for (let colIndex = col.min; colIndex <= Math.min(col.max, sheetDims[0]); colIndex++) {
                for (let rowIndex = 1; rowIndex <= sheetDims[1]; rowIndex++) {
                    const xc = toXC(colIndex - 1, rowIndex - 1); // Excel indexes start at 1
                    let cell = cells[xc];
                    if (!cell) {
                        cell = {};
                        cells[xc] = cell;
                    }
                    cell.style = cell.style ? cell.style : col.styleIndex + 1;
                    cell.border = cell.border ? cell.border : data.styles[col.styleIndex].borderId + 1;
                    cell.format = cell.format ? cell.format : data.styles[col.styleIndex].numFmtId + 1;
                }
            }
        }
        return cells;
    }
    function getCellValue(cell, hyperLinksMap, sharedStrings, warningManager) {
        let cellValue;
        switch (cell.type) {
            case "sharedString":
                const ssIndex = parseInt(cell.value, 10);
                cellValue = sharedStrings[ssIndex];
                break;
            case "boolean":
                cellValue = Number(cell.value) ? "TRUE" : "FALSE";
                break;
            case "date": // I'm not sure where this is used rather than a number with a format
            case "error": // I don't think Excel really uses this
            case "inlineStr":
            case "number":
            case "str":
                cellValue = cell.value;
                break;
        }
        if (cellValue && hyperLinksMap[cell.xc]) {
            cellValue = convertHyperlink(hyperLinksMap[cell.xc], cellValue, warningManager);
        }
        if (cell.formula) {
            cellValue = cell.formula.content;
        }
        return cellValue;
    }
    function convertHyperlink(link, cellValue, warningManager) {
        const label = link.display || cellValue;
        if (!link.relTarget && !link.location) {
            warningManager.generateNotSupportedWarning(WarningTypes.BadlyFormattedHyperlink);
        }
        const url = link.relTarget
            ? link.relTarget
            : buildSheetLink(splitReference(link.location).sheetName);
        return markdownLink(label, url);
    }
    function getSheetDims(sheet) {
        const dims = [0, 0];
        for (let row of sheet.rows) {
            dims[0] = Math.max(dims[0], ...row.cells.map((cell) => toCartesian(cell.xc).col));
            dims[1] = Math.max(dims[1], row.index);
        }
        dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
        dims[1] = Math.max(dims[1], EXCEL_IMPORT_DEFAULT_NUMBER_OF_ROWS);
        return dims;
    }

    const TABLE_HEADER_STYLE = {
        fillColor: "#000000",
        textColor: "#ffffff",
        bold: true,
    };
    const TABLE_HIGHLIGHTED_CELL_STYLE = {
        bold: true,
    };
    const TABLE_BORDER_STYLE = { style: "thin", color: "#000000FF" };
    /**
     * Convert the imported XLSX tables.
     *
     * We will create a FilterTable if the imported table have filters, then apply a style in all the cells of the table
     * and convert the table-specific formula references into standard references.
     *
     * Change the converted data in-place.
     */
    function convertTables(convertedData, xlsxData) {
        for (const xlsxSheet of xlsxData.sheets) {
            for (const table of xlsxSheet.tables) {
                const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName);
                if (!sheet || !table.autoFilter)
                    continue;
                if (!sheet.filterTables)
                    sheet.filterTables = [];
                sheet.filterTables.push({ range: table.ref });
            }
        }
        applyTableStyle(convertedData, xlsxData);
        convertTableFormulaReferences(convertedData.sheets, xlsxData.sheets);
    }
    /**
     * Apply a style to all the cells that are in a table, and add the created styles in the  converted data.
     *
     * In XLSXs, the style of the cells of a table are not directly in the sheet, but rather deduced from the style of
     * the table that is defined in the table's XML file. The style of the table is a string referencing a standard style
     * defined in the OpenXML specifications. As there are 80+ different styles, we won't implement every one of them but
     * we will just define a style that will be used for all the imported tables.
     */
    function applyTableStyle(convertedData, xlsxData) {
        const styles = objectToArray(convertedData.styles);
        const borders = objectToArray(convertedData.borders);
        for (let xlsxSheet of xlsxData.sheets) {
            for (let table of xlsxSheet.tables) {
                const sheet = convertedData.sheets.find((sheet) => sheet.name === xlsxSheet.sheetName);
                if (!sheet)
                    continue;
                const tableZone = toZone(table.ref);
                // Table style
                for (let i = 0; i < table.headerRowCount; i++) {
                    applyStyleToZone(TABLE_HEADER_STYLE, { ...tableZone, bottom: tableZone.top + i }, sheet.cells, styles);
                }
                for (let i = 0; i < table.totalsRowCount; i++) {
                    applyStyleToZone(TABLE_HIGHLIGHTED_CELL_STYLE, { ...tableZone, top: tableZone.bottom - i }, sheet.cells, styles);
                }
                if (table.style?.showFirstColumn) {
                    applyStyleToZone(TABLE_HIGHLIGHTED_CELL_STYLE, { ...tableZone, right: tableZone.left }, sheet.cells, styles);
                }
                if (table.style?.showLastColumn) {
                    applyStyleToZone(TABLE_HIGHLIGHTED_CELL_STYLE, { ...tableZone, left: tableZone.right }, sheet.cells, styles);
                }
                // Table borders
                // Borders at : table outline + col(/row) if showColumnStripes(/showRowStripes) + border above totalRow
                for (let col = tableZone.left; col <= tableZone.right; col++) {
                    for (let row = tableZone.top; row <= tableZone.bottom; row++) {
                        const xc = toXC(col, row);
                        const cell = sheet.cells[xc];
                        const border = {
                            left: col === tableZone.left || table.style?.showColumnStripes
                                ? TABLE_BORDER_STYLE
                                : undefined,
                            right: col === tableZone.right ? TABLE_BORDER_STYLE : undefined,
                            top: row === tableZone.top ||
                                table.style?.showRowStripes ||
                                row > tableZone.bottom - table.totalsRowCount
                                ? TABLE_BORDER_STYLE
                                : undefined,
                            bottom: row === tableZone.bottom ? TABLE_BORDER_STYLE : undefined,
                        };
                        const newBorder = cell?.border ? { ...borders[cell.border], ...border } : border;
                        let borderIndex = borders.findIndex((border) => deepEquals(border, newBorder));
                        if (borderIndex === -1) {
                            borderIndex = borders.length;
                            borders.push(newBorder);
                        }
                        if (cell) {
                            cell.border = borderIndex;
                        }
                        else {
                            sheet.cells[xc] = { border: borderIndex };
                        }
                    }
                }
            }
        }
        convertedData.styles = arrayToObject(styles);
        convertedData.borders = arrayToObject(borders);
    }
    /**
     * Apply a style to all the cells in the zone. The applied style WILL NOT overwrite values in existing style of the cell.
     *
     * If a style that was not in the styles array was applied, push it into the style array.
     */
    function applyStyleToZone(appliedStyle, zone, cells, styles) {
        for (let col = zone.left; col <= zone.right; col++) {
            for (let row = zone.top; row <= zone.bottom; row++) {
                const xc = toXC(col, row);
                const cell = cells[xc];
                const newStyle = cell?.style ? { ...styles[cell.style], ...appliedStyle } : appliedStyle;
                let styleIndex = styles.findIndex((style) => deepEquals(style, newStyle));
                if (styleIndex === -1) {
                    styleIndex = styles.length;
                    styles.push(newStyle);
                }
                if (cell) {
                    cell.style = styleIndex;
                }
                else {
                    cells[xc] = { style: styleIndex };
                }
            }
        }
    }
    /**
     * In all the sheets, replace the table-only references in the formula cells with standard references.
     */
    function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
        for (let sheet of convertedSheets) {
            const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
            for (let table of tables) {
                const tabRef = table.name + "[";
                for (let position of positions(toZone(table.ref))) {
                    const xc = toXC(position.col, position.row);
                    const cell = sheet.cells[xc];
                    if (cell && cell.content && cell.content.startsWith("=")) {
                        let refIndex;
                        while ((refIndex = cell.content.indexOf(tabRef)) !== -1) {
                            let reference = cell.content.slice(refIndex + tabRef.length);
                            // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
                            let endIndex = reference.indexOf("]");
                            if (reference.startsWith(`[`)) {
                                endIndex = reference.indexOf("]", endIndex + 1);
                                endIndex = reference.indexOf("]", endIndex + 1);
                            }
                            reference = reference.slice(0, endIndex);
                            const convertedRef = convertTableReference(reference, table, xc);
                            cell.content =
                                cell.content.slice(0, refIndex) +
                                    convertedRef +
                                    cell.content.slice(tabRef.length + refIndex + endIndex + 1);
                        }
                    }
                }
            }
        }
    }
    /**
     * Convert table-specific references in formulas into standard references.
     *
     * A reference in a table can have the form (only the part between brackets should be given to this function):
     *  - tableName[colName] : reference to the whole column "colName"
     *  - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
     *
     * The available keywords are :
     * - #All : all the column (including totals)
     * - #Data : only the column data (no headers/totals)
     * - #Headers : only the header of the column
     * - #Totals : only the totals of the column
     * - #This Row : only the element in the same row as the cell
     */
    function convertTableReference(expr, table, cellXc) {
        const refElements = expr.split(",");
        const tableZone = toZone(table.ref);
        const refZone = { ...tableZone };
        let isReferencedZoneValid = true;
        // Single column reference
        if (refElements.length === 1) {
            const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
            refZone.left = refZone.right = colRelativeIndex + tableZone.left;
            if (table.headerRowCount) {
                refZone.top += table.headerRowCount;
            }
            if (table.totalsRowCount) {
                refZone.bottom -= 1;
            }
        }
        // Other references
        else {
            switch (refElements[0].slice(1, refElements[0].length - 1)) {
                case "#All":
                    refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
                    refZone.bottom = tableZone.bottom;
                    break;
                case "#Data":
                    refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
                    refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
                    break;
                case "#This Row":
                    refZone.top = refZone.bottom = toCartesian(cellXc).row;
                    break;
                case "#Headers":
                    refZone.top = refZone.bottom = tableZone.top;
                    if (!table.headerRowCount) {
                        isReferencedZoneValid = false;
                    }
                    break;
                case "#Totals":
                    refZone.top = refZone.bottom = tableZone.bottom;
                    if (!table.totalsRowCount) {
                        isReferencedZoneValid = false;
                    }
                    break;
            }
            const colRef = refElements[1].slice(1, refElements[1].length - 1);
            const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
            refZone.left = refZone.right = colRelativeIndex + tableZone.left;
        }
        if (!isReferencedZoneValid) {
            return INCORRECT_RANGE_STRING;
        }
        return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
    }

    // -------------------------------------
    //            XML HELPERS
    // -------------------------------------
    function createXMLFile(doc, path, contentType) {
        return {
            content: new XMLSerializer().serializeToString(doc),
            path,
            contentType,
        };
    }
    function xmlEscape(str) {
        return (String(str)
            .replace(/\&/g, "&amp;")
            .replace(/\</g, "&lt;")
            .replace(/\>/g, "&gt;")
            .replace(/\"/g, "&quot;")
            .replace(/\'/g, "&apos;")
            // Delete all ASCII control characters except for TAB (\x09), LF (\x0A) and CR (\x0D)
            // They are not valid at all in XML 1.0 (even escaped)
            .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ""));
    }
    function formatAttributes(attrs) {
        return new XMLString(attrs.map(([key, val]) => `${key}="${xmlEscape(val)}"`).join(" "));
    }
    function parseXML(xmlString, mimeType = "text/xml") {
        const document = new DOMParser().parseFromString(xmlString.toString(), mimeType);
        const parserError = document.querySelector("parsererror");
        if (parserError) {
            const errorString = parserError.innerHTML;
            const lineNumber = parseInt(errorString.split(":")[0], 10);
            const xmlStringArray = xmlString.toString().trim().split("\n");
            const xmlPreview = xmlStringArray
                .slice(Math.max(lineNumber - 3, 0), Math.min(lineNumber + 2, xmlStringArray.length))
                .join("\n");
            throw new Error(`XML string could not be parsed: ${errorString}\n${xmlPreview}`);
        }
        return document;
    }
    function getDefaultXLSXStructure() {
        return {
            relsFiles: [],
            sharedStrings: [],
            // default Values that will always be part of the style sheet
            styles: [
                {
                    fontId: 0,
                    fillId: 0,
                    numFmtId: 0,
                    borderId: 0,
                    alignment: { vertical: "bottom" },
                },
            ],
            fonts: [
                {
                    size: DEFAULT_FONT_SIZE,
                    family: 2,
                    color: { rgb: "000000" },
                    name: "Calibri",
                },
            ],
            fills: [{ reservedAttribute: "none" }, { reservedAttribute: "gray125" }],
            borders: [{}],
            numFmts: [],
            dxfs: [],
        };
    }
    function createOverride(partName, contentType) {
        return escapeXml /*xml*/ `
    <Override ContentType="${contentType}" PartName="${partName}" />
  `;
    }
    function createDefaultXMLElement(extension, contentType) {
        return escapeXml /*xml*/ `
    <Default Extension="${extension}" ContentType="${contentType}" />
  `;
    }
    function joinXmlNodes(xmlNodes) {
        return new XMLString(xmlNodes.join("\n"));
    }
    /**
     * Escape interpolated values except if the value is already
     * a properly escaped XML string.
     *
     * ```
     * escapeXml`<t>${"This will be escaped"}</t>`
     * ```
     */
    function escapeXml(strings, ...expressions) {
        let str = [strings[0]];
        for (let i = 0; i < expressions.length; i++) {
            const value = expressions[i] instanceof XMLString ? expressions[i] : xmlEscape(expressions[i]);
            str.push(value + strings[i + 1]);
        }
        return new XMLString(concat(str));
    }
    /**
     * Removes the escaped namespace of all the xml tags in the string.
     *
     * Eg. : "NAMESPACEnsNAMESPACEtest a" => "test a"
     */
    function removeTagEscapedNamespaces(tag) {
        return tag.replace(/NAMESPACE.*NAMESPACE(.*)/, "$1");
    }
    /**
     * Encase the namespaces in the element's tags with NAMESPACE string
     *
     * e.g. <x:foo> becomes <NAMESPACExNAMESPACEFoo>
     *
     * That's useful because namespaces aren't supported by the HTML specification, so it's arbitrary whether a HTML parser/querySelector
     * implementation will support namespaces in the tags or not.
     */
    function escapeTagNamespaces(str) {
        return str.replaceAll(/(<\/?)([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "$1" + "NAMESPACE" + "$2" + "NAMESPACE" + "$3");
    }
    function escapeQueryNameSpaces(query) {
        return query.replaceAll(/([a-zA-Z0-9]+):([a-zA-Z0-9]+)/g, "NAMESPACE" + "$1" + "NAMESPACE" + "$2");
    }

    class AttributeValue {
        value;
        constructor(value) {
            this.value = value;
        }
        asString() {
            return fixXlsxUnicode(String(this.value));
        }
        asBool() {
            if (this.value === "true")
                return true; // for files exported from Libre Office
            if (this.value === "false")
                return false;
            return Boolean(Number(this.value));
        }
        asNum() {
            return Number(this.value);
        }
    }
    class XlsxBaseExtractor {
        rootFile;
        xlsxFileStructure;
        warningManager;
        relationships;
        // The xml file we are currently parsing. We should have one Extractor class by XLSXImportFile, but
        // the XLSXImportFile contains both the main .xml file, and the .rels file
        currentFile = undefined;
        /**
         * /!\ Important : There should be no namespaces in the tags of the XML files.
         *
         * This class use native querySelector and querySelectorAll, that's used for HTML (not XML). These aren't supposed to
         * handled namespaces, as they are not supported by the HTML specification. Some implementations (most browsers) do
         * actually support namespaces, but some don't (e.g. jsdom).
         *
         * The namespace should be escaped as with NAMESPACE string (eg. <t:foo> => <NAMESPACEtNAMESPACEfoo>).
         */
        constructor(rootFile, xlsxStructure, warningManager) {
            this.rootFile = rootFile;
            this.currentFile = rootFile.file.fileName;
            this.xlsxFileStructure = xlsxStructure;
            this.warningManager = warningManager;
            this.relationships = {};
            if (rootFile.rels) {
                this.extractRelationships(rootFile.rels).map((rel) => {
                    this.relationships[rel.id] = rel;
                });
            }
        }
        /**
         * Extract all the relationships inside a .xml.rels file
         */
        extractRelationships(relFile) {
            return this.mapOnElements({ parent: relFile.xml, query: "Relationship" }, (relationshipElement) => {
                return {
                    id: this.extractAttr(relationshipElement, "Id", { required: true }).asString(),
                    target: this.extractAttr(relationshipElement, "Target", { required: true }).asString(),
                    type: this.extractAttr(relationshipElement, "Type", { required: true }).asString(),
                };
            });
        }
        /**
         * Get the list of all the XLSX files in the XLSX file structure
         */
        getListOfXMLFiles() {
            const XMLFiles = Object.entries(this.xlsxFileStructure)
                .filter(([key]) => key !== "images")
                .map(([_, value]) => value)
                .flat()
                .filter(isDefined$1);
            return XMLFiles;
        }
        /**
         * Return an array containing the return value of the given function applied to all the XML elements
         * found using the MapOnElementArgs.
         *
         * The arguments contains :
         *  - query : a QuerySelector string to find the elements to apply the function to
         *  - parent : an XML element or XML document in which to find the queried elements
         *  - children : if true, the function is applied on the direct children of the queried element
         *
         * This method will also handle the errors thrown in the argument function.
         */
        mapOnElements(args, fct) {
            const ret = [];
            const oldWorkingDocument = this.currentFile;
            let elements;
            if (args.children) {
                const children = this.querySelector(args.parent, args.query)?.children;
                elements = children ? children : [];
            }
            else {
                elements = this.querySelectorAll(args.parent, args.query);
            }
            if (elements) {
                for (let element of elements) {
                    try {
                        ret.push(fct(element));
                    }
                    catch (e) {
                        this.catchErrorOnElement(e, element);
                    }
                }
            }
            this.currentFile = oldWorkingDocument;
            return ret;
        }
        /**
         * Log an error caught when parsing an element in the warningManager.
         */
        catchErrorOnElement(error, onElement) {
            const errorMsg = onElement
                ? `Error when parsing an element <${onElement.tagName}> of file ${this.currentFile}, skip this element. \n${error.stack}`
                : `Error when parsing file ${this.currentFile}.`;
            this.warningManager.addParsingWarning([errorMsg, error.message].join("\n"));
        }
        /**
         * Extract an attribute from an Element.
         *
         * If the attribute is required but was not found, will add a warning in the warningManager if it was given a default
         * value, and throw an error if no default value was given.
         *
         * Can only return undefined value for non-required attributes without default value.
         */
        extractAttr(e, attName, optionalArgs) {
            const attribute = e.attributes[attName];
            if (!attribute)
                this.handleMissingValue(e, `attribute "${attName}"`, optionalArgs);
            const value = attribute?.value ? attribute.value : optionalArgs?.default;
            return (value === undefined ? undefined : new AttributeValue(value));
        }
        /**
         * Extract the text content of an Element.
         *
         * If the text content is required but was not found, will add a warning in the warningManager if it was given a default
         * value, and throw an error if no default value was given.
         *
         * Can only return undefined value for non-required text content without default value.
         */
        extractTextContent(element, optionalArgs) {
            if (optionalArgs?.default !== undefined && typeof optionalArgs.default !== "string") {
                throw new Error("extractTextContent default value should be a string");
            }
            const shouldPreserveSpaces = element?.attributes["xml:space"]?.value === "preserve";
            let textContent = element?.textContent;
            if (!element || textContent === null) {
                this.handleMissingValue(element, `text content`, optionalArgs);
            }
            if (textContent) {
                textContent = shouldPreserveSpaces ? textContent : textContent.trim();
            }
            return (textContent ? fixXlsxUnicode(textContent) : optionalArgs?.default);
        }
        /**
         * Extract an attribute of a child of the given element.
         *
         * The reference of a child can be a string (tag of the child) or an number (index in the list of children of the element)
         *
         * If the attribute is required but either the attribute or the referenced child element was not found, it will
         * will add a warning in the warningManager if it was given a default value, and throw an error if no default value was given.
         *
         * Can only return undefined value for non-required attributes without default value.
         */
        extractChildAttr(e, childRef, attName, optionalArgs) {
            let child;
            if (typeof childRef === "number") {
                child = e.children[childRef];
            }
            else {
                child = this.querySelector(e, childRef);
            }
            if (!child) {
                this.handleMissingValue(e, typeof childRef === "number" ? `child at index ${childRef}` : `child <${childRef}>`, optionalArgs);
            }
            const value = child
                ? this.extractAttr(child, attName, optionalArgs)?.asString()
                : optionalArgs?.default;
            return (value !== undefined ? new AttributeValue(value) : undefined);
        }
        /**
         * Extract the text content of a child of the given element.
         *
         * If the text content is required but either the text content or the referenced child element was not found, it will
         * will add a warning in the warningManager if it was given a default value, and throw an error if no default value was given.
         *
         * Can only return undefined value for non-required text content without default value.
         */
        extractChildTextContent(e, childRef, optionalArgs) {
            if (optionalArgs?.default !== undefined && typeof optionalArgs.default !== "string") {
                throw new Error("extractTextContent default value should be a string");
            }
            let child = this.querySelector(e, childRef);
            if (!child) {
                this.handleMissingValue(e, `child <${childRef}>`, optionalArgs);
            }
            return (child ? this.extractTextContent(child, optionalArgs) : optionalArgs?.default);
        }
        /**
         * Should be called if a extractAttr/extractTextContent doesn't find the element it needs to extract.
         *
         * If the extractable was required, this function will add a warning in the warningManager if there was a default value,
         * and throw an error if no default value was given.
         */
        handleMissingValue(parentElement, missingElementName, optionalArgs) {
            if (optionalArgs?.required) {
                if (optionalArgs?.default) {
                    this.warningManager.addParsingWarning(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, replacing it by the default value ${optionalArgs.default}`);
                }
                else {
                    throw new Error(`Missing required ${missingElementName} in element <${parentElement.tagName}> of ${this.currentFile}, and no default value was set`);
                }
            }
        }
        /**
         * Extract a color, extracting it from the theme if needed.
         *
         * Will throw an error if the element references a theme, but no theme was provided or the theme it doesn't contain the color.
         */
        extractColor(colorElement, theme, defaultColor) {
            if (!colorElement) {
                return defaultColor ? { rgb: defaultColor } : undefined;
            }
            const themeIndex = this.extractAttr(colorElement, "theme")?.asString();
            let rgb;
            if (themeIndex !== undefined) {
                if (!theme || !theme.clrScheme) {
                    throw new Error("Color referencing a theme but no theme was provided");
                }
                rgb = this.getThemeColor(themeIndex, theme.clrScheme);
            }
            else {
                rgb = this.extractAttr(colorElement, "rgb")?.asString();
            }
            const color = {
                rgb,
                auto: this.extractAttr(colorElement, "auto")?.asBool(),
                indexed: this.extractAttr(colorElement, "indexed")?.asNum(),
                tint: this.extractAttr(colorElement, "tint")?.asNum(),
            };
            return color;
        }
        /**
         * Returns the xml file targeted by a relationship.
         */
        getTargetXmlFile(relationship) {
            if (!relationship)
                throw new Error("Undefined target file");
            const target = this.processRelationshipTargetName(relationship.target);
            // Use "endsWith" because targets are relative paths, and we know the files by their absolute path.
            const f = this.getListOfXMLFiles().find((f) => f.file.fileName.endsWith(target));
            if (!f || !f.file)
                throw new Error("Cannot find target file");
            return f;
        }
        /**
         * Returns the image parameters targeted by a relationship.
         */
        getTargetImageFile(relationship) {
            if (!relationship)
                throw new Error("Undefined target file");
            const target = this.processRelationshipTargetName(relationship.target);
            // Use "endsWith" because targets are relative paths, and we know the files by their absolute path.
            const f = this.xlsxFileStructure.images.find((f) => f.fileName.endsWith(target));
            if (!f)
                throw new Error("Cannot find target file");
            return f;
        }
        querySelector(element, query) {
            const escapedQuery = escapeQueryNameSpaces(query);
            return element.querySelector(escapedQuery);
        }
        querySelectorAll(element, query) {
            const escapedQuery = escapeQueryNameSpaces(query);
            return element.querySelectorAll(escapedQuery);
        }
        /**
         * Get a color from its id in the Theme's colorScheme.
         *
         * Note that Excel don't use the colors from the theme but from its own internal theme, so the displayed
         * colors will be different in the import than in excel.
         * .
         */
        getThemeColor(colorId, clrScheme) {
            switch (colorId) {
                case "0": // 0 : sysColor window text
                    return "FFFFFF";
                case "1": // 1 : sysColor window background
                    return "000000";
                // Don't ask me why these 2 are inverted, I cannot find any documentation for it but everyone does it
                case "2":
                    return clrScheme["3"].value;
                case "3":
                    return clrScheme["2"].value;
                default:
                    return clrScheme[colorId].value;
            }
        }
        /** Remove signs of relative path. */
        processRelationshipTargetName(targetName) {
            return targetName.replace(/\.+\//, "");
        }
    }

    /**
     * XLSX Extractor class that can be used for either sharedString XML files or theme XML files.
     *
     * Since they both are quite simple, it make sense to make a single class to manage them all, to avoid unnecessary file
     * cluttering.
     */
    class XlsxMiscExtractor extends XlsxBaseExtractor {
        getTheme() {
            const clrScheme = this.mapOnElements({ query: "a:clrScheme", parent: this.rootFile.file.xml, children: true }, (element) => {
                return {
                    name: element.tagName,
                    value: this.extractChildAttr(element, 0, "val", {
                        required: true,
                        default: AUTO_COLOR,
                    }).asString(),
                    lastClr: this.extractChildAttr(element, 0, "lastClr", {
                        default: AUTO_COLOR,
                    }).asString(),
                };
            });
            return { clrScheme };
        }
        /**
         * Get the array of shared strings of the XLSX.
         *
         * Worth noting that running a prettier on the xml can mess up some strings, since there is an option in the
         * xmls to keep the spacing and not trim the string.
         */
        getSharedStrings() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "si" }, (ssElement) => {
                // Shared string can either be a simple text, or a rich text (text with formatting, possibly in multiple parts)
                if (ssElement.children[0].tagName === "t") {
                    return this.extractTextContent(ssElement) || "";
                }
                // We don't support rich text formatting, we'll only extract the text
                else {
                    return this.mapOnElements({ parent: ssElement, query: "t" }, (textElement) => {
                        return this.extractTextContent(textElement) || "";
                    }).join("");
                }
            });
        }
    }

    class XlsxCfExtractor extends XlsxBaseExtractor {
        theme;
        constructor(sheetFile, xlsxStructure, warningManager, theme) {
            super(sheetFile, xlsxStructure, warningManager);
            this.theme = theme;
        }
        extractConditionalFormattings() {
            const cfs = this.mapOnElements({ parent: this.rootFile.file.xml, query: "worksheet > conditionalFormatting" }, (cfElement) => {
                return {
                    // sqref = ranges on which the cf applies, separated by spaces
                    sqref: this.extractAttr(cfElement, "sqref", { required: true }).asString().split(" "),
                    pivot: this.extractAttr(cfElement, "pivot")?.asBool(),
                    cfRules: this.extractCFRules(cfElement, this.theme),
                };
            });
            // XLSX extension to OpenXml
            cfs.push(...this.mapOnElements({ parent: this.rootFile.file.xml, query: "extLst x14:conditionalFormatting" }, (cfElement) => {
                return {
                    sqref: this.extractChildTextContent(cfElement, "xm:sqref", { required: true }).split(" "),
                    pivot: this.extractAttr(cfElement, "xm:pivot")?.asBool(),
                    cfRules: this.extractCFRules(cfElement, this.theme),
                };
            }));
            return cfs;
        }
        extractCFRules(cfElement, theme) {
            return this.mapOnElements({ parent: cfElement, query: "cfRule, x14:cfRule" }, (cfRuleElement) => {
                const cfType = this.extractAttr(cfRuleElement, "type", {
                    required: true,
                }).asString();
                if (cfType === "dataBar") {
                    // Databars are an extension to OpenXml and have a different format (XLSX §2.6.30). Do'nt bother
                    // extracting them as we don't support them.
                    throw new Error("Databars conditional formats are not supported.");
                }
                return {
                    type: cfType,
                    priority: this.extractAttr(cfRuleElement, "priority", { required: true }).asNum(),
                    colorScale: this.extractCfColorScale(cfRuleElement, theme),
                    formula: this.extractCfFormula(cfRuleElement),
                    iconSet: this.extractCfIconSet(cfRuleElement),
                    dxfId: this.extractAttr(cfRuleElement, "dxfId")?.asNum(),
                    stopIfTrue: this.extractAttr(cfRuleElement, "stopIfTrue")?.asBool(),
                    aboveAverage: this.extractAttr(cfRuleElement, "aboveAverage")?.asBool(),
                    percent: this.extractAttr(cfRuleElement, "percent")?.asBool(),
                    bottom: this.extractAttr(cfRuleElement, "bottom")?.asBool(),
                    operator: this.extractAttr(cfRuleElement, "operator")?.asString(),
                    text: this.extractAttr(cfRuleElement, "text")?.asString(),
                    timePeriod: this.extractAttr(cfRuleElement, "timePeriod")?.asString(),
                    rank: this.extractAttr(cfRuleElement, "rank")?.asNum(),
                    stdDev: this.extractAttr(cfRuleElement, "stdDev")?.asNum(),
                    equalAverage: this.extractAttr(cfRuleElement, "equalAverage")?.asBool(),
                };
            });
        }
        extractCfFormula(cfRulesElement) {
            return this.mapOnElements({ parent: cfRulesElement, query: "formula" }, (cfFormulaElements) => {
                return this.extractTextContent(cfFormulaElements, { required: true });
            });
        }
        extractCfColorScale(cfRulesElement, theme) {
            const colorScaleElement = this.querySelector(cfRulesElement, "colorScale");
            if (!colorScaleElement)
                return undefined;
            return {
                colors: this.mapOnElements({ parent: colorScaleElement, query: "color" }, (colorElement) => {
                    return this.extractColor(colorElement, theme, "ffffff");
                }),
                cfvos: this.extractCFVos(colorScaleElement),
            };
        }
        extractCfIconSet(cfRulesElement) {
            const iconSetElement = this.querySelector(cfRulesElement, "iconSet, x14:iconSet");
            if (!iconSetElement)
                return undefined;
            return {
                iconSet: this.extractAttr(iconSetElement, "iconSet", {
                    default: "3TrafficLights1",
                }).asString(),
                showValue: this.extractAttr(iconSetElement, "showValue", { default: true }).asBool(),
                percent: this.extractAttr(iconSetElement, "percent", { default: true }).asBool(),
                reverse: this.extractAttr(iconSetElement, "reverse")?.asBool(),
                custom: this.extractAttr(iconSetElement, "custom")?.asBool(),
                cfvos: this.extractCFVos(iconSetElement),
                cfIcons: this.extractCfIcons(iconSetElement),
            };
        }
        extractCfIcons(iconSetElement) {
            const icons = this.mapOnElements({ parent: iconSetElement, query: "cfIcon, x14:cfIcon" }, (cfIconElement) => {
                return {
                    iconSet: this.extractAttr(cfIconElement, "iconSet", {
                        required: true,
                    }).asString(),
                    iconId: this.extractAttr(cfIconElement, "iconId", { required: true }).asNum(),
                };
            });
            return icons.length === 0 ? undefined : icons;
        }
        extractCFVos(parent) {
            return this.mapOnElements({ parent, query: "cfvo, x14:cfvo" }, (cfVoElement) => {
                return {
                    type: this.extractAttr(cfVoElement, "type", {
                        required: true,
                    }).asString(),
                    gte: this.extractAttr(cfVoElement, "gte", { default: true })?.asBool(),
                    value: cfVoElement.attributes["val"]
                        ? this.extractAttr(cfVoElement, "val")?.asString()
                        : this.extractChildTextContent(cfVoElement, "f, xm:f"),
                };
            });
        }
    }

    class XlsxChartExtractor extends XlsxBaseExtractor {
        extractChart() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "c:chartSpace" }, (rootChartElement) => {
                const chartType = this.getChartType(rootChartElement);
                if (!CHART_TYPE_CONVERSION_MAP[chartType]) {
                    throw new Error(`Unsupported chart type ${chartType}`);
                }
                // Title can be separated into multiple xml elements (for styling and such), we only import the text
                const chartTitle = this.mapOnElements({ parent: rootChartElement, query: "c:title a:t" }, (textElement) => {
                    return textElement.textContent || "";
                }).join("");
                const barChartGrouping = this.extractChildAttr(rootChartElement, "c:grouping", "val", {
                    default: "clustered",
                }).asString();
                return {
                    title: chartTitle,
                    type: CHART_TYPE_CONVERSION_MAP[chartType],
                    dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`)),
                    labelRange: this.extractChildTextContent(rootChartElement, "c:ser c:cat c:f"),
                    backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
                        default: "ffffff",
                    }).asString(),
                    verticalAxisPosition: this.extractChildAttr(rootChartElement, "c:valAx > c:axPos", "val", {
                        default: "l",
                    }).asString() === "r"
                        ? "right"
                        : "left",
                    legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(rootChartElement, "c:legendPos", "val", {
                        default: "b",
                    }).asString()],
                    stacked: barChartGrouping === "stacked",
                    fontColor: "000000",
                };
            })[0];
        }
        extractChartDatasets(chartElement) {
            return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
                return {
                    label: this.extractChildTextContent(chartDataElement, "c:tx c:f"),
                    range: this.extractChildTextContent(chartDataElement, "c:val c:f", { required: true }),
                };
            });
        }
        /**
         * The chart type in the XML isn't explicitly defined, but there is an XML element that define the
         * chart, and this element tag name tells us which type of chart it is. We just need to find this XML element.
         */
        getChartType(chartElement) {
            const plotAreaElement = this.querySelector(chartElement, "c:plotArea");
            if (!plotAreaElement) {
                throw new Error("Missing plot area in the chart definition.");
            }
            for (let child of plotAreaElement.children) {
                const tag = removeTagEscapedNamespaces(child.tagName);
                if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
                    return tag;
                }
            }
            throw new Error("Unknown chart type");
        }
    }

    class XlsxFigureExtractor extends XlsxBaseExtractor {
        extractFigures() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "xdr:wsDr", children: true }, (figureElement) => {
                const anchorType = removeTagEscapedNamespaces(figureElement.tagName);
                if (anchorType !== "twoCellAnchor") {
                    throw new Error("Only twoCellAnchor are supported for xlsx drawings.");
                }
                const chartElement = this.querySelector(figureElement, "c:chart");
                const imageElement = this.querySelector(figureElement, "a:blip");
                if (!chartElement && !imageElement) {
                    throw new Error("Only chart and image figures are currently supported.");
                }
                return {
                    anchors: [
                        this.extractFigureAnchor("xdr:from", figureElement),
                        this.extractFigureAnchor("xdr:to", figureElement),
                    ],
                    data: chartElement ? this.extractChart(chartElement) : this.extractImage(figureElement),
                };
            });
        }
        extractFigureAnchor(anchorTag, figureElement) {
            const anchor = this.querySelector(figureElement, anchorTag);
            if (!anchor) {
                throw new Error(`Missing anchor element ${anchorTag}`);
            }
            return {
                col: Number(this.extractChildTextContent(anchor, "xdr:col", { required: true })),
                colOffset: Number(this.extractChildTextContent(anchor, "xdr:colOff", { required: true })),
                row: Number(this.extractChildTextContent(anchor, "xdr:row", { required: true })),
                rowOffset: Number(this.extractChildTextContent(anchor, "xdr:rowOff", { required: true })),
            };
        }
        extractChart(chartElement) {
            const chartId = this.extractAttr(chartElement, "r:id", { required: true }).asString();
            const chartFile = this.getTargetXmlFile(this.relationships[chartId]);
            const chartDefinition = new XlsxChartExtractor(chartFile, this.xlsxFileStructure, this.warningManager).extractChart();
            if (!chartDefinition) {
                throw new Error("Unable to extract chart definition");
            }
            return chartDefinition;
        }
        extractImage(figureElement) {
            const imageElement = this.querySelector(figureElement, "a:blip");
            const imageId = this.extractAttr(imageElement, "r:embed", { required: true }).asString();
            const image = this.getTargetImageFile(this.relationships[imageId]);
            if (!image) {
                throw new Error("Unable to extract image");
            }
            const shapePropertyElement = this.querySelector(figureElement, "a:xfrm");
            const extension = image.fileName.split(".").at(-1);
            return {
                imageSrc: image.imageSrc,
                mimetype: extension ? IMAGE_EXTENSION_TO_MIMETYPE_MAPPING[extension] : undefined,
                size: {
                    cx: this.extractChildAttr(shapePropertyElement, "a:ext", "cx", { required: true }).asNum(),
                    cy: this.extractChildAttr(shapePropertyElement, "a:ext", "cy", { required: true }).asNum(),
                },
            };
        }
    }

    /**
     * We don't really support pivot tables, we'll just extract them as Tables.
     */
    class XlsxPivotExtractor extends XlsxBaseExtractor {
        getPivotTable() {
            return this.mapOnElements(
            // Use :root instead of "pivotTableDefinition" because others pivotTableDefinition elements are present inside the root
            // pivotTableDefinition elements.
            { query: ":root", parent: this.rootFile.file.xml }, (pivotElement) => {
                return {
                    displayName: this.extractAttr(pivotElement, "name", { required: true }).asString(),
                    id: this.extractAttr(pivotElement, "name", { required: true }).asString(),
                    ref: this.extractChildAttr(pivotElement, "location", "ref", {
                        required: true,
                    }).asString(),
                    headerRowCount: this.extractChildAttr(pivotElement, "location", "firstDataRow", {
                        default: 0,
                    }).asNum(),
                    totalsRowCount: 1,
                    cols: [],
                    style: {
                        showFirstColumn: true,
                        showRowStripes: true,
                    },
                };
            })[0];
        }
    }

    class XlsxTableExtractor extends XlsxBaseExtractor {
        getTable() {
            return this.mapOnElements({ query: "table", parent: this.rootFile.file.xml }, (tableElement) => {
                return {
                    displayName: this.extractAttr(tableElement, "displayName", {
                        required: true,
                    }).asString(),
                    name: this.extractAttr(tableElement, "name")?.asString(),
                    id: this.extractAttr(tableElement, "id", { required: true }).asString(),
                    ref: this.extractAttr(tableElement, "ref", { required: true }).asString(),
                    headerRowCount: this.extractAttr(tableElement, "headerRowCount", {
                        default: 1,
                    }).asNum(),
                    totalsRowCount: this.extractAttr(tableElement, "totalsRowCount", {
                        default: 0,
                    }).asNum(),
                    cols: this.extractTableCols(tableElement),
                    style: this.extractTableStyleInfo(tableElement),
                    autoFilter: this.extractTableAutoFilter(tableElement),
                };
            })[0];
        }
        extractTableCols(tableElement) {
            return this.mapOnElements({ query: "tableColumn", parent: tableElement }, (tableColElement) => {
                return {
                    id: this.extractAttr(tableColElement, "id", { required: true }).asString(),
                    name: this.extractAttr(tableColElement, "name", { required: true }).asString(),
                    colFormula: this.extractChildTextContent(tableColElement, "calculatedColumnFormula"),
                };
            });
        }
        extractTableStyleInfo(tableElement) {
            return this.mapOnElements({ query: "tableStyleInfo", parent: tableElement }, (tableStyleElement) => {
                return {
                    name: this.extractAttr(tableStyleElement, "name")?.asString(),
                    showFirstColumn: this.extractAttr(tableStyleElement, "showFirstColumn")?.asBool(),
                    showLastColumn: this.extractAttr(tableStyleElement, "showLastColumn")?.asBool(),
                    showRowStripes: this.extractAttr(tableStyleElement, "showRowStripes")?.asBool(),
                    showColumnStripes: this.extractAttr(tableStyleElement, "showColumnStripes")?.asBool(),
                };
            })[0];
        }
        extractTableAutoFilter(tableElement) {
            return this.mapOnElements({ query: "autoFilter", parent: tableElement }, (autoFilterElement) => {
                return {
                    columns: this.extractFilterColumns(autoFilterElement),
                    zone: this.extractAttr(autoFilterElement, "ref", { required: true }).asString(),
                };
            })[0];
        }
        extractFilterColumns(autoFilterElement) {
            return this.mapOnElements({ query: "tableColumn", parent: autoFilterElement }, (filterColumnElement) => {
                return {
                    colId: this.extractAttr(autoFilterElement, "colId", { required: true }).asNum(),
                    hiddenButton: this.extractAttr(autoFilterElement, "hiddenButton", {
                        default: false,
                    }).asBool(),
                    filters: this.extractSimpleFilter(filterColumnElement),
                };
            });
        }
        extractSimpleFilter(filterColumnElement) {
            return this.mapOnElements({ query: "filter", parent: filterColumnElement }, (filterColumnElement) => {
                return {
                    val: this.extractAttr(filterColumnElement, "val", { required: true }).asString(),
                };
            });
        }
    }

    class XlsxSheetExtractor extends XlsxBaseExtractor {
        theme;
        constructor(sheetFile, xlsxStructure, warningManager, theme) {
            super(sheetFile, xlsxStructure, warningManager);
            this.theme = theme;
        }
        getSheet() {
            return this.mapOnElements({ query: "worksheet", parent: this.rootFile.file.xml }, (sheetElement) => {
                const sheetWorkbookInfo = this.getSheetWorkbookInfo();
                return {
                    sheetName: this.extractSheetName(),
                    sheetViews: this.extractSheetViews(sheetElement),
                    sheetFormat: this.extractSheetFormat(sheetElement),
                    cols: this.extractCols(sheetElement),
                    rows: this.extractRows(sheetElement),
                    sharedFormulas: this.extractSharedFormulas(sheetElement),
                    merges: this.extractMerges(sheetElement),
                    cfs: this.extractConditionalFormats(),
                    figures: this.extractFigures(sheetElement),
                    hyperlinks: this.extractHyperLinks(sheetElement),
                    tables: [...this.extractTables(sheetElement), ...this.extractPivotTables()],
                    isVisible: sheetWorkbookInfo.state === "visible" ? true : false,
                };
            })[0];
        }
        extractSheetViews(worksheet) {
            return this.mapOnElements({ parent: worksheet, query: "sheetView" }, (sheetViewElement) => {
                const paneElement = this.querySelector(sheetViewElement, "pane");
                return {
                    tabSelected: this.extractAttr(sheetViewElement, "tabSelected", {
                        default: false,
                    }).asBool(),
                    showFormulas: this.extractAttr(sheetViewElement, "showFormulas", {
                        default: false,
                    }).asBool(),
                    showGridLines: this.extractAttr(sheetViewElement, "showGridLines", {
                        default: true,
                    }).asBool(),
                    showRowColHeaders: this.extractAttr(sheetViewElement, "showRowColHeaders", {
                        default: true,
                    }).asBool(),
                    pane: {
                        xSplit: paneElement
                            ? this.extractAttr(paneElement, "xSplit", { default: 0 }).asNum()
                            : 0,
                        ySplit: paneElement
                            ? this.extractAttr(paneElement, "ySplit", { default: 0 }).asNum()
                            : 0,
                    },
                };
            });
        }
        extractSheetName() {
            const relativePath = getRelativePath(this.xlsxFileStructure.workbook.file.fileName, this.rootFile.file.fileName);
            const workbookRels = this.extractRelationships(this.xlsxFileStructure.workbook.rels);
            const relId = workbookRels.find((rel) => rel.target === relativePath).id;
            // Having a namespace in the attributes names mess with the querySelector, and the behavior is not the same
            // for every XML parser. So we'll search manually instead of using a querySelector to search for an attribute value.
            for (let sheetElement of this.querySelectorAll(this.xlsxFileStructure.workbook.file.xml, "sheet")) {
                if (sheetElement.attributes["r:id"].value === relId) {
                    return sheetElement.attributes["name"].value;
                }
            }
            throw new Error("Missing sheet name");
        }
        getSheetWorkbookInfo() {
            const relativePath = getRelativePath(this.xlsxFileStructure.workbook.file.fileName, this.rootFile.file.fileName);
            const workbookRels = this.extractRelationships(this.xlsxFileStructure.workbook.rels);
            const relId = workbookRels.find((rel) => rel.target === relativePath).id;
            const workbookSheets = this.mapOnElements({ parent: this.xlsxFileStructure.workbook.file.xml, query: "sheet" }, (sheetElement) => {
                return {
                    relationshipId: this.extractAttr(sheetElement, "r:id", { required: true }).asString(),
                    sheetId: this.extractAttr(sheetElement, "sheetId", { required: true }).asString(),
                    sheetName: this.extractAttr(sheetElement, "name", { required: true }).asString(),
                    state: this.extractAttr(sheetElement, "state", {
                        default: "visible",
                    }).asString(),
                };
            });
            const info = workbookSheets.find((info) => info.relationshipId === relId);
            if (!info) {
                throw new Error("Cannot find corresponding workbook sheet");
            }
            return info;
        }
        extractConditionalFormats() {
            return new XlsxCfExtractor(this.rootFile, this.xlsxFileStructure, this.warningManager, this.theme).extractConditionalFormattings();
        }
        extractFigures(worksheet) {
            const figures = this.mapOnElements({ parent: worksheet, query: "drawing" }, (drawingElement) => {
                const drawingId = this.extractAttr(drawingElement, "r:id", { required: true })?.asString();
                const drawingFile = this.getTargetXmlFile(this.relationships[drawingId]);
                const figures = new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();
                return figures;
            })[0];
            return figures || [];
        }
        extractTables(worksheet) {
            return this.mapOnElements({ query: "tablePart", parent: worksheet }, (tablePartElement) => {
                const tableId = this.extractAttr(tablePartElement, "r:id", { required: true })?.asString();
                const tableFile = this.getTargetXmlFile(this.relationships[tableId]);
                const tableExtractor = new XlsxTableExtractor(tableFile, this.xlsxFileStructure, this.warningManager);
                return tableExtractor.getTable();
            });
        }
        extractPivotTables() {
            try {
                return Object.values(this.relationships)
                    .filter((relationship) => relationship.type.endsWith("pivotTable"))
                    .map((pivotRelationship) => {
                    const pivotFile = this.getTargetXmlFile(pivotRelationship);
                    const pivot = new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();
                    return pivot;
                });
            }
            catch (e) {
                this.catchErrorOnElement(e);
                return [];
            }
        }
        extractMerges(worksheet) {
            return this.mapOnElements({ parent: worksheet, query: "mergeCell" }, (mergeElement) => {
                return this.extractAttr(mergeElement, "ref", { required: true }).asString();
            });
        }
        extractSheetFormat(worksheet) {
            const formatElement = this.querySelector(worksheet, "sheetFormatPr");
            if (!formatElement)
                return undefined;
            return {
                defaultColWidth: this.extractAttr(formatElement, "defaultColWidth", {
                    default: EXCEL_DEFAULT_COL_WIDTH.toString(),
                }).asNum(),
                defaultRowHeight: this.extractAttr(formatElement, "defaultRowHeight", {
                    default: EXCEL_DEFAULT_ROW_HEIGHT.toString(),
                }).asNum(),
            };
        }
        extractCols(worksheet) {
            return this.mapOnElements({ parent: worksheet, query: "cols col" }, (colElement) => {
                return {
                    width: this.extractAttr(colElement, "width")?.asNum(),
                    customWidth: this.extractAttr(colElement, "customWidth")?.asBool(),
                    bestFit: this.extractAttr(colElement, "bestFit")?.asBool(),
                    hidden: this.extractAttr(colElement, "hidden")?.asBool(),
                    min: this.extractAttr(colElement, "min", { required: true })?.asNum(),
                    max: this.extractAttr(colElement, "max", { required: true })?.asNum(),
                    styleIndex: this.extractAttr(colElement, "style")?.asNum(),
                };
            });
        }
        extractRows(worksheet) {
            return this.mapOnElements({ parent: worksheet, query: "sheetData row" }, (rowElement) => {
                return {
                    index: this.extractAttr(rowElement, "r", { required: true })?.asNum(),
                    cells: this.extractCells(rowElement),
                    height: this.extractAttr(rowElement, "ht")?.asNum(),
                    customHeight: this.extractAttr(rowElement, "customHeight")?.asBool(),
                    hidden: this.extractAttr(rowElement, "hidden")?.asBool(),
                    styleIndex: this.extractAttr(rowElement, "s")?.asNum(),
                };
            });
        }
        extractCells(row) {
            return this.mapOnElements({ parent: row, query: "c" }, (cellElement) => {
                return {
                    xc: this.extractAttr(cellElement, "r", { required: true })?.asString(),
                    styleIndex: this.extractAttr(cellElement, "s")?.asNum(),
                    type: CELL_TYPE_CONVERSION_MAP[this.extractAttr(cellElement, "t", { default: "n" })?.asString()],
                    value: this.extractChildTextContent(cellElement, "v"),
                    formula: this.extractCellFormula(cellElement),
                };
            });
        }
        extractCellFormula(cellElement) {
            const formulaElement = this.querySelector(cellElement, "f");
            if (!formulaElement)
                return undefined;
            return {
                content: this.extractTextContent(formulaElement),
                sharedIndex: this.extractAttr(formulaElement, "si")?.asNum(),
                ref: this.extractAttr(formulaElement, "ref")?.asString(),
            };
        }
        extractHyperLinks(worksheet) {
            return this.mapOnElements({ parent: worksheet, query: "hyperlink" }, (linkElement) => {
                const relId = this.extractAttr(linkElement, "r:id")?.asString();
                return {
                    xc: this.extractAttr(linkElement, "ref", { required: true })?.asString(),
                    location: this.extractAttr(linkElement, "location")?.asString(),
                    display: this.extractAttr(linkElement, "display")?.asString(),
                    relTarget: relId ? this.relationships[relId].target : undefined,
                };
            });
        }
        extractSharedFormulas(worksheet) {
            const sfElements = this.querySelectorAll(worksheet, `f[si][ref]`);
            const sfMap = {};
            for (let sfElement of sfElements) {
                const index = this.extractAttr(sfElement, "si", { required: true }).asNum();
                const formula = this.extractTextContent(sfElement, { required: true });
                sfMap[index] = formula;
            }
            const sfs = [];
            for (let i = 0; i < Object.keys(sfMap).length; i++) {
                if (!sfMap[i]) {
                    this.warningManager.addParsingWarning(`Missing shared formula ${i}, replacing it by empty formula`);
                    sfs.push("");
                }
                else {
                    sfs.push(sfMap[i]);
                }
            }
            return sfs;
        }
    }

    class XlsxStyleExtractor extends XlsxBaseExtractor {
        theme;
        constructor(xlsxStructure, warningManager, theme) {
            super(xlsxStructure.styles, xlsxStructure, warningManager);
            this.theme = theme;
        }
        getNumFormats() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "numFmt" }, (numFmtElement) => {
                return this.extractNumFormats(numFmtElement);
            });
        }
        extractNumFormats(numFmtElement) {
            return {
                id: this.extractAttr(numFmtElement, "numFmtId", {
                    required: true,
                }).asNum(),
                format: this.extractAttr(numFmtElement, "formatCode", {
                    required: true,
                    default: "",
                }).asString(),
            };
        }
        getFonts() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "font" }, (font) => {
                return this.extractFont(font);
            });
        }
        extractFont(fontElement) {
            const name = this.extractChildAttr(fontElement, "name", "val", {
                default: "Arial",
            }).asString();
            const size = this.extractChildAttr(fontElement, "sz", "val", {
                default: DEFAULT_FONT_SIZE.toString(),
            }).asNum();
            const color = this.extractColor(this.querySelector(fontElement, `color`), this.theme);
            // The behavior for these is kinda strange. The text is italic if there is either a "italic" tag with no "val"
            // attribute, or a tag with a "val" attribute = "1" (boolean).
            const italicElement = this.querySelector(fontElement, `i`) || undefined;
            const italic = italicElement && italicElement.attributes["val"]?.value !== "0";
            const boldElement = this.querySelector(fontElement, `b`) || undefined;
            const bold = boldElement && boldElement.attributes["val"]?.value !== "0";
            const strikeElement = this.querySelector(fontElement, `strike`) || undefined;
            const strike = strikeElement && strikeElement.attributes["val"]?.value !== "0";
            const underlineElement = this.querySelector(fontElement, `u`) || undefined;
            const underline = underlineElement && underlineElement.attributes["val"]?.value !== "none";
            return { name, size, color, italic, bold, underline, strike };
        }
        getFills() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "fill" }, (fillElement) => {
                return this.extractFill(fillElement);
            });
        }
        extractFill(fillElement) {
            // Fills are either patterns of gradients
            const fillChild = fillElement.children[0];
            if (fillChild.tagName === "patternFill") {
                return {
                    patternType: fillChild.attributes["patternType"]?.value,
                    bgColor: this.extractColor(this.querySelector(fillChild, "bgColor"), this.theme),
                    fgColor: this.extractColor(this.querySelector(fillChild, "fgColor"), this.theme),
                };
            }
            else {
                // We don't support gradients. Take the second gradient color as fill color
                return {
                    patternType: "solid",
                    fgColor: this.extractColor(this.querySelectorAll(fillChild, "color")[1], this.theme),
                };
            }
        }
        getBorders() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "border" }, (borderElement) => {
                return this.extractBorder(borderElement);
            });
        }
        extractBorder(borderElement) {
            const border = {
                left: this.extractSingleBorder(borderElement, "left", this.theme),
                right: this.extractSingleBorder(borderElement, "right", this.theme),
                top: this.extractSingleBorder(borderElement, "top", this.theme),
                bottom: this.extractSingleBorder(borderElement, "bottom", this.theme),
                diagonal: this.extractSingleBorder(borderElement, "diagonal", this.theme),
            };
            if (border.diagonal) {
                border.diagonalUp = this.extractAttr(borderElement, "diagonalUp")?.asBool();
                border.diagonalDown = this.extractAttr(borderElement, "diagonalDown")?.asBool();
            }
            return border;
        }
        extractSingleBorder(borderElement, direction, theme) {
            const directionElement = this.querySelector(borderElement, direction);
            if (!directionElement || !directionElement.attributes["style"])
                return undefined;
            return {
                style: this.extractAttr(directionElement, "style", {
                    required: true,
                    default: "thin",
                }).asString(),
                color: this.extractColor(directionElement.children[0], theme, "000000"),
            };
        }
        extractAlignment(alignmentElement) {
            return {
                horizontal: this.extractAttr(alignmentElement, "horizontal", {
                    default: "general",
                }).asString(),
                vertical: this.extractAttr(alignmentElement, "vertical", {
                    default: "bottom",
                }).asString(),
                textRotation: this.extractAttr(alignmentElement, "textRotation")?.asNum(),
                wrapText: this.extractAttr(alignmentElement, "wrapText")?.asBool(),
                indent: this.extractAttr(alignmentElement, "indent")?.asNum(),
                relativeIndent: this.extractAttr(alignmentElement, "relativeIndent")?.asNum(),
                justifyLastLine: this.extractAttr(alignmentElement, "justifyLastLine")?.asBool(),
                shrinkToFit: this.extractAttr(alignmentElement, "shrinkToFit")?.asBool(),
                readingOrder: this.extractAttr(alignmentElement, "readingOrder")?.asNum(),
            };
        }
        getDxfs() {
            return this.mapOnElements({ query: "dxf", parent: this.rootFile.file.xml }, (dxfElement) => {
                const fontElement = this.querySelector(dxfElement, "font");
                const fillElement = this.querySelector(dxfElement, "fill");
                const borderElement = this.querySelector(dxfElement, "border");
                const numFmtElement = this.querySelector(dxfElement, "numFmt");
                const alignmentElement = this.querySelector(dxfElement, "alignment");
                return {
                    font: fontElement ? this.extractFont(fontElement) : undefined,
                    fill: fillElement ? this.extractFill(fillElement) : undefined,
                    numFmt: numFmtElement ? this.extractNumFormats(numFmtElement) : undefined,
                    alignment: alignmentElement ? this.extractAlignment(alignmentElement) : undefined,
                    border: borderElement ? this.extractBorder(borderElement) : undefined,
                };
            });
        }
        getStyles() {
            return this.mapOnElements({ query: "cellXfs xf", parent: this.rootFile.file.xml }, (styleElement) => {
                const alignmentElement = this.querySelector(styleElement, "alignment");
                return {
                    fontId: this.extractAttr(styleElement, "fontId", {
                        required: true,
                        default: 0,
                    }).asNum(),
                    fillId: this.extractAttr(styleElement, "fillId", {
                        required: true,
                        default: 0,
                    }).asNum(),
                    borderId: this.extractAttr(styleElement, "borderId", {
                        required: true,
                        default: 0,
                    }).asNum(),
                    numFmtId: this.extractAttr(styleElement, "numFmtId", {
                        required: true,
                        default: 0,
                    }).asNum(),
                    alignment: alignmentElement ? this.extractAlignment(alignmentElement) : undefined,
                };
            });
        }
    }

    class XlsxExternalBookExtractor extends XlsxBaseExtractor {
        getExternalBook() {
            return this.mapOnElements({ parent: this.rootFile.file.xml, query: "externalBook" }, (bookElement) => {
                return {
                    rId: this.extractAttr(bookElement, "r:id", { required: true }).asString(),
                    sheetNames: this.mapOnElements({ parent: bookElement, query: "sheetName" }, (sheetNameElement) => {
                        return this.extractAttr(sheetNameElement, "val", { required: true }).asString();
                    }),
                    datasets: this.extractExternalSheetData(bookElement),
                };
            })[0];
        }
        extractExternalSheetData(externalBookElement) {
            return this.mapOnElements({ parent: externalBookElement, query: "sheetData" }, (sheetDataElement) => {
                const cellsData = this.mapOnElements({ parent: sheetDataElement, query: "cell" }, (cellElement) => {
                    return {
                        xc: this.extractAttr(cellElement, "r", { required: true }).asString(),
                        value: this.extractChildTextContent(cellElement, "v", { required: true }),
                    };
                });
                const dataMap = {};
                for (let cell of cellsData) {
                    dataMap[cell.xc] = cell.value;
                }
                return {
                    sheetId: this.extractAttr(sheetDataElement, "sheetId", { required: true }).asNum(),
                    data: dataMap,
                };
            });
        }
    }

    /**
     * Return all the xmls converted to XLSXImportFile corresponding to the given content type.
     */
    function getXLSXFilesOfType(contentType, xmls) {
        const paths = getPathsOfContent(contentType, xmls);
        return getXlsxFile(paths, xmls);
    }
    /**
     * From an array of file path, return the equivalents XLSXFiles. An XLSX File is composed of an XML,
     * and optionally of a relationships XML.
     */
    function getXlsxFile(files, xmls) {
        const ret = [];
        for (let file of files) {
            const rels = getRelationFile(file, xmls);
            ret.push({
                file: { fileName: file, xml: xmls[file] },
                rels: rels ? { fileName: rels, xml: xmls[rels] } : undefined,
            });
        }
        return ret;
    }
    /**
     * Return all the path of the files in a XLSX directory that have content of the given type.
     */
    function getPathsOfContent(contentType, xmls) {
        const xml = xmls[CONTENT_TYPES_FILE];
        const sheetItems = xml.querySelectorAll(`Override[ContentType="${contentType}"]`);
        const paths = [];
        for (let item of sheetItems) {
            const file = item?.attributes["PartName"].value;
            paths.push(file.substring(1)); // Remove the heading "/"
        }
        return paths;
    }
    /**
     * Get the corresponding relationship file for a given xml file in a XLSX directory.
     */
    function getRelationFile(file, xmls) {
        if (file === CONTENT_TYPES_FILE) {
            return "_rels/.rels";
        }
        let relsFile = "";
        const pathParts = file.split("/");
        for (let i = 0; i < pathParts.length - 1; i++) {
            relsFile += pathParts[i] + "/";
        }
        relsFile += "_rels/";
        relsFile += pathParts[pathParts.length - 1] + ".rels";
        if (!xmls[relsFile]) {
            relsFile = undefined;
        }
        return relsFile;
    }

    const EXCEL_IMPORT_VERSION = 12;
    class XlsxReader {
        warningManager;
        xmls;
        images;
        constructor(files) {
            this.warningManager = new XLSXImportWarningManager();
            this.xmls = {};
            this.images = [];
            for (let key of Object.keys(files)) {
                // Random files can be in xlsx (like a bin file for printer settings)
                if (key.endsWith(".xml") || key.endsWith(".rels")) {
                    const contentString = escapeTagNamespaces(files[key]);
                    this.xmls[key] = parseXML(new XMLString(contentString));
                }
                else if (key.includes("media/image")) {
                    this.images.push({
                        fileName: key,
                        imageSrc: files[key]["imageSrc"],
                    });
                }
            }
        }
        convertXlsx() {
            const xlsxData = this.getXlsxData();
            const convertedData = this.convertImportedData(xlsxData);
            return convertedData;
        }
        // ---------------------------------------------------------------------------
        // Parsing XMLs
        // ---------------------------------------------------------------------------
        getXlsxData() {
            const xlsxFileStructure = this.buildXlsxFileStructure();
            const theme = xlsxFileStructure.theme
                ? new XlsxMiscExtractor(xlsxFileStructure.theme, xlsxFileStructure, this.warningManager).getTheme()
                : undefined;
            const sharedStrings = xlsxFileStructure.sharedStrings
                ? new XlsxMiscExtractor(xlsxFileStructure.sharedStrings, xlsxFileStructure, this.warningManager).getSharedStrings()
                : [];
            // Sort sheets by file name : the sheets will always be named sheet1.xml, sheet2.xml, ... in order
            const sheets = xlsxFileStructure.sheets
                .sort((a, b) => a.file.fileName.localeCompare(b.file.fileName, undefined, { numeric: true }))
                .map((sheetFile) => {
                return new XlsxSheetExtractor(sheetFile, xlsxFileStructure, this.warningManager, theme).getSheet();
            });
            const externalBooks = xlsxFileStructure.externalLinks.map((externalLinkFile) => {
                return new XlsxExternalBookExtractor(externalLinkFile, xlsxFileStructure, this.warningManager).getExternalBook();
            });
            const styleExtractor = new XlsxStyleExtractor(xlsxFileStructure, this.warningManager, theme);
            return {
                fonts: styleExtractor.getFonts(),
                fills: styleExtractor.getFills(),
                borders: styleExtractor.getBorders(),
                dxfs: styleExtractor.getDxfs(),
                numFmts: styleExtractor.getNumFormats(),
                styles: styleExtractor.getStyles(),
                sheets: sheets,
                sharedStrings,
                externalBooks,
            };
        }
        buildXlsxFileStructure() {
            const xlsxFileStructure = {
                sheets: getXLSXFilesOfType(CONTENT_TYPES.sheet, this.xmls),
                workbook: getXLSXFilesOfType(CONTENT_TYPES.workbook, this.xmls)[0],
                styles: getXLSXFilesOfType(CONTENT_TYPES.styles, this.xmls)[0],
                sharedStrings: getXLSXFilesOfType(CONTENT_TYPES.sharedStrings, this.xmls)[0],
                theme: getXLSXFilesOfType(CONTENT_TYPES.themes, this.xmls)[0],
                charts: getXLSXFilesOfType(CONTENT_TYPES.chart, this.xmls),
                figures: getXLSXFilesOfType(CONTENT_TYPES.drawing, this.xmls),
                tables: getXLSXFilesOfType(CONTENT_TYPES.table, this.xmls),
                pivots: getXLSXFilesOfType(CONTENT_TYPES.pivot, this.xmls),
                externalLinks: getXLSXFilesOfType(CONTENT_TYPES.externalLink, this.xmls),
                images: this.images,
            };
            if (!xlsxFileStructure.workbook.rels) {
                throw Error(_t("Cannot find workbook relations file"));
            }
            return xlsxFileStructure;
        }
        // ---------------------------------------------------------------------------
        // Conversion
        // ---------------------------------------------------------------------------
        convertImportedData(data) {
            const convertedData = {
                version: EXCEL_IMPORT_VERSION,
                sheets: convertSheets(data, this.warningManager),
                styles: convertStyles(data, this.warningManager),
                formats: convertFormats(data, this.warningManager),
                borders: convertBorders(data, this.warningManager),
                entities: {},
                revisionId: DEFAULT_REVISION_ID,
            };
            convertTables(convertedData, data);
            // Remove falsy attributes in styles. Not mandatory, but make objects more readable when debugging
            Object.keys(data.styles).map((key) => {
                data.styles[key] = removeFalsyAttributes(data.styles[key]);
            });
            return convertedData;
        }
    }

    var State;
    (function (State) {
        /**
         * Initial state.
         * Expecting any reference for the left part of a range
         * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
         */
        State[State["LeftRef"] = 0] = "LeftRef";
        /**
         * Expecting any reference for the right part of a range
         * e.g. "A1", "1", "A", "Sheet1!A1", "Sheet1!A"
         */
        State[State["RightRef"] = 1] = "RightRef";
        /**
         * Expecting the separator without any constraint on the right part
         */
        State[State["Separator"] = 2] = "Separator";
        /**
         * Expecting the separator for a full column range
         */
        State[State["FullColumnSeparator"] = 3] = "FullColumnSeparator";
        /**
         * Expecting the separator for a full row range
         */
        State[State["FullRowSeparator"] = 4] = "FullRowSeparator";
        /**
         * Expecting the right part of a full column range
         * e.g. "1", "A1"
         */
        State[State["RightColumnRef"] = 5] = "RightColumnRef";
        /**
         * Expecting the right part of a full row range
         * e.g. "A", "A1"
         */
        State[State["RightRowRef"] = 6] = "RightRowRef";
        /**
         * Final state. A range has been matched
         */
        State[State["Found"] = 7] = "Found";
    })(State || (State = {}));
    const goTo = (state, guard = () => true) => [
        {
            goTo: state,
            guard,
        },
    ];
    const goToMulti = (state, guard = () => true) => ({
        goTo: state,
        guard,
    });
    const machine = {
        [State.LeftRef]: {
            REFERENCE: goTo(State.Separator),
            NUMBER: goTo(State.FullRowSeparator),
            SYMBOL: [
                goToMulti(State.FullColumnSeparator, (token) => isColReference(token.value)),
                goToMulti(State.FullRowSeparator, (token) => isRowReference(token.value)),
            ],
        },
        [State.FullColumnSeparator]: {
            SPACE: goTo(State.FullColumnSeparator),
            OPERATOR: goTo(State.RightColumnRef, (token) => token.value === ":"),
        },
        [State.FullRowSeparator]: {
            SPACE: goTo(State.FullRowSeparator),
            OPERATOR: goTo(State.RightRowRef, (token) => token.value === ":"),
        },
        [State.Separator]: {
            SPACE: goTo(State.Separator),
            OPERATOR: goTo(State.RightRef, (token) => token.value === ":"),
        },
        [State.RightRef]: {
            SPACE: goTo(State.RightRef),
            NUMBER: goTo(State.Found),
            REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
            SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
        },
        [State.RightColumnRef]: {
            SPACE: goTo(State.RightColumnRef),
            SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
            REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
        },
        [State.RightRowRef]: {
            SPACE: goTo(State.RightRowRef),
            NUMBER: goTo(State.Found),
            REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
            SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
        },
        [State.Found]: {},
    };
    /**
     * Check if the list of tokens starts with a sequence of tokens representing
     * a range.
     * If a range is found, the sequence is removed from the list and is returned
     * as a single token.
     */
    function matchReference(tokens) {
        let head = 0;
        let transitions = machine[State.LeftRef];
        const matchedTokens = [];
        while (transitions !== undefined) {
            const token = tokens[head++];
            if (!token) {
                return null;
            }
            const transition = transitions[token.type]?.find((transition) => transition.guard(token));
            const nextState = transition ? transition.goTo : undefined;
            switch (nextState) {
                case undefined:
                    return null;
                case State.Found:
                    matchedTokens.push(token);
                    tokens.splice(0, head);
                    return {
                        type: "REFERENCE",
                        value: concat(matchedTokens.map((token) => token.value)),
                    };
                default:
                    transitions = machine[nextState];
                    matchedTokens.push(token);
                    break;
            }
        }
        return null;
    }
    /**
     * Take the result of the tokenizer and transform it to be usable in the
     * manipulations of range
     *
     * @param formula
     */
    function rangeTokenize(formula, locale = DEFAULT_LOCALE) {
        const tokens = tokenize(formula, locale);
        const result = [];
        while (tokens.length) {
            result.push(matchReference(tokens) || tokens.shift());
        }
        return result;
    }

    /**
     * parses a formula (as a string) into the same formula,
     * but with the references to other cells extracted
     *
     * =sum(a3:b1) + c3 --> =sum(|0|) + |1|
     *
     * @param formula
     */
    function normalizeV9(formula) {
        const tokens = rangeTokenize(formula);
        let dependencies = [];
        let noRefFormula = "".concat(...tokens.map((token) => {
            if (token.type === "REFERENCE" && cellReference.test(token.value)) {
                const value = token.value.trim();
                if (!dependencies.includes(value)) {
                    dependencies.push(value);
                }
                return `${FORMULA_REF_IDENTIFIER}${dependencies.indexOf(value)}${FORMULA_REF_IDENTIFIER}`;
            }
            else {
                return token.value;
            }
        }));
        return { text: noRefFormula, dependencies };
    }

    /**
     * This is the current state version number. It should be incremented each time
     * a breaking change is made in the way the state is handled, and an upgrade
     * function should be defined
     */
    const CURRENT_VERSION = 14;
    const INITIAL_SHEET_ID = "Sheet1";
    /**
     * This function tries to load anything that could look like a valid
     * workbookData object. It applies any migrations, if needed, and return a
     * current, complete workbookData object.
     *
     * It also ensures that there is at least one sheet.
     */
    function load(data, verboseImport) {
        if (!data) {
            return createEmptyWorkbookData();
        }
        if (data["[Content_Types].xml"]) {
            const reader = new XlsxReader(data);
            data = reader.convertXlsx();
            if (verboseImport) {
                for (let parsingError of reader.warningManager.warnings.sort()) {
                    console.warn(parsingError);
                }
            }
        }
        // apply migrations, if needed
        if ("version" in data) {
            if (data.version < CURRENT_VERSION) {
                data = migrate(data);
            }
        }
        data = repairData(data);
        return data;
    }
    function migrate(data) {
        const index = MIGRATIONS.findIndex((m) => m.from === data.version);
        for (let i = index; i < MIGRATIONS.length; i++) {
            data = MIGRATIONS[i].applyMigration(data);
        }
        return data;
    }
    const MIGRATIONS = [
        {
            description: "add the `activeSheet` field on data",
            from: 1,
            to: 2,
            applyMigration(data) {
                if (data.sheets && data.sheets[0]) {
                    data.activeSheet = data.sheets[0].name;
                }
                return data;
            },
        },
        {
            description: "add an id field in each sheet",
            from: 2,
            to: 3,
            applyMigration(data) {
                if (data.sheets && data.sheets.length) {
                    for (let sheet of data.sheets) {
                        sheet.id = sheet.id || sheet.name;
                    }
                }
                return data;
            },
        },
        {
            description: "activeSheet is now an id, not the name of a sheet",
            from: 3,
            to: 4,
            applyMigration(data) {
                if (data.sheets && data.activeSheet) {
                    const activeSheet = data.sheets.find((s) => s.name === data.activeSheet);
                    data.activeSheet = activeSheet.id;
                }
                return data;
            },
        },
        {
            description: "add figures object in each sheets",
            from: 4,
            to: 5,
            applyMigration(data) {
                for (let sheet of data.sheets || []) {
                    sheet.figures = sheet.figures || [];
                }
                return data;
            },
        },
        {
            description: "normalize the content of the cell if it is a formula to avoid parsing all the formula that vary only by the cells they use",
            from: 5,
            to: 6,
            applyMigration(data) {
                for (let sheet of data.sheets || []) {
                    for (let xc in sheet.cells || []) {
                        const cell = sheet.cells[xc];
                        if (cell.content && cell.content.startsWith("=")) {
                            cell.formula = normalizeV9(cell.content);
                        }
                    }
                }
                return data;
            },
        },
        {
            description: "transform chart data structure",
            from: 6,
            to: 7,
            applyMigration(data) {
                for (let sheet of data.sheets || []) {
                    for (let f in sheet.figures || []) {
                        const { dataSets, ...newData } = sheet.figures[f].data;
                        const newDataSets = [];
                        for (let ds of dataSets) {
                            if (ds.labelCell) {
                                const dataRange = toZone(ds.dataRange);
                                const newRange = ds.labelCell + ":" + toXC(dataRange.right, dataRange.bottom);
                                newDataSets.push(newRange);
                            }
                            else {
                                newDataSets.push(ds.dataRange);
                            }
                        }
                        newData.dataSetsHaveTitle = Boolean(dataSets[0].labelCell);
                        newData.dataSets = newDataSets;
                        sheet.figures[f].data = newData;
                    }
                }
                return data;
            },
        },
        {
            description: "remove single quotes in sheet names",
            from: 7,
            to: 8,
            applyMigration(data) {
                const namesTaken = [];
                const globalForbiddenInExcel = new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g");
                for (let sheet of data.sheets || []) {
                    if (!sheet.name) {
                        continue;
                    }
                    const oldName = sheet.name;
                    const escapedName = oldName.replace(globalForbiddenInExcel, "_");
                    let i = 1;
                    let newName = escapedName;
                    while (namesTaken.includes(newName)) {
                        newName = `${escapedName}${i}`;
                        i++;
                    }
                    sheet.name = newName;
                    namesTaken.push(newName);
                    const replaceName = (str) => {
                        if (str === undefined) {
                            return str;
                        }
                        // replaceAll is only available in next Typescript version
                        let newString = str.replace(oldName, newName);
                        let currentString = str;
                        while (currentString !== newString) {
                            currentString = newString;
                            newString = currentString.replace(oldName, newName);
                        }
                        return currentString;
                    };
                    //cells
                    for (let xc in sheet.cells) {
                        const cell = sheet.cells[xc];
                        if (cell.formula) {
                            cell.formula.dependencies = cell.formula.dependencies.map(replaceName);
                        }
                    }
                    //charts
                    for (let figure of sheet.figures || []) {
                        if (figure.type === "chart") {
                            const dataSets = figure.data.dataSets.map(replaceName);
                            const labelRange = replaceName(figure.data.labelRange);
                            figure.data = { ...figure.data, dataSets, labelRange };
                        }
                    }
                    //ConditionalFormats
                    for (let cf of sheet.conditionalFormats || []) {
                        cf.ranges = cf.ranges.map(replaceName);
                        for (const thresholdName of [
                            "minimum",
                            "maximum",
                            "midpoint",
                            "upperInflectionPoint",
                            "lowerInflectionPoint",
                        ]) {
                            if (cf.rule[thresholdName]?.type === "formula") {
                                cf.rule[thresholdName].value = replaceName(cf.rule[thresholdName].value);
                            }
                        }
                    }
                }
                return data;
            },
        },
        {
            description: "transform chart data structure with design attributes",
            from: 8,
            to: 9,
            applyMigration(data) {
                for (const sheet of data.sheets || []) {
                    for (const chart of sheet.figures || []) {
                        chart.data.background = BACKGROUND_CHART_COLOR;
                        chart.data.verticalAxisPosition = "left";
                        chart.data.legendPosition = "top";
                        chart.data.stacked = false;
                    }
                }
                return data;
            },
        },
        {
            description: "de-normalize formula to reduce exported json size (~30%)",
            from: 9,
            to: 10,
            applyMigration(data) {
                for (let sheet of data.sheets || []) {
                    for (let xc in sheet.cells || []) {
                        const cell = sheet.cells[xc];
                        if (cell.formula) {
                            let { text, dependencies } = cell.formula;
                            for (let [index, d] of Object.entries(dependencies)) {
                                const stringPosition = `\\${FORMULA_REF_IDENTIFIER}${index}\\${FORMULA_REF_IDENTIFIER}`;
                                text = text.replace(new RegExp(stringPosition, "g"), d);
                            }
                            cell.content = text;
                            delete cell.formula;
                        }
                    }
                }
                return data;
            },
        },
        {
            description: "normalize the formats of the cells",
            from: 10,
            to: 11,
            applyMigration(data) {
                const formats = {};
                for (let sheet of data.sheets || []) {
                    for (let xc in sheet.cells || []) {
                        const cell = sheet.cells[xc];
                        if (cell.format) {
                            cell.format = getItemId(cell.format, formats);
                        }
                    }
                }
                data.formats = formats;
                return data;
            },
        },
        {
            description: "Add isVisible to sheets",
            from: 11,
            to: 12,
            applyMigration(data) {
                for (let sheet of data.sheets || []) {
                    sheet.isVisible = true;
                }
                return data;
            },
        },
        {
            description: "Fix datafilter duplication",
            from: 12,
            to: 12.5,
            applyMigration(data) {
                for (let sheet of data.sheets || []) {
                    let knownDataFilterZones = [];
                    for (let filterTable of sheet.filterTables || []) {
                        const zone = toZone(filterTable.range);
                        // See commit message for the details
                        const intersectZoneIndex = knownDataFilterZones.findIndex((knownZone) => overlap(knownZone, zone));
                        if (intersectZoneIndex !== -1) {
                            knownDataFilterZones[intersectZoneIndex] = zone;
                        }
                        else {
                            knownDataFilterZones.push(zone);
                        }
                    }
                    sheet.filterTables = knownDataFilterZones.map((zone) => ({
                        range: zoneToXc(zone),
                    }));
                }
                return data;
            },
        },
        {
            description: "Change Border description structure",
            from: 12,
            to: 13,
            applyMigration(data) {
                for (const borderId in data.borders) {
                    const border = data.borders[borderId];
                    for (const position in border) {
                        if (Array.isArray(border[position])) {
                            border[position] = {
                                style: border[position][0],
                                color: border[position][1],
                            };
                        }
                    }
                }
                return data;
            },
        },
        {
            description: "Add locale to spreadsheet settings",
            from: 13,
            to: 14,
            applyMigration(data) {
                if (!data.settings) {
                    data.settings = {};
                }
                if (!data.settings.locale) {
                    data.settings.locale = DEFAULT_LOCALE;
                }
                return data;
            },
        },
    ];
    /**
     * This function is used to repair faulty data independently of the migration.
     */
    function repairData(data) {
        data = forceUnicityOfFigure(data);
        data = setDefaults(data);
        return data;
    }
    /**
     * Force the unicity of figure ids accross sheets
     */
    function forceUnicityOfFigure(data) {
        if (data.uniqueFigureIds) {
            return data;
        }
        const figureIds = new Set();
        const uuidGenerator = new UuidGenerator();
        for (const sheet of data.sheets || []) {
            for (const figure of sheet.figures || []) {
                if (figureIds.has(figure.id)) {
                    figure.id += uuidGenerator.uuidv4();
                }
                figureIds.add(figure.id);
            }
        }
        data.uniqueFigureIds = true;
        return data;
    }
    /**
     * sanity check: try to fix missing fields/corrupted state by providing
     * sensible default values
     */
    function setDefaults(partialData) {
        const data = Object.assign(createEmptyWorkbookData(), partialData, {
            version: CURRENT_VERSION,
        });
        data.sheets = data.sheets
            ? data.sheets.map((s, i) => Object.assign(createEmptySheet(`Sheet${i + 1}`, `Sheet${i + 1}`), s))
            : [];
        if (data.sheets.length === 0) {
            data.sheets.push(createEmptySheet(INITIAL_SHEET_ID, "Sheet1"));
        }
        if (!isValidLocale(data.settings.locale)) {
            data.settings.locale = DEFAULT_LOCALE;
        }
        return data;
    }
    /**
     * The goal of this function is to repair corrupted/wrong initial messages caused by
     * a bug.
     * The bug should obviously be fixed, but it's too late for existing spreadsheet.
     */
    function repairInitialMessages(data, initialMessages) {
        initialMessages = fixTranslatedSheetIds(data, initialMessages);
        initialMessages = dropCommands(initialMessages, "SORT_CELLS");
        initialMessages = dropCommands(initialMessages, "SET_DECIMAL");
        initialMessages = fixChartDefinitions(data, initialMessages);
        return initialMessages;
    }
    /**
     * When the workbook data is originally empty, a new one is generated on-the-fly.
     * A bug caused the sheet id to be non-deterministic. The sheet id was propagated in
     * commands.
     * This function repairs initial commands with a wrong sheetId.
     */
    function fixTranslatedSheetIds(data, initialMessages) {
        // the fix is only needed when the workbook is generated on-the-fly
        if (Object.keys(data).length !== 0) {
            return initialMessages;
        }
        const sheetIds = [];
        const messages = [];
        const fixSheetId = (cmd) => {
            if (cmd.type === "CREATE_SHEET") {
                sheetIds.push(cmd.sheetId);
            }
            else if ("sheetId" in cmd && !sheetIds.includes(cmd.sheetId)) {
                return { ...cmd, sheetId: INITIAL_SHEET_ID };
            }
            return cmd;
        };
        for (const message of initialMessages) {
            if (message.type === "REMOTE_REVISION") {
                messages.push({
                    ...message,
                    commands: message.commands.map(fixSheetId),
                });
            }
            else {
                messages.push(message);
            }
        }
        return messages;
    }
    function dropCommands(initialMessages, commandType) {
        const messages = [];
        for (const message of initialMessages) {
            if (message.type === "REMOTE_REVISION") {
                messages.push({
                    ...message,
                    commands: message.commands.filter((command) => command.type !== commandType),
                });
            }
            else {
                messages.push(message);
            }
        }
        return messages;
    }
    function fixChartDefinitions(data, initialMessages) {
        const messages = [];
        const map = {};
        for (const sheet of data.sheets || []) {
            sheet.figures?.forEach((figure) => {
                if (figure.tag === "chart") {
                    // chart definition
                    map[figure.id] = figure.data;
                }
            });
        }
        for (const message of initialMessages) {
            if (message.type === "REMOTE_REVISION") {
                const commands = [];
                for (const cmd of message.commands) {
                    let command = cmd;
                    switch (cmd.type) {
                        case "CREATE_CHART":
                            map[cmd.id] = cmd.definition;
                            break;
                        case "UPDATE_CHART":
                            if (!map[cmd.id]) {
                                /** the chart does not exist on the map, it might have been created after a duplicate sheet.
                                 * We don't have access to the definition, so we skip the command.
                                 */
                                console.log(`Fix chart definition: chart with id ${cmd.id} not found.`);
                                continue;
                            }
                            const definition = map[cmd.id];
                            const newDefinition = { ...definition, ...cmd.definition };
                            command = { ...cmd, definition: newDefinition };
                            map[cmd.id] = newDefinition;
                            break;
                    }
                    commands.push(command);
                }
                messages.push({
                    ...message,
                    commands,
                });
            }
            else {
                messages.push(message);
            }
        }
        return messages;
    }
    // -----------------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------------
    function createEmptySheet(sheetId, name) {
        return {
            id: sheetId,
            name,
            colNumber: 26,
            rowNumber: 100,
            cells: {},
            cols: {},
            rows: {},
            merges: [],
            conditionalFormats: [],
            figures: [],
            filterTables: [],
            isVisible: true,
        };
    }
    function createEmptyWorkbookData(sheetName = "Sheet1") {
        const data = {
            version: CURRENT_VERSION,
            sheets: [createEmptySheet(INITIAL_SHEET_ID, sheetName)],
            entities: {},
            styles: {},
            formats: {},
            borders: {},
            revisionId: DEFAULT_REVISION_ID,
            uniqueFigureIds: true,
            settings: { locale: DEFAULT_LOCALE },
        };
        return data;
    }
    function createEmptyExcelSheet(sheetId, name) {
        return {
            ...createEmptySheet(sheetId, name),
            charts: [],
            images: [],
        };
    }
    function createEmptyExcelWorkbookData() {
        return {
            ...createEmptyWorkbookData(),
            sheets: [createEmptyExcelSheet(INITIAL_SHEET_ID, "Sheet1")],
        };
    }

    /**
     * BasePlugin
     *
     * Since the spreadsheet internal state is quite complex, it is split into
     * multiple parts, each managing a specific concern.
     *
     * This file introduce the BasePlugin, which is the common class that defines
     * how each of these model sub parts should interact with each other.
     * There are two kind of plugins: core plugins handling persistent data
     * and UI plugins handling transient data.
     */
    class BasePlugin {
        static getters = [];
        history;
        dispatch;
        constructor(stateObserver, dispatch) {
            this.history = Object.assign(Object.create(stateObserver), {
                update: stateObserver.addChange.bind(stateObserver, this),
                selectCell: () => { },
            });
            this.dispatch = dispatch;
        }
        /**
         * Export for excel should be available for all plugins, even for the UI.
         * In some case, we need to export evaluated value, which is available from
         * UI plugin only.
         */
        exportForExcel(data) { }
        // ---------------------------------------------------------------------------
        // Command handling
        // ---------------------------------------------------------------------------
        /**
         * Before a command is accepted, the model will ask each plugin if the command
         * is allowed.  If all of then return true, then we can proceed. Otherwise,
         * the command is cancelled.
         *
         * There should not be any side effects in this method.
         */
        allowDispatch(command) {
            return "Success" /* CommandResult.Success */;
        }
        /**
         * This method is useful when a plugin need to perform some action before a
         * command is handled in another plugin. This should only be used if it is not
         * possible to do the work in the handle method.
         */
        beforeHandle(command) { }
        /**
         * This is the standard place to handle any command. Most of the plugin
         * command handling work should take place here.
         */
        handle(command) { }
        /**
         * Sometimes, it is useful to perform some work after a command (and all its
         * subcommands) has been completely handled.  For example, when we paste
         * multiple cells, we only want to reevaluate the cell values once at the end.
         */
        finalize() { }
        /**
         * Combine multiple validation functions into a single function
         * returning the list of result of every validation.
         */
        batchValidations(...validations) {
            return (toValidate) => validations.map((validation) => validation.call(this, toValidate)).flat();
        }
        /**
         * Combine multiple validation functions. Every validation is executed one after
         * the other. As soon as one validation fails, it stops and the cancelled reason
         * is returned.
         */
        chainValidations(...validations) {
            return (toValidate) => {
                for (const validation of validations) {
                    let results = validation.call(this, toValidate);
                    if (!Array.isArray(results)) {
                        results = [results];
                    }
                    const cancelledReasons = results.filter((result) => result !== "Success" /* CommandResult.Success */);
                    if (cancelledReasons.length) {
                        return cancelledReasons;
                    }
                }
                return "Success" /* CommandResult.Success */;
            };
        }
        checkValidations(command, ...validations) {
            return this.batchValidations(...validations)(command);
        }
    }

    /**
     * Core plugins handle spreadsheet data.
     * They are responsible to import, export and maintain the spreadsheet
     * persisted state.
     * They should not be concerned about UI parts or transient state.
     */
    class CorePlugin extends BasePlugin {
        getters;
        uuidGenerator;
        constructor({ getters, stateObserver, range, dispatch, uuidGenerator }) {
            super(stateObserver, dispatch);
            range.addRangeProvider(this.adaptRanges.bind(this));
            this.getters = getters;
            this.uuidGenerator = uuidGenerator;
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) { }
        export(data) { }
        /**
         * This method can be implemented in any plugin, to loop over the plugin's data structure and adapt the plugin's ranges.
         * To adapt them, the implementation of the function must have a perfect knowledge of the data structure, thus
         * implementing the loops over it makes sense in the plugin itself.
         * When calling the method applyChange, the range will be adapted if necessary, then a copy will be returned along with
         * the type of change that occurred.
         *
         * @param applyChange a function that, when called, will adapt the range according to the change on the grid
         * @param sheetId an optional sheetId to adapt either range of that sheet specifically, or ranges pointing to that sheet
         */
        adaptRanges(applyChange, sheetId) { }
        /**
         * Implement this method to clean unused external resources, such as images
         * stored on a server which have been deleted.
         */
        garbageCollectExternalResources() { }
    }

    /**
     * Formatting plugin.
     *
     * This plugin manages all things related to a cell look:
     * - borders
     */
    class BordersPlugin extends CorePlugin {
        static getters = ["getCellBorder", "getBordersColors"];
        borders = {};
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "SET_BORDER":
                    return this.checkBordersUnchanged(cmd);
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "ADD_MERGE":
                    for (const zone of cmd.target) {
                        this.addBordersToMerge(cmd.sheetId, zone);
                    }
                    break;
                case "DUPLICATE_SHEET":
                    const borders = this.borders[cmd.sheetId];
                    if (borders) {
                        // borders is a sparse 2D array.
                        // map and slice preserve empty values and do not set `undefined` instead
                        const bordersCopy = borders
                            .slice()
                            .map((col) => col?.slice().map((border) => ({ ...border })));
                        this.history.update("borders", cmd.sheetIdTo, bordersCopy);
                    }
                    break;
                case "DELETE_SHEET":
                    const allBorders = { ...this.borders };
                    delete allBorders[cmd.sheetId];
                    this.history.update("borders", allBorders);
                    break;
                case "SET_BORDER":
                    this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);
                    break;
                case "SET_ZONE_BORDERS":
                    if (cmd.border) {
                        const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
                        this.setBorders(cmd.sheetId, target, cmd.border.position, cmd.border.color === ""
                            ? undefined
                            : {
                                style: cmd.border.style || DEFAULT_BORDER_DESC.style,
                                color: cmd.border.color || DEFAULT_BORDER_DESC.color,
                            });
                    }
                    break;
                case "CLEAR_FORMATTING":
                    this.clearBorders(cmd.sheetId, cmd.target);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                    for (let el of cmd.elements) {
                        if (cmd.dimension === "COL") {
                            this.shiftBordersHorizontally(cmd.sheetId, el + 1, -1);
                        }
                        else {
                            this.shiftBordersVertically(cmd.sheetId, el + 1, -1);
                        }
                    }
                    break;
                case "ADD_COLUMNS_ROWS":
                    if (cmd.dimension === "COL") {
                        this.handleAddColumns(cmd);
                    }
                    else {
                        this.handleAddRows(cmd);
                    }
                    break;
            }
        }
        /**
         * Move borders according to the inserted columns.
         * Ensure borders continuity.
         */
        handleAddColumns(cmd) {
            // The new columns have already been inserted in the sheet at this point.
            let colLeftOfInsertion;
            let colRightOfInsertion;
            if (cmd.position === "before") {
                this.shiftBordersHorizontally(cmd.sheetId, cmd.base, cmd.quantity, {
                    moveFirstLeftBorder: true,
                });
                colLeftOfInsertion = cmd.base - 1;
                colRightOfInsertion = cmd.base + cmd.quantity;
            }
            else {
                this.shiftBordersHorizontally(cmd.sheetId, cmd.base + 1, cmd.quantity, {
                    moveFirstLeftBorder: false,
                });
                colLeftOfInsertion = cmd.base;
                colRightOfInsertion = cmd.base + cmd.quantity + 1;
            }
            this.ensureColumnBorderContinuity(cmd.sheetId, colLeftOfInsertion, colRightOfInsertion);
        }
        /**
         * Move borders according to the inserted rows.
         * Ensure borders continuity.
         */
        handleAddRows(cmd) {
            // The new rows have already been inserted at this point.
            let rowAboveInsertion;
            let rowBelowInsertion;
            if (cmd.position === "before") {
                this.shiftBordersVertically(cmd.sheetId, cmd.base, cmd.quantity, {
                    moveFirstTopBorder: true,
                });
                rowAboveInsertion = cmd.base - 1;
                rowBelowInsertion = cmd.base + cmd.quantity;
            }
            else {
                this.shiftBordersVertically(cmd.sheetId, cmd.base + 1, cmd.quantity, {
                    moveFirstTopBorder: false,
                });
                rowAboveInsertion = cmd.base;
                rowBelowInsertion = cmd.base + cmd.quantity + 1;
            }
            this.ensureRowBorderContinuity(cmd.sheetId, rowAboveInsertion, rowBelowInsertion);
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getCellBorder({ sheetId, col, row }) {
            const border = {
                top: this.borders[sheetId]?.[col]?.[row]?.horizontal,
                bottom: this.borders[sheetId]?.[col]?.[row + 1]?.horizontal,
                left: this.borders[sheetId]?.[col]?.[row]?.vertical,
                right: this.borders[sheetId]?.[col + 1]?.[row]?.vertical,
            };
            if (!border.bottom && !border.left && !border.right && !border.top) {
                return null;
            }
            return border;
        }
        getBordersColors(sheetId) {
            const colors = [];
            const sheetBorders = this.borders[sheetId];
            if (sheetBorders) {
                for (const borders of sheetBorders.filter(isDefined$1)) {
                    for (const cellBorder of borders) {
                        if (cellBorder?.horizontal) {
                            colors.push(cellBorder.horizontal.color);
                        }
                        if (cellBorder?.vertical) {
                            colors.push(cellBorder.vertical.color);
                        }
                    }
                }
            }
            return colors;
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        /**
         * Ensure border continuity between two columns.
         * If the two columns have the same borders (at each row respectively),
         * the same borders are applied to each cell in between.
         */
        ensureColumnBorderContinuity(sheetId, leftColumn, rightColumn) {
            const targetCols = range(leftColumn + 1, rightColumn);
            for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
                const leftBorder = this.getCellBorder({ sheetId, col: leftColumn, row });
                const rightBorder = this.getCellBorder({ sheetId, col: rightColumn, row });
                if (leftBorder && rightBorder) {
                    const commonSides = this.getCommonSides(leftBorder, rightBorder);
                    for (let col of targetCols) {
                        this.addBorder(sheetId, col, row, commonSides);
                    }
                }
            }
        }
        /**
         * Ensure border continuity between two rows.
         * If the two rows have the same borders (at each column respectively),
         * the same borders are applied to each cell in between.
         */
        ensureRowBorderContinuity(sheetId, topRow, bottomRow) {
            const targetRows = range(topRow + 1, bottomRow);
            for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {
                const aboveBorder = this.getCellBorder({ sheetId, col, row: topRow });
                const belowBorder = this.getCellBorder({ sheetId, col, row: bottomRow });
                if (aboveBorder && belowBorder) {
                    const commonSides = this.getCommonSides(aboveBorder, belowBorder);
                    for (let row of targetRows) {
                        this.addBorder(sheetId, col, row, commonSides);
                    }
                }
            }
        }
        /**
         * From two borders, return a new border with sides defined in both borders.
         * i.e. the intersection of two borders.
         */
        getCommonSides(border1, border2) {
            const commonBorder = {};
            for (let side of ["top", "bottom", "left", "right"]) {
                if (border1[side] && border1[side] === border2[side]) {
                    commonBorder[side] = border1[side];
                }
            }
            return commonBorder;
        }
        /**
         * Get all the columns which contains at least a border
         */
        getColumnsWithBorders(sheetId) {
            const sheetBorders = this.borders[sheetId];
            if (!sheetBorders)
                return [];
            return Object.keys(sheetBorders).map((index) => parseInt(index, 10));
        }
        /**
         * Get the range of all the rows in the sheet
         */
        getRowsRange(sheetId) {
            const sheetBorders = this.borders[sheetId];
            if (!sheetBorders)
                return [];
            return range(0, this.getters.getNumberRows(sheetId) + 1);
        }
        /**
         * Move borders of a sheet horizontally.
         * @param sheetId
         * @param start starting column (included)
         * @param delta how much borders will be moved (negative if moved to the left)
         */
        shiftBordersHorizontally(sheetId, start, delta, { moveFirstLeftBorder } = {}) {
            const borders = this.borders[sheetId];
            if (!borders)
                return;
            if (delta < 0) {
                this.moveBordersOfColumn(sheetId, start, delta, "vertical", {
                    destructive: false,
                });
            }
            this.getColumnsWithBorders(sheetId)
                .filter((col) => col >= start)
                .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up
                .forEach((col) => {
                if ((col === start && moveFirstLeftBorder) || col !== start) {
                    this.moveBordersOfColumn(sheetId, col, delta, "vertical");
                }
                this.moveBordersOfColumn(sheetId, col, delta, "horizontal");
            });
        }
        /**
         * Move borders of a sheet vertically.
         * @param sheetId
         * @param start starting row (included)
         * @param delta how much borders will be moved (negative if moved to the above)
         */
        shiftBordersVertically(sheetId, start, delta, { moveFirstTopBorder } = {}) {
            const borders = this.borders[sheetId];
            if (!borders)
                return;
            if (delta < 0) {
                this.moveBordersOfRow(sheetId, start, delta, "horizontal", {
                    destructive: false,
                });
            }
            this.getRowsRange(sheetId)
                .filter((row) => row >= start)
                .sort((a, b) => (delta < 0 ? a - b : b - a)) // start by the end when moving up
                .forEach((row) => {
                if ((row === start && moveFirstTopBorder) || row !== start) {
                    this.moveBordersOfRow(sheetId, row, delta, "horizontal");
                }
                this.moveBordersOfRow(sheetId, row, delta, "vertical");
            });
        }
        /**
         * Moves the borders (left if `vertical` or top if `horizontal` depending on
         * `borderDirection`) of all cells in an entire row `delta` rows to the right
         * (`delta` > 0) or to the left (`delta` < 0).
         * Note that as the left of a cell is the right of the cell-1, if the left is
         * moved the right is also moved. However, if `horizontal`, the bottom border
         * is not moved.
         * It does it by replacing the target border by the moved border. If the
         * argument `destructive` is given false, the target border is preserved if
         * the moved border is empty
         */
        moveBordersOfRow(sheetId, row, delta, borderDirection, { destructive } = { destructive: true }) {
            const borders = this.borders[sheetId];
            if (!borders)
                return;
            this.getColumnsWithBorders(sheetId).forEach((col) => {
                const targetBorder = borders[col]?.[row + delta]?.[borderDirection];
                const movedBorder = borders[col]?.[row]?.[borderDirection];
                this.history.update("borders", sheetId, col, row + delta, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);
                this.history.update("borders", sheetId, col, row, borderDirection, undefined);
            });
        }
        /**
         * Moves the borders (left if `vertical` or top if `horizontal` depending on
         * `borderDirection`) of all cells in an entire column `delta` columns below
         * (`delta` > 0) or above (`delta` < 0).
         * Note that as the top of a cell is the bottom of the cell-1, if the top is
         * moved the bottom is also moved. However, if `vertical`, the right border
         * is not moved.
         * It does it by replacing the target border by the moved border. If the
         * argument `destructive` is given false, the target border is preserved if
         * the moved border is empty
         */
        moveBordersOfColumn(sheetId, col, delta, borderDirection, { destructive } = { destructive: true }) {
            const borders = this.borders[sheetId];
            if (!borders)
                return;
            this.getRowsRange(sheetId).forEach((row) => {
                const targetBorder = borders[col + delta]?.[row]?.[borderDirection];
                const movedBorder = borders[col]?.[row]?.[borderDirection];
                this.history.update("borders", sheetId, col + delta, row, borderDirection, destructive ? movedBorder : movedBorder || targetBorder);
                this.history.update("borders", sheetId, col, row, borderDirection, undefined);
            });
        }
        /**
         * Set the borders of a cell.
         * It overrides the current border if override === true.
         */
        setBorder(sheetId, col, row, border, override = true) {
            if (override || !this.borders?.[sheetId]?.[col]?.[row]?.vertical) {
                this.history.update("borders", sheetId, col, row, "vertical", border?.left);
            }
            if (override || !this.borders?.[sheetId]?.[col]?.[row]?.horizontal) {
                this.history.update("borders", sheetId, col, row, "horizontal", border?.top);
            }
            if (override || !this.borders?.[sheetId]?.[col + 1]?.[row]?.vertical) {
                this.history.update("borders", sheetId, col + 1, row, "vertical", border?.right);
            }
            if (override || !this.borders?.[sheetId]?.[col]?.[row + 1]?.horizontal) {
                this.history.update("borders", sheetId, col, row + 1, "horizontal", border?.bottom);
            }
        }
        /**
         * Remove the borders of a zone
         */
        clearBorders(sheetId, zones) {
            for (let zone of zones) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    this.history.update("borders", sheetId, zone.right + 1, row, "vertical", undefined);
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.history.update("borders", sheetId, col, row, undefined);
                    }
                }
                for (let col = zone.left; col <= zone.right; col++) {
                    this.history.update("borders", sheetId, col, zone.bottom + 1, "horizontal", undefined);
                }
            }
        }
        /**
         * Add a border to the existing one to a cell
         */
        addBorder(sheetId, col, row, border) {
            this.setBorder(sheetId, col, row, {
                ...this.getCellBorder({ sheetId, col, row }),
                ...border,
            });
        }
        /**
         * Set the borders of a zone by computing the borders to add from the given
         * command
         */
        setBorders(sheetId, zones, position, border) {
            if (position === "clear") {
                return this.clearBorders(sheetId, zones);
            }
            for (let zone of zones) {
                if (position === "h" || position === "hv" || position === "all") {
                    for (let row = zone.top + 1; row <= zone.bottom; row++) {
                        for (let col = zone.left; col <= zone.right; col++) {
                            this.addBorder(sheetId, col, row, { top: border });
                        }
                    }
                }
                if (position === "v" || position === "hv" || position === "all") {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        for (let col = zone.left + 1; col <= zone.right; col++) {
                            this.addBorder(sheetId, col, row, { left: border });
                        }
                    }
                }
                if (position === "left" || position === "all" || position === "external") {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        this.addBorder(sheetId, zone.left, row, { left: border });
                    }
                }
                if (position === "right" || position === "all" || position === "external") {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        this.addBorder(sheetId, zone.right + 1, row, { left: border });
                    }
                }
                if (position === "top" || position === "all" || position === "external") {
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.addBorder(sheetId, col, zone.top, { top: border });
                    }
                }
                if (position === "bottom" || position === "all" || position === "external") {
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.addBorder(sheetId, col, zone.bottom + 1, { top: border });
                    }
                }
            }
        }
        /**
         * Compute the borders to add to the given zone merged.
         */
        addBordersToMerge(sheetId, zone) {
            const { left, right, top, bottom } = zone;
            const bordersTopLeft = this.getCellBorder({ sheetId, col: left, row: top });
            const bordersBottomRight = this.getCellBorder({ sheetId, col: right, row: bottom });
            this.clearBorders(sheetId, [zone]);
            if (bordersTopLeft?.top) {
                this.setBorders(sheetId, [{ ...zone, bottom: top }], "top", bordersTopLeft.top);
            }
            if (bordersTopLeft?.left) {
                this.setBorders(sheetId, [{ ...zone, right: left }], "left", bordersTopLeft.left);
            }
            if (bordersBottomRight?.bottom) {
                this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersBottomRight.bottom);
            }
            else if (bordersTopLeft?.bottom) {
                this.setBorders(sheetId, [{ ...zone, top: bottom }], "bottom", bordersTopLeft.bottom);
            }
            if (bordersBottomRight?.right) {
                this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersBottomRight.right);
            }
            else if (bordersTopLeft?.right) {
                this.setBorders(sheetId, [{ ...zone, left: right }], "right", bordersTopLeft.right);
            }
        }
        checkBordersUnchanged(cmd) {
            const currentBorder = this.getCellBorder(cmd);
            const areAllNewBordersUndefined = !cmd.border?.bottom && !cmd.border?.left && !cmd.border?.right && !cmd.border?.top;
            if ((!currentBorder && areAllNewBordersUndefined) || deepEquals(currentBorder, cmd.border)) {
                return "NoChanges" /* CommandResult.NoChanges */;
            }
            return "Success" /* CommandResult.Success */;
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            // Borders
            if (Object.keys(data.borders || {}).length) {
                for (let sheet of data.sheets) {
                    for (const xc in sheet.cells) {
                        const cell = sheet.cells[xc];
                        if (cell?.border) {
                            const border = data.borders[cell.border];
                            const { col, row } = toCartesian(xc);
                            this.setBorder(sheet.id, col, row, border, false);
                        }
                    }
                }
            }
            // Merges
            for (let sheetData of data.sheets) {
                if (sheetData.merges) {
                    for (let merge of sheetData.merges) {
                        this.addBordersToMerge(sheetData.id, toZone(merge));
                    }
                }
            }
        }
        export(data) {
            const borders = {};
            for (let sheet of data.sheets) {
                for (let col = 0; col < sheet.colNumber; col++) {
                    for (let row = 0; row < sheet.rowNumber; row++) {
                        const border = this.getCellBorder({ sheetId: sheet.id, col, row });
                        if (border) {
                            const xc = toXC(col, row);
                            const cell = sheet.cells[xc];
                            const borderId = getItemId(border, borders);
                            if (cell) {
                                cell.border = borderId;
                            }
                            else {
                                sheet.cells[xc] = { border: borderId };
                            }
                        }
                    }
                }
            }
            data.borders = borders;
        }
        exportForExcel(data) {
            this.export(data);
        }
    }

    class FunctionCodeBuilder {
        scope;
        code = "";
        constructor(scope = new Scope()) {
            this.scope = scope;
        }
        append(...lines) {
            this.code += lines.map((line) => line.toString()).join("\n") + "\n";
        }
        return(expression) {
            return new FunctionCodeImpl(this.scope, this.code, expression);
        }
        toString() {
            return indentCode(this.code);
        }
    }
    class FunctionCodeImpl {
        scope;
        returnExpression;
        code;
        constructor(scope, code, returnExpression) {
            this.scope = scope;
            this.returnExpression = returnExpression;
            this.code = indentCode(code);
        }
        toString() {
            return this.code;
        }
        wrapInClosure() {
            const closureName = this.scope.nextVariableName();
            const code = new FunctionCodeBuilder(this.scope);
            code.append(`const ${closureName} = () => {`);
            code.append(this.code);
            code.append(`return ${this.returnExpression};`);
            code.append(`}`);
            return code.return(closureName);
        }
        assignResultToVariable() {
            if (this.scope.isAlreadyDeclared(this.returnExpression)) {
                return this;
            }
            const variableName = this.scope.nextVariableName();
            const code = new FunctionCodeBuilder(this.scope);
            code.append(this.code);
            code.append(`const ${variableName} = ${this.returnExpression};`);
            return code.return(variableName);
        }
    }
    class Scope {
        nextId = 1;
        declaredVariables = new Set();
        nextVariableName() {
            const name = `_${this.nextId++}`;
            this.declaredVariables.add(name);
            return name;
        }
        isAlreadyDeclared(name) {
            return this.declaredVariables.has(name);
        }
    }
    /**
     * Takes a list of strings that might be single or multiline
     * and maps them in a list of single line strings.
     */
    function splitLines(str) {
        return str
            .split("\n")
            .map((line) => line.trim())
            .filter((line) => line !== "");
    }
    function indentCode(code) {
        let result = "";
        let indentLevel = 0;
        const lines = splitLines(code);
        for (const line of lines) {
            if (line.startsWith("}")) {
                indentLevel--;
            }
            result += "\t".repeat(indentLevel) + line + "\n";
            if (line.endsWith("{")) {
                indentLevel++;
            }
        }
        return result.trim();
    }

    const functionRegex = /[a-zA-Z0-9\_]+(\.[a-zA-Z0-9\_]+)*/;
    const UNARY_OPERATORS_PREFIX = ["-", "+"];
    const UNARY_OPERATORS_POSTFIX = ["%"];
    const ASSOCIATIVE_OPERATORS = ["*", "+", "&"];
    const OP_PRIORITY = {
        "^": 30,
        "%": 30,
        "*": 20,
        "/": 20,
        "+": 15,
        "-": 15,
        "&": 13,
        ">": 10,
        "<>": 10,
        ">=": 10,
        "<": 10,
        "<=": 10,
        "=": 10,
    };
    /**
     * Parse the next operand in an arithmetic expression.
     * e.g.
     *  for 1+2*3, the next operand is 1
     *  for (1+2)*3, the next operand is (1+2)
     *  for SUM(1,2)+3, the next operand is SUM(1,2)
     */
    function parseOperand(tokens) {
        const current = tokens.shift();
        if (!current) {
            throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
        }
        switch (current.type) {
            case "DEBUGGER":
                const next = parseExpression(tokens, 1000);
                next.debug = true;
                return next;
            case "NUMBER":
                return { type: "NUMBER", value: parseNumber(current.value, DEFAULT_LOCALE) };
            case "STRING":
                return { type: "STRING", value: removeStringQuotes(current.value) };
            case "INVALID_REFERENCE":
                throw new InvalidReferenceError();
            case "REFERENCE":
                if (tokens[0]?.value === ":" && tokens[1]?.type === "REFERENCE") {
                    tokens.shift();
                    const rightReference = tokens.shift();
                    return {
                        type: "REFERENCE",
                        value: `${current.value}:${rightReference?.value}`,
                    };
                }
                return {
                    type: "REFERENCE",
                    value: current.value,
                };
            case "SYMBOL":
                const value = current.value;
                const nextToken = tokens[0];
                if (nextToken?.type === "LEFT_PAREN" && functionRegex.test(current.value)) {
                    const args = parseFunctionArgs(tokens);
                    return { type: "FUNCALL", value: value, args };
                }
                const upperCaseValue = value.toUpperCase();
                if (upperCaseValue === "TRUE" || upperCaseValue === "FALSE") {
                    return { type: "BOOLEAN", value: upperCaseValue === "TRUE" };
                }
                throw new BadExpressionError(_t("Invalid formula"));
            case "LEFT_PAREN":
                const result = parseExpression(tokens);
                consumeOrThrow(tokens, "RIGHT_PAREN", _t("Missing closing parenthesis"));
                return result;
            case "OPERATOR":
                const operator = current.value;
                if (UNARY_OPERATORS_PREFIX.includes(operator)) {
                    return {
                        type: "UNARY_OPERATION",
                        value: operator,
                        operand: parseExpression(tokens, OP_PRIORITY[operator]),
                    };
                }
                throw new BadExpressionError(_t("Unexpected token: %s", current.value));
            default:
                throw new BadExpressionError(_t("Unexpected token: %s", current.value));
        }
    }
    function parseFunctionArgs(tokens) {
        consumeOrThrow(tokens, "LEFT_PAREN", _t("Missing opening parenthesis"));
        const nextToken = tokens[0];
        if (nextToken?.type === "RIGHT_PAREN") {
            consumeOrThrow(tokens, "RIGHT_PAREN");
            return [];
        }
        const args = [];
        args.push(parseOneFunctionArg(tokens));
        while (tokens[0]?.type !== "RIGHT_PAREN") {
            consumeOrThrow(tokens, "ARG_SEPARATOR", _t("Wrong function call"));
            args.push(parseOneFunctionArg(tokens));
        }
        consumeOrThrow(tokens, "RIGHT_PAREN");
        return args;
    }
    function parseOneFunctionArg(tokens) {
        const nextToken = tokens[0];
        if (nextToken?.type === "ARG_SEPARATOR" || nextToken?.type === "RIGHT_PAREN") {
            // arg is empty: "sum(1,,2)" "sum(,1)" "sum(1,)"
            return { type: "EMPTY", value: "" };
        }
        return parseExpression(tokens);
    }
    function consumeOrThrow(tokens, type, message = DEFAULT_ERROR_MESSAGE) {
        const token = tokens.shift();
        if (!token || token.type !== type) {
            throw new BadExpressionError(message);
        }
    }
    function parseExpression(tokens, parent_priority = 0) {
        if (tokens.length === 0) {
            throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
        }
        let left = parseOperand(tokens);
        // as long as we have operators with higher priority than the parent one,
        // continue parsing the expression because it is a child sub-expression
        while (tokens[0]?.type === "OPERATOR" && OP_PRIORITY[tokens[0].value] > parent_priority) {
            const operator = tokens.shift().value;
            if (UNARY_OPERATORS_POSTFIX.includes(operator)) {
                left = {
                    type: "UNARY_OPERATION",
                    value: operator,
                    operand: left,
                    postfix: true,
                };
            }
            else {
                const right = parseExpression(tokens, OP_PRIORITY[operator]);
                left = {
                    type: "BIN_OPERATION",
                    value: operator,
                    left,
                    right,
                };
            }
        }
        return left;
    }
    /**
     * Parse an expression (as a string) into an AST.
     */
    function parse(str) {
        return parseTokens(tokenize(str));
    }
    function parseTokens(tokens) {
        tokens = tokens.filter((x) => x.type !== "SPACE");
        if (tokens[0].value === "=") {
            tokens.splice(0, 1);
        }
        const result = parseExpression(tokens);
        if (tokens.length) {
            throw new BadExpressionError(DEFAULT_ERROR_MESSAGE);
        }
        return result;
    }
    /**
     * Allows to visit all nodes of an AST and apply a mapping function
     * to nodes of a specific type.
     * Useful if you want to convert some part of a formula.
     *
     * @example
     * convertAstNodes(ast, "FUNCALL", convertFormulaToExcel)
     *
     * function convertFormulaToExcel(ast: ASTFuncall) {
     *   // ...
     *   return modifiedAst
     * }
     */
    function convertAstNodes(ast, type, fn) {
        return mapAst(ast, (ast) => {
            if (ast.type === type) {
                return fn(ast);
            }
            return ast;
        });
    }
    function iterateAstNodes(ast) {
        return Array.from(astIterator(ast));
    }
    function* astIterator(ast) {
        yield ast;
        switch (ast.type) {
            case "FUNCALL":
                for (const arg of ast.args) {
                    yield* astIterator(arg);
                }
                break;
            case "UNARY_OPERATION":
                yield* astIterator(ast.operand);
                break;
            case "BIN_OPERATION":
                yield* astIterator(ast.left);
                yield* astIterator(ast.right);
                break;
        }
    }
    function mapAst(ast, fn) {
        ast = fn(ast);
        switch (ast.type) {
            case "FUNCALL":
                return {
                    ...ast,
                    args: ast.args.map((child) => mapAst(child, fn)),
                };
            case "UNARY_OPERATION":
                return {
                    ...ast,
                    operand: mapAst(ast.operand, fn),
                };
            case "BIN_OPERATION":
                return {
                    ...ast,
                    right: mapAst(ast.right, fn),
                    left: mapAst(ast.left, fn),
                };
            default:
                return ast;
        }
    }
    /**
     * Converts an ast formula to the corresponding string
     */
    function astToFormula(ast) {
        switch (ast.type) {
            case "FUNCALL":
                const args = ast.args.map((arg) => astToFormula(arg));
                return `${ast.value}(${args.join(",")})`;
            case "NUMBER":
                return ast.value.toString();
            case "REFERENCE":
                return ast.value;
            case "STRING":
                return `"${ast.value}"`;
            case "BOOLEAN":
                return ast.value ? "TRUE" : "FALSE";
            case "UNARY_OPERATION":
                return ast.postfix
                    ? leftOperandToFormula(ast) + ast.value
                    : ast.value + rightOperandToFormula(ast);
            case "BIN_OPERATION":
                return leftOperandToFormula(ast) + ast.value + rightOperandToFormula(ast);
            default:
                return ast.value;
        }
    }
    /**
     * Convert the left operand of a binary operation to the corresponding string
     * and enclose the result inside parenthesis if necessary.
     */
    function leftOperandToFormula(operationAST) {
        const mainOperator = operationAST.value;
        const leftOperation = "left" in operationAST ? operationAST.left : operationAST.operand;
        const leftOperator = leftOperation.value;
        const needParenthesis = leftOperation.type === "BIN_OPERATION" && OP_PRIORITY[leftOperator] < OP_PRIORITY[mainOperator];
        return needParenthesis ? `(${astToFormula(leftOperation)})` : astToFormula(leftOperation);
    }
    /**
     * Convert the right operand of a binary or unary operation to the corresponding string
     * and enclose the result inside parenthesis if necessary.
     */
    function rightOperandToFormula(operationAST) {
        const mainOperator = operationAST.value;
        const rightOperation = "right" in operationAST ? operationAST.right : operationAST.operand;
        const rightPriority = OP_PRIORITY[rightOperation.value];
        const mainPriority = OP_PRIORITY[mainOperator];
        let needParenthesis = false;
        if (rightOperation.type !== "BIN_OPERATION") {
            needParenthesis = false;
        }
        else if (rightPriority < mainPriority) {
            needParenthesis = true;
        }
        else if (rightPriority === mainPriority && !ASSOCIATIVE_OPERATORS.includes(mainOperator)) {
            needParenthesis = true;
        }
        return needParenthesis ? `(${astToFormula(rightOperation)})` : astToFormula(rightOperation);
    }

    const functions$1 = functionRegistry.content;
    const OPERATOR_MAP = {
        "=": "EQ",
        "+": "ADD",
        "-": "MINUS",
        "*": "MULTIPLY",
        "/": "DIVIDE",
        ">=": "GTE",
        "<>": "NE",
        ">": "GT",
        "<=": "LTE",
        "<": "LT",
        "^": "POWER",
        "&": "CONCATENATE",
    };
    const UNARY_OPERATOR_MAP = {
        "-": "UMINUS",
        "+": "UPLUS",
        "%": "UNARY.PERCENT",
    };
    // this cache contains all compiled function code, grouped by "structure". For
    // example, "=2*sum(A1:A4)" and "=2*sum(B1:B4)" are compiled into the same
    // structural function.
    // It is only exported for testing purposes
    const functionCache = {};
    // -----------------------------------------------------------------------------
    // COMPILER
    // -----------------------------------------------------------------------------
    function compile(formula) {
        const tokens = rangeTokenize(formula);
        return compileTokens(tokens);
    }
    function compileTokens(tokens) {
        const { dependencies, constantValues } = formulaArguments(tokens);
        const cacheKey = compilationCacheKey(tokens, dependencies, constantValues);
        if (!functionCache[cacheKey]) {
            const ast = parseTokens([...tokens]);
            const scope = new Scope();
            if (ast.type === "BIN_OPERATION" && ast.value === ":") {
                throw new BadExpressionError(_t("Invalid formula"));
            }
            if (ast.type === "EMPTY") {
                throw new BadExpressionError(_t("Invalid formula"));
            }
            const compiledAST = compileAST(ast);
            const code = new FunctionCodeBuilder();
            code.append(`// ${cacheKey}`);
            code.append(compiledAST);
            code.append(`return ${compiledAST.returnExpression};`);
            let baseFunction = new Function("deps", // the dependencies in the current formula
            "ref", // a function to access a certain dependency at a given index
            "range", // same as above, but guarantee that the result is in the form of a range
            "ctx", code.toString());
            functionCache[cacheKey] = {
                // @ts-ignore
                execute: baseFunction,
            };
            /**
             * This function compile the function arguments. It is mostly straightforward,
             * except that there is a non trivial transformation in one situation:
             *
             * If a function argument is asking for a range, and get a cell, we transform
             * the cell value into a range. This allow the grid model to differentiate
             * between a cell value and a non cell value.
             */
            function compileFunctionArgs(ast) {
                const { args } = ast;
                const functionName = ast.value.toUpperCase();
                const functionDefinition = functions$1[functionName];
                if (!functionDefinition) {
                    throw new UnknownFunctionError(ast.value);
                }
                assertEnoughArgs(ast);
                const compiledArgs = [];
                for (let i = 0; i < args.length; i++) {
                    const argToFocus = functionDefinition.getArgToFocus(i + 1) - 1;
                    const argDefinition = functionDefinition.args[argToFocus];
                    const currentArg = args[i];
                    const argTypes = argDefinition.type || [];
                    // detect when an argument need to be evaluated as a meta argument
                    const isMeta = argTypes.includes("META");
                    // detect when an argument need to be evaluated as a lazy argument
                    const isLazy = argDefinition.lazy;
                    const hasRange = argTypes.some((t) => isRangeType(t));
                    const isRangeOnly = argTypes.every((t) => isRangeType(t));
                    if (isRangeOnly) {
                        if (!isRangeInput(currentArg)) {
                            throw new BadExpressionError(_t("Function %s expects the parameter %s to be reference to a cell or range, not a %s.", functionName, (i + 1).toString(), currentArg.type.toLowerCase()));
                        }
                    }
                    const compiledAST = compileAST(currentArg, isMeta, hasRange, {
                        functionName,
                        paramIndex: i + 1,
                    });
                    compiledArgs.push(isLazy ? compiledAST.wrapInClosure() : compiledAST);
                }
                return compiledArgs;
            }
            /**
             * This function compiles all the information extracted by the parser into an
             * executable code for the evaluation of the cells content. It uses a cash to
             * not reevaluate identical code structures.
             *
             * The function is sensitive to parameter “isMeta”. This
             * parameter may vary when compiling function arguments:
             * isMeta: In some cases the function arguments expects information on the
             * cell/range other than the associated value(s). For example the COLUMN
             * function needs to receive as argument the coordinates of a cell rather
             * than its value. For this we have meta arguments.
             */
            function compileAST(ast, isMeta = false, hasRange = false, referenceVerification = {}) {
                const code = new FunctionCodeBuilder(scope);
                if (ast.type !== "REFERENCE" && !(ast.type === "BIN_OPERATION" && ast.value === ":")) {
                    if (isMeta) {
                        throw new BadExpressionError(_t("Argument must be a reference to a cell or range."));
                    }
                }
                if (ast.debug) {
                    code.append("debugger;");
                }
                switch (ast.type) {
                    case "BOOLEAN":
                        return code.return(`{ value: ${ast.value} }`);
                    case "NUMBER":
                        return code.return(`{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`);
                    case "STRING":
                        return code.return(`{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`);
                    case "REFERENCE":
                        const referenceIndex = dependencies.indexOf(ast.value);
                        if (hasRange) {
                            return code.return(`range(deps[${referenceIndex}])`);
                        }
                        else {
                            return code.return(`ref(deps[${referenceIndex}], ${isMeta ? "true" : "false"}, "${referenceVerification.functionName || OPERATOR_MAP["="]}",  ${referenceVerification.paramIndex})`);
                        }
                    case "FUNCALL":
                        const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
                        code.append(...args);
                        const fnName = ast.value.toUpperCase();
                        code.append(`ctx.__lastFnCalled = '${fnName}';`);
                        return code.return(`ctx['${fnName}'](${args.map((arg) => arg.returnExpression)})`);
                    case "UNARY_OPERATION": {
                        const fnName = UNARY_OPERATOR_MAP[ast.value];
                        const operand = compileAST(ast.operand, false, false, {
                            functionName: fnName,
                        }).assignResultToVariable();
                        code.append(operand);
                        code.append(`ctx.__lastFnCalled = '${fnName}';`);
                        return code.return(`ctx['${fnName}'](${operand.returnExpression})`);
                    }
                    case "BIN_OPERATION": {
                        const fnName = OPERATOR_MAP[ast.value];
                        const left = compileAST(ast.left, false, false, {
                            functionName: fnName,
                        }).assignResultToVariable();
                        const right = compileAST(ast.right, false, false, {
                            functionName: fnName,
                        }).assignResultToVariable();
                        code.append(left);
                        code.append(right);
                        code.append(`ctx.__lastFnCalled = '${fnName}';`);
                        return code.return(`ctx['${fnName}'](${left.returnExpression}, ${right.returnExpression})`);
                    }
                    case "EMPTY":
                        return code.return("undefined");
                }
            }
        }
        const compiledFormula = {
            execute: functionCache[cacheKey].execute,
            dependencies,
            constantValues,
            tokens,
        };
        return compiledFormula;
    }
    /**
     * Compute a cache key for the formula.
     * References, numbers and strings are replaced with placeholders because
     * the compiled formula does not depend on their actual value.
     * Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function.
     *
     * Spaces are also ignored to compute the cache key.
     *
     * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)`
     */
    function compilationCacheKey(tokens, dependencies, constantValues) {
        return concat(tokens.map((token) => {
            switch (token.type) {
                case "STRING":
                    const value = removeStringQuotes(token.value);
                    return `|S${constantValues.strings.indexOf(value)}|`;
                case "NUMBER":
                    return `|N${constantValues.numbers.indexOf(parseNumber(token.value, DEFAULT_LOCALE))}|`;
                case "REFERENCE":
                case "INVALID_REFERENCE":
                    return `|${dependencies.indexOf(token.value)}|`;
                case "SPACE":
                    return "";
                default:
                    return token.value;
            }
        }));
    }
    /**
     * Return formula arguments which are references, strings and numbers.
     */
    function formulaArguments(tokens) {
        const constantValues = {
            numbers: [],
            strings: [],
        };
        const dependencies = [];
        for (const token of tokens) {
            switch (token.type) {
                case "INVALID_REFERENCE":
                case "REFERENCE":
                    dependencies.push(token.value);
                    break;
                case "STRING":
                    const value = removeStringQuotes(token.value);
                    if (!constantValues.strings.includes(value)) {
                        constantValues.strings.push(value);
                    }
                    break;
                case "NUMBER": {
                    const value = parseNumber(token.value, DEFAULT_LOCALE);
                    if (!constantValues.numbers.includes(value)) {
                        constantValues.numbers.push(value);
                    }
                    break;
                }
            }
        }
        return {
            dependencies,
            constantValues,
        };
    }
    /**
     * Check if arguments are supplied in the correct quantities
     */
    function assertEnoughArgs(ast) {
        const nbrArg = ast.args.length;
        const functionName = ast.value.toUpperCase();
        const functionDefinition = functions$1[functionName];
        if (nbrArg < functionDefinition.minArgRequired) {
            throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s minimum, but got %s instead.", functionName, functionDefinition.minArgRequired.toString(), nbrArg.toString()));
        }
        if (nbrArg > functionDefinition.maxArgPossible) {
            throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected %s maximum, but got %s instead.", functionName, functionDefinition.maxArgPossible.toString(), nbrArg.toString()));
        }
        const repeatableArgs = functionDefinition.nbrArgRepeating;
        if (repeatableArgs > 1) {
            const unrepeatableArgs = functionDefinition.args.length - repeatableArgs;
            const repeatingArgs = nbrArg - unrepeatableArgs;
            if (repeatingArgs % repeatableArgs !== 0) {
                throw new BadExpressionError(_t("Invalid number of arguments for the %s function. Expected all arguments after position %s to be supplied by groups of %s arguments", functionName, unrepeatableArgs.toString(), repeatableArgs.toString()));
            }
        }
    }
    function isRangeType(type) {
        return type.startsWith("RANGE");
    }
    function isRangeInput(arg) {
        if (arg.type === "REFERENCE") {
            return true;
        }
        if (arg.type === "FUNCALL") {
            const fnDef = functions$1[arg.value.toUpperCase()];
            return fnDef && isRangeType(fnDef.returns[0]);
        }
        return false;
    }

    /**
     * Add the following information on tokens:
     * - length
     * - start
     * - end
     */
    function enrichTokens(tokens) {
        let current = 0;
        return tokens.map((x) => {
            const len = x.value.toString().length;
            const token = Object.assign({}, x, {
                start: current,
                end: current + len,
                length: len,
            });
            current = token.end;
            return token;
        });
    }
    /**
     * add on each token the length, start and end
     * also matches the opening to its closing parenthesis (using the same number)
     */
    function mapParenthesis(tokens) {
        let maxParen = 1;
        const stack = [];
        return tokens.map((token) => {
            if (token.type === "LEFT_PAREN") {
                stack.push(maxParen);
                token.parenIndex = maxParen;
                maxParen++;
            }
            else if (token.type === "RIGHT_PAREN") {
                token.parenIndex = stack.pop();
            }
            return token;
        });
    }
    /**
     * add on each token its parent function and the index corresponding to
     * its position as an argument of the function.
     * In this example "=MIN(42,SUM(MAX(1,2),3))":
     * - the parent function of the token correspond to number 42 is the MIN function
     * - the argument position of the token correspond to number 42 is 0
     * - the parent function of the token correspond to number 3 is the SUM function
     * - the argument position of the token correspond to number 3 is 1
     */
    function mapParentFunction(tokens) {
        let stack = [];
        let functionStarted = "";
        const res = tokens.map((token, i) => {
            if (!["SPACE", "LEFT_PAREN"].includes(token.type)) {
                functionStarted = "";
            }
            switch (token.type) {
                case "SYMBOL":
                    functionStarted = token.value;
                    break;
                case "LEFT_PAREN":
                    stack.push({ parent: functionStarted, argPosition: 0 });
                    functionStarted = "";
                    break;
                case "RIGHT_PAREN":
                    stack.pop();
                    break;
                case "ARG_SEPARATOR":
                    if (stack.length) {
                        // increment position on current function
                        stack[stack.length - 1].argPosition++;
                    }
                    break;
            }
            if (stack.length) {
                const functionContext = stack[stack.length - 1];
                if (functionContext.parent) {
                    token.functionContext = Object.assign({}, functionContext);
                }
            }
            return token;
        });
        return res;
    }
    /**
     * Take the result of the tokenizer and transform it to be usable in the composer.
     *
     * @param formula
     */
    function composerTokenize(formula, locale) {
        const tokens = rangeTokenize(formula, locale);
        return mapParentFunction(mapParenthesis(enrichTokens(tokens)));
    }

    const functions = functionRegistry.content;
    function isExportableToExcel(tokens) {
        try {
            const nonExportableFunctions = iterateAstNodes(parseTokens(tokens)).filter((ast) => ast.type === "FUNCALL" && !functions[ast.value.toUpperCase()]?.isExported);
            return nonExportableFunctions.length === 0;
        }
        catch (error) {
            return false;
        }
    }

    /**
     * Core Plugin
     *
     * This is the most fundamental of all plugins. It defines how to interact with
     * cell and sheet content.
     */
    class CellPlugin extends CorePlugin {
        static getters = [
            "zoneToXC",
            "getCells",
            "getFormulaCellContent",
            "getTranslatedCellFormula",
            "getCellStyle",
            "getCellById",
        ];
        nextId = 1;
        cells = {};
        adaptRanges(applyChange, sheetId) {
            for (const sheet of Object.keys(this.cells)) {
                for (const cell of Object.values(this.cells[sheet] || {})) {
                    if (cell.isFormula) {
                        for (const range of cell.compiledFormula.dependencies) {
                            if (!sheetId || range.sheetId === sheetId) {
                                const change = applyChange(range);
                                if (change.changeType !== "NONE") {
                                    this.history.update("cells", sheet, cell.id, "compiledFormula", "dependencies", cell.compiledFormula.dependencies.indexOf(range), change.range);
                                }
                            }
                        }
                    }
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "UPDATE_CELL":
                    return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessUpdateCell);
                case "CLEAR_CELL":
                    return this.checkValidations(cmd, this.checkCellOutOfSheet, this.checkUselessClearCell);
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "SET_FORMATTING":
                    if ("style" in cmd) {
                        this.setStyle(cmd.sheetId, cmd.target, cmd.style);
                    }
                    if ("format" in cmd && cmd.format !== undefined) {
                        this.setFormatter(cmd.sheetId, cmd.target, cmd.format);
                    }
                    break;
                case "CLEAR_FORMATTING":
                    this.clearFormatting(cmd.sheetId, cmd.target);
                    break;
                case "ADD_COLUMNS_ROWS":
                    if (cmd.dimension === "COL") {
                        this.handleAddColumnsRows(cmd, this.copyColumnStyle.bind(this));
                    }
                    else {
                        this.handleAddColumnsRows(cmd, this.copyRowStyle.bind(this));
                    }
                    break;
                case "UPDATE_CELL":
                    this.updateCell(cmd.sheetId, cmd.col, cmd.row, cmd);
                    break;
                case "CLEAR_CELL":
                    this.dispatch("UPDATE_CELL", {
                        sheetId: cmd.sheetId,
                        col: cmd.col,
                        row: cmd.row,
                        content: "",
                        style: null,
                        format: "",
                    });
                    break;
            }
        }
        /**
         * Set a format to all the cells in a zone
         */
        setFormatter(sheetId, zones, format) {
            for (let zone of zones) {
                for (let row = zone.top; row <= zone.bottom; row++) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        this.dispatch("UPDATE_CELL", {
                            sheetId,
                            col,
                            row,
                            format,
                        });
                    }
                }
            }
        }
        /**
         * Clear the styles and format of zones
         */
        clearFormatting(sheetId, zones) {
            for (let zone of zones) {
                for (let col = zone.left; col <= zone.right; col++) {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        // commandHelpers.updateCell(sheetId, col, row, { style: undefined});
                        this.dispatch("UPDATE_CELL", {
                            sheetId,
                            col,
                            row,
                            style: null,
                            format: "",
                        });
                    }
                }
            }
        }
        /**
         * Copy the style of the reference column/row to the new columns/rows.
         */
        handleAddColumnsRows(cmd, fn) {
            // The new elements have already been inserted in the sheet at this point.
            let insertedElements;
            let styleReference;
            if (cmd.position === "before") {
                insertedElements = range(cmd.base, cmd.base + cmd.quantity);
                styleReference = cmd.base + cmd.quantity;
            }
            else {
                insertedElements = range(cmd.base + 1, cmd.base + cmd.quantity + 1);
                styleReference = cmd.base;
            }
            fn(cmd.sheetId, styleReference, insertedElements);
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            for (let sheet of data.sheets) {
                // cells
                for (let xc in sheet.cells) {
                    const cellData = sheet.cells[xc];
                    const { col, row } = toCartesian(xc);
                    if (cellData?.content || cellData?.format || cellData?.style) {
                        const cell = this.importCell(sheet.id, cellData, data.styles, data.formats);
                        this.history.update("cells", sheet.id, cell.id, cell);
                        this.dispatch("UPDATE_CELL_POSITION", {
                            cellId: cell.id,
                            col,
                            row,
                            sheetId: sheet.id,
                        });
                    }
                }
            }
        }
        export(data) {
            const styles = {};
            const formats = {};
            for (let _sheet of data.sheets) {
                const cells = {};
                const positions = Object.keys(this.cells[_sheet.id] || {})
                    .map((cellId) => this.getters.getCellPosition(cellId))
                    .sort((a, b) => (a.col === b.col ? a.row - b.row : a.col - b.col));
                for (const position of positions) {
                    const cell = this.getters.getCell(position);
                    const xc = toXC(position.col, position.row);
                    const style = this.removeDefaultStyleValues(cell.style);
                    cells[xc] = {
                        style: Object.keys(style).length ? getItemId(style, styles) : undefined,
                        format: cell.format ? getItemId(cell.format, formats) : undefined,
                        content: cell.content || undefined,
                    };
                }
                _sheet.cells = cells;
            }
            data.styles = styles;
            data.formats = formats;
        }
        importCell(sheetId, cellData, normalizedStyles, normalizedFormats) {
            const style = (cellData.style && normalizedStyles[cellData.style]) || undefined;
            const format = (cellData.format && normalizedFormats[cellData.format]) || undefined;
            const cellId = this.getNextUid();
            return this.createCell(cellId, cellData?.content || "", format, style, sheetId);
        }
        exportForExcel(data) {
            this.export(data);
        }
        removeDefaultStyleValues(style) {
            const cleanedStyle = { ...style };
            for (const property in DEFAULT_STYLE) {
                if (cleanedStyle[property] === DEFAULT_STYLE[property]) {
                    delete cleanedStyle[property];
                }
            }
            return cleanedStyle;
        }
        // ---------------------------------------------------------------------------
        // GETTERS
        // ---------------------------------------------------------------------------
        getCells(sheetId) {
            return this.cells[sheetId] || {};
        }
        /**
         * get a cell by ID. Used in evaluation when evaluating an async cell, we need to be able to find it back after
         * starting an async evaluation even if it has been moved or re-allocated
         */
        getCellById(cellId) {
            // this must be as fast as possible
            const position = this.getters.getCellPosition(cellId);
            const sheet = this.cells[position.sheetId];
            return sheet[cellId];
        }
        /*
         * Reconstructs the original formula string based on a normalized form and its dependencies
         */
        getFormulaCellContent(sheetId, compiledFormula, dependencies, useFixedReference = false) {
            const ranges = dependencies || compiledFormula.dependencies;
            let rangeIndex = 0;
            return concat(compiledFormula.tokens.map((token) => {
                if (token.type === "REFERENCE") {
                    const range = ranges[rangeIndex++];
                    return this.getters.getRangeString(range, sheetId, { useFixedReference });
                }
                return token.value;
            }));
        }
        /*
         * Constructs a formula string based on an initial formula and a translation vector
         */
        getTranslatedCellFormula(sheetId, offsetX, offsetY, compiledFormula) {
            const adaptedDependencies = this.getters.createAdaptedRanges(compiledFormula.dependencies, offsetX, offsetY, sheetId);
            return this.getFormulaCellContent(sheetId, compiledFormula, adaptedDependencies);
        }
        getCellStyle(position) {
            return this.getters.getCell(position)?.style || {};
        }
        /**
         * Converts a zone to a XC coordinate system
         *
         * The conversion also treats merges as one single cell
         *
         * Examples:
         * {top:0,left:0,right:0,bottom:0} ==> A1
         * {top:0,left:0,right:1,bottom:1} ==> A1:B2
         *
         * if A1:B2 is a merge:
         * {top:0,left:0,right:1,bottom:1} ==> A1
         * {top:1,left:0,right:1,bottom:2} ==> A1:B3
         *
         * if A1:B2 and A4:B5 are merges:
         * {top:1,left:0,right:1,bottom:3} ==> A1:A5
         */
        zoneToXC(sheetId, zone, fixedParts = [{ colFixed: false, rowFixed: false }]) {
            zone = this.getters.expandZone(sheetId, zone);
            const topLeft = toXC(zone.left, zone.top, fixedParts[0]);
            const botRight = toXC(zone.right, zone.bottom, fixedParts.length > 1 ? fixedParts[1] : fixedParts[0]);
            const cellTopLeft = this.getters.getMainCellPosition({
                sheetId,
                col: zone.left,
                row: zone.top,
            });
            const cellBotRight = this.getters.getMainCellPosition({
                sheetId,
                col: zone.right,
                row: zone.bottom,
            });
            const sameCell = cellTopLeft.col === cellBotRight.col && cellTopLeft.row === cellBotRight.row;
            if (topLeft != botRight && !sameCell) {
                return topLeft + ":" + botRight;
            }
            return topLeft;
        }
        setStyle(sheetId, target, style) {
            for (let zone of target) {
                for (let col = zone.left; col <= zone.right; col++) {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        const cell = this.getters.getCell({ sheetId, col, row });
                        this.dispatch("UPDATE_CELL", {
                            sheetId,
                            col,
                            row,
                            style: style ? { ...cell?.style, ...style } : undefined,
                        });
                    }
                }
            }
        }
        /**
         * Copy the style of one column to other columns.
         */
        copyColumnStyle(sheetId, refColumn, targetCols) {
            for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
                const format = this.getFormat(sheetId, refColumn, row);
                if (format.style || format.format) {
                    for (let col of targetCols) {
                        this.dispatch("UPDATE_CELL", { sheetId, col, row, ...format });
                    }
                }
            }
        }
        /**
         * Copy the style of one row to other rows.
         */
        copyRowStyle(sheetId, refRow, targetRows) {
            for (let col = 0; col < this.getters.getNumberCols(sheetId); col++) {
                const format = this.getFormat(sheetId, col, refRow);
                if (format.style || format.format) {
                    for (let row of targetRows) {
                        this.dispatch("UPDATE_CELL", { sheetId, col, row, ...format });
                    }
                }
            }
        }
        /**
         * gets the currently used style/border of a cell based on it's coordinates
         */
        getFormat(sheetId, col, row) {
            const format = {};
            const position = this.getters.getMainCellPosition({ sheetId, col, row });
            const cell = this.getters.getCell(position);
            if (cell) {
                if (cell.style) {
                    format["style"] = cell.style;
                }
                if (cell.format) {
                    format["format"] = cell.format;
                }
            }
            return format;
        }
        getNextUid() {
            const id = this.nextId.toString();
            this.history.update("nextId", this.nextId + 1);
            return id;
        }
        updateCell(sheetId, col, row, after) {
            const before = this.getters.getCell({ sheetId, col, row });
            const hasContent = "content" in after || "formula" in after;
            // Compute the new cell properties
            const afterContent = hasContent ? replaceSpecialSpaces(after?.content) : before?.content || "";
            let style;
            if (after.style !== undefined) {
                style = after.style || undefined;
            }
            else {
                style = before ? before.style : undefined;
            }
            const format = "format" in after ? after.format : before && before.format;
            /* Read the following IF as:
             * we need to remove the cell if it is completely empty, but we can know if it completely empty if:
             * - the command says the new content is empty and has no border/format/style
             * - the command has no content property, in this case
             *     - either there wasn't a cell at this place and the command says border/format/style is empty
             *     - or there was a cell at this place, but it's an empty cell and the command says border/format/style is empty
             *  */
            if (((hasContent && !afterContent && !after.formula) ||
                (!hasContent && (!before || before.content === ""))) &&
                !style &&
                !format) {
                if (before) {
                    this.history.update("cells", sheetId, before.id, undefined);
                    this.dispatch("UPDATE_CELL_POSITION", {
                        cellId: undefined,
                        col,
                        row,
                        sheetId,
                    });
                }
                return;
            }
            const cellId = before?.id || this.getNextUid();
            const cell = this.createCell(cellId, afterContent, format, style, sheetId);
            this.history.update("cells", sheetId, cell.id, cell);
            this.dispatch("UPDATE_CELL_POSITION", { cellId: cell.id, col, row, sheetId });
        }
        createCell(id, content, format, style, sheetId) {
            if (!content.startsWith("=")) {
                return this.createLiteralCell(id, content, format, style);
            }
            try {
                return this.createFormulaCell(id, content, format, style, sheetId);
            }
            catch (error) {
                return this.createErrorFormula(id, content, format, style, error);
            }
        }
        createLiteralCell(id, content, format, style) {
            const locale = this.getters.getLocale();
            return {
                id,
                content: parseLiteral(content, locale).toString(),
                style,
                format: format || detectDateFormat(content, locale) || detectNumberFormat(content),
                isFormula: false,
            };
        }
        createFormulaCell(id, content, format, style, sheetId) {
            const compiledFormula = compile(content);
            if (compiledFormula.dependencies.length) {
                return this.createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId);
            }
            return {
                id,
                content,
                style,
                format,
                isFormula: true,
                compiledFormula: {
                    ...compiledFormula,
                    dependencies: [],
                },
            };
        }
        /**
         * Create a new formula cell with the content
         * being a computed property to rebuild the dependencies XC.
         */
        createFormulaCellWithDependencies(id, compiledFormula, format, style, sheetId) {
            const dependencies = [];
            for (const xc of compiledFormula.dependencies) {
                dependencies.push(this.getters.getRangeFromSheetXC(sheetId, xc));
            }
            return new FormulaCellWithDependencies(id, compiledFormula, format, style, dependencies, sheetId, this.getters.getRangeString);
        }
        createErrorFormula(id, content, format, style, error) {
            return {
                id,
                content,
                style,
                format,
                isFormula: true,
                compiledFormula: {
                    tokens: tokenize(content),
                    dependencies: [],
                    execute: function () {
                        throw error;
                    },
                },
            };
        }
        checkCellOutOfSheet(cmd) {
            const { sheetId, col, row } = cmd;
            const sheet = this.getters.tryGetSheet(sheetId);
            if (!sheet)
                return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
            const sheetZone = this.getters.getSheetZone(sheetId);
            return isInside(col, row, sheetZone) ? "Success" /* CommandResult.Success */ : "TargetOutOfSheet" /* CommandResult.TargetOutOfSheet */;
        }
        checkUselessClearCell(cmd) {
            const cell = this.getters.getCell(cmd);
            if (!cell)
                return "NoChanges" /* CommandResult.NoChanges */;
            if (!cell.content && !cell.style && !cell.format) {
                return "NoChanges" /* CommandResult.NoChanges */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkUselessUpdateCell(cmd) {
            const cell = this.getters.getCell(cmd);
            const hasContent = "content" in cmd || "formula" in cmd;
            const hasStyle = "style" in cmd;
            const hasFormat = "format" in cmd;
            if ((!hasContent || cell?.content === cmd.content) &&
                (!hasStyle || deepEquals(cell?.style, cmd.style)) &&
                (!hasFormat || cell?.format === cmd.format)) {
                return "NoChanges" /* CommandResult.NoChanges */;
            }
            return "Success" /* CommandResult.Success */;
        }
    }
    class FormulaCellWithDependencies {
        id;
        format;
        style;
        sheetId;
        getRangeString;
        isFormula = true;
        compiledFormula;
        constructor(id, compiledFormula, format, style, dependencies, sheetId, getRangeString) {
            this.id = id;
            this.format = format;
            this.style = style;
            this.sheetId = sheetId;
            this.getRangeString = getRangeString;
            let rangeIndex = 0;
            const tokens = compiledFormula.tokens.map((token) => {
                if (token.type === "REFERENCE") {
                    const index = rangeIndex++;
                    return new RangeReferenceToken(dependencies, index, this.sheetId, this.getRangeString);
                }
                return token;
            });
            this.compiledFormula = {
                ...compiledFormula,
                dependencies,
                tokens,
            };
        }
        get content() {
            return concat(this.compiledFormula.tokens.map((token) => token.value));
        }
        get contentWithFixedReferences() {
            let rangeIndex = 0;
            return concat(this.compiledFormula.tokens.map((token) => {
                if (token.type === "REFERENCE") {
                    const index = rangeIndex++;
                    return this.getRangeString(this.compiledFormula.dependencies[index], this.sheetId, {
                        useFixedReference: true,
                    });
                }
                return token.value;
            }));
        }
    }
    class RangeReferenceToken {
        ranges;
        rangeIndex;
        sheetId;
        getRangeString;
        type = "REFERENCE";
        constructor(ranges, rangeIndex, sheetId, getRangeString) {
            this.ranges = ranges;
            this.rangeIndex = rangeIndex;
            this.sheetId = sheetId;
            this.getRangeString = getRangeString;
        }
        get value() {
            const range = this.ranges[this.rangeIndex];
            return this.getRangeString(range, this.sheetId);
        }
    }

    class ChartPlugin extends CorePlugin {
        static getters = [
            "isChartDefined",
            "getChartDefinition",
            "getChartType",
            "getChartIds",
            "getChart",
            "getContextCreationChart",
        ];
        charts = {};
        createChart = chartFactory(this.getters);
        validateChartDefinition = (cmd) => validateChartDefinition(this, cmd.definition);
        adaptRanges(applyChange) {
            for (const [chartId, chart] of Object.entries(this.charts)) {
                this.history.update("charts", chartId, chart?.updateRanges(applyChange));
            }
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "CREATE_CHART":
                    return this.checkValidations(cmd, this.chainValidations(this.validateChartDefinition, this.checkChartDuplicate));
                case "UPDATE_CHART":
                    return this.checkValidations(cmd, this.chainValidations(this.validateChartDefinition, this.checkChartExists));
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_CHART":
                    this.addFigure(cmd.id, cmd.sheetId, cmd.position, cmd.size);
                    this.addChart(cmd.id, cmd.definition);
                    break;
                case "UPDATE_CHART": {
                    this.addChart(cmd.id, cmd.definition);
                    break;
                }
                case "DUPLICATE_SHEET": {
                    const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId);
                    for (const fig of sheetFiguresFrom) {
                        if (fig.tag === "chart") {
                            const figureIdBase = fig.id.split(FIGURE_ID_SPLITTER).pop();
                            const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;
                            const chart = this.charts[fig.id]?.copyForSheetId(cmd.sheetIdTo);
                            if (chart) {
                                this.dispatch("CREATE_CHART", {
                                    id: duplicatedFigureId,
                                    position: { x: fig.x, y: fig.y },
                                    size: { width: fig.width, height: fig.height },
                                    definition: chart.getDefinition(),
                                    sheetId: cmd.sheetIdTo,
                                });
                            }
                        }
                    }
                    break;
                }
                case "DELETE_FIGURE":
                    this.history.update("charts", cmd.id, undefined);
                    break;
                case "DELETE_SHEET":
                    for (let id of this.getChartIds(cmd.sheetId)) {
                        this.history.update("charts", id, undefined);
                    }
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getContextCreationChart(figureId) {
            return this.charts[figureId]?.getContextCreation();
        }
        getChart(figureId) {
            return this.charts[figureId];
        }
        getChartType(figureId) {
            const type = this.charts[figureId]?.type;
            if (!type) {
                throw new Error("Chart not defined.");
            }
            return type;
        }
        isChartDefined(figureId) {
            return figureId in this.charts && this.charts !== undefined;
        }
        getChartIds(sheetId) {
            return Object.entries(this.charts)
                .filter(([, chart]) => chart?.sheetId === sheetId)
                .map(([id]) => id);
        }
        getChartDefinition(figureId) {
            const definition = this.charts[figureId]?.getDefinition();
            if (!definition) {
                throw new Error(`There is no chart with the given figureId: ${figureId}`);
            }
            return definition;
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            for (let sheet of data.sheets) {
                if (sheet.figures) {
                    for (let figure of sheet.figures) {
                        // TODO:
                        // figure data should be external IMO => chart should be in sheet.chart
                        // instead of in figure.data
                        if (figure.tag === "chart") {
                            this.charts[figure.id] = this.createChart(figure.id, figure.data, sheet.id);
                        }
                    }
                }
            }
        }
        export(data) {
            if (data.sheets) {
                for (let sheet of data.sheets) {
                    // TODO This code is false, if two plugins want to insert figures on the sheet, it will crash !
                    const sheetFigures = this.getters.getFigures(sheet.id);
                    const figures = [];
                    for (let sheetFigure of sheetFigures) {
                        const figure = sheetFigure;
                        if (figure && figure.tag === "chart") {
                            const data = this.charts[figure.id]?.getDefinition();
                            if (data) {
                                figure.data = data;
                                figures.push(figure);
                            }
                        }
                        else {
                            figures.push(figure);
                        }
                    }
                    sheet.figures = figures;
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        /**
         * Add a figure with tag chart with the given id at the given position
         */
        addFigure(id, sheetId, position = { x: 0, y: 0 }, size = {
            width: DEFAULT_FIGURE_WIDTH,
            height: DEFAULT_FIGURE_HEIGHT,
        }) {
            if (this.getters.getFigure(sheetId, id)) {
                return;
            }
            const figure = {
                id,
                x: position.x,
                y: position.y,
                width: size.width,
                height: size.height,
                tag: "chart",
            };
            this.dispatch("CREATE_FIGURE", { sheetId, figure });
        }
        /**
         * Add a chart in the local state. If a chart already exists, this chart is
         * replaced
         */
        addChart(id, definition) {
            const sheetId = this.getters.getFigureSheetId(id);
            if (sheetId) {
                this.history.update("charts", id, this.createChart(id, definition, sheetId));
            }
        }
        checkChartDuplicate(cmd) {
            return this.getters.getFigureSheetId(cmd.id)
                ? "DuplicatedChartId" /* CommandResult.DuplicatedChartId */
                : "Success" /* CommandResult.Success */;
        }
        checkChartExists(cmd) {
            return this.getters.getFigureSheetId(cmd.id)
                ? "Success" /* CommandResult.Success */
                : "ChartDoesNotExist" /* CommandResult.ChartDoesNotExist */;
        }
    }

    // -----------------------------------------------------------------------------
    // Constants
    // -----------------------------------------------------------------------------
    function stringToNumber(value) {
        return value === "" ? NaN : Number(value);
    }
    class ConditionalFormatPlugin extends CorePlugin {
        static getters = [
            "getConditionalFormats",
            "getRulesSelection",
            "getRulesByCell",
            "getAdaptedCfRanges",
        ];
        cfRules = {};
        loopThroughRangesOfSheet(sheetId, applyChange) {
            for (const rule of this.cfRules[sheetId]) {
                for (const range of rule.ranges) {
                    const change = applyChange(range);
                    switch (change.changeType) {
                        case "REMOVE":
                            let copy = rule.ranges.slice();
                            copy.splice(rule.ranges.indexOf(range), 1);
                            if (copy.length >= 1) {
                                this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "ranges", copy);
                            }
                            else {
                                this.removeConditionalFormatting(rule.id, sheetId);
                            }
                            break;
                        case "RESIZE":
                        case "MOVE":
                        case "CHANGE":
                            this.history.update("cfRules", sheetId, this.cfRules[sheetId].indexOf(rule), "ranges", rule.ranges.indexOf(range), change.range);
                            break;
                    }
                }
            }
        }
        adaptRanges(applyChange, sheetId) {
            if (sheetId) {
                this.loopThroughRangesOfSheet(sheetId, applyChange);
            }
            else {
                for (const sheetId of Object.keys(this.cfRules)) {
                    this.loopThroughRangesOfSheet(sheetId, applyChange);
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "ADD_CONDITIONAL_FORMAT":
                    return this.checkValidations(cmd, this.checkCFRule, this.checkEmptyRange);
                case "CHANGE_CONDITIONAL_FORMAT_PRIORITY":
                    return this.checkValidPriorityChange(cmd.cfId, cmd.delta, cmd.sheetId);
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET":
                    this.cfRules[cmd.sheetId] = [];
                    break;
                case "DUPLICATE_SHEET":
                    this.history.update("cfRules", cmd.sheetIdTo, []);
                    for (const cf of this.getConditionalFormats(cmd.sheetId)) {
                        this.addConditionalFormatting(cf, cmd.sheetIdTo);
                    }
                    break;
                case "DELETE_SHEET":
                    const cfRules = Object.assign({}, this.cfRules);
                    delete cfRules[cmd.sheetId];
                    this.history.update("cfRules", cfRules);
                    break;
                case "ADD_CONDITIONAL_FORMAT":
                    const cf = {
                        ...cmd.cf,
                        ranges: cmd.ranges.map((rangeData) => this.getters.getRangeString(this.getters.getRangeFromRangeData(rangeData), cmd.sheetId)),
                    };
                    this.addConditionalFormatting(cf, cmd.sheetId);
                    break;
                case "REMOVE_CONDITIONAL_FORMAT":
                    this.removeConditionalFormatting(cmd.id, cmd.sheetId);
                    break;
                case "CHANGE_CONDITIONAL_FORMAT_PRIORITY":
                    this.changeCFPriority(cmd.cfId, cmd.delta, cmd.sheetId);
                    break;
            }
        }
        import(data) {
            for (let sheet of data.sheets) {
                this.cfRules[sheet.id] = sheet.conditionalFormats.map((rule) => this.mapToConditionalFormatInternal(sheet.id, rule));
            }
        }
        export(data) {
            if (data.sheets) {
                for (let sheet of data.sheets) {
                    if (this.cfRules[sheet.id]) {
                        sheet.conditionalFormats = this.cfRules[sheet.id].map((rule) => this.mapToConditionalFormat(sheet.id, rule));
                    }
                }
            }
        }
        exportForExcel(data) {
            if (data.sheets) {
                for (let sheet of data.sheets) {
                    if (this.cfRules[sheet.id]) {
                        sheet.conditionalFormats = this.cfRules[sheet.id].map((rule) => this.mapToConditionalFormat(sheet.id, rule, { useFixedReference: true }));
                    }
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        /**
         * Returns all the conditional format rules defined for the current sheet to display the user
         */
        getConditionalFormats(sheetId) {
            return this.cfRules[sheetId]?.map((cf) => this.mapToConditionalFormat(sheetId, cf)) || [];
        }
        getRulesSelection(sheetId, selection) {
            const ruleIds = new Set();
            selection.forEach((zone) => {
                const zoneRuleId = this.getRulesByZone(sheetId, zone);
                zoneRuleId.forEach((ruleId) => {
                    ruleIds.add(ruleId);
                });
            });
            return Array.from(ruleIds);
        }
        getRulesByZone(sheetId, zone) {
            const ruleIds = new Set();
            for (let row = zone.top; row <= zone.bottom; row++) {
                for (let col = zone.left; col <= zone.right; col++) {
                    const cellRules = this.getRulesByCell(sheetId, col, row);
                    cellRules.forEach((rule) => {
                        ruleIds.add(rule.id);
                    });
                }
            }
            return ruleIds;
        }
        getRulesByCell(sheetId, cellCol, cellRow) {
            const rules = [];
            for (let cf of this.cfRules[sheetId]) {
                for (let range of cf.ranges) {
                    if (isInside(cellCol, cellRow, range.zone)) {
                        rules.push(cf);
                    }
                }
            }
            return new Set(rules.map((rule) => {
                return this.mapToConditionalFormat(sheetId, rule);
            }));
        }
        /**
         * Add or remove cells to a given conditional formatting rule and return the adapted CF's XCs.
         */
        getAdaptedCfRanges(sheetId, cf, toAdd, toRemove) {
            if (toAdd.length === 0 && toRemove.length === 0) {
                return;
            }
            const rules = this.getters.getConditionalFormats(sheetId);
            const replaceIndex = rules.findIndex((c) => c.id === cf.id);
            let currentRanges = [];
            if (replaceIndex > -1) {
                currentRanges = rules[replaceIndex].ranges;
            }
            currentRanges = currentRanges.concat(toAdd);
            return recomputeZones(currentRanges, toRemove);
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        mapToConditionalFormat(sheetId, cf, { useFixedReference } = { useFixedReference: false }) {
            return {
                ...cf,
                ranges: cf.ranges.map((range) => {
                    return this.getters.getRangeString(range, sheetId, { useFixedReference });
                }),
            };
        }
        mapToConditionalFormatInternal(sheet, cf) {
            const conditionalFormat = {
                ...cf,
                ranges: cf.ranges.map((range) => {
                    return this.getters.getRangeFromSheetXC(sheet, range);
                }),
            };
            return conditionalFormat;
        }
        /**
         * Add or replace a conditional format rule
         */
        addConditionalFormatting(cf, sheet) {
            const currentCF = this.cfRules[sheet].slice();
            const replaceIndex = currentCF.findIndex((c) => c.id === cf.id);
            const newCF = this.mapToConditionalFormatInternal(sheet, cf);
            if (replaceIndex > -1) {
                currentCF.splice(replaceIndex, 1, newCF);
            }
            else {
                currentCF.push(newCF);
            }
            this.history.update("cfRules", sheet, currentCF);
        }
        checkValidPriorityChange(cfId, delta, sheetId) {
            if (!this.cfRules[sheetId])
                return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
            const ruleIndex = this.cfRules[sheetId].findIndex((cf) => cf.id === cfId);
            if (ruleIndex === -1)
                return "InvalidConditionalFormatId" /* CommandResult.InvalidConditionalFormatId */;
            const cfIndex2 = ruleIndex - delta;
            if (cfIndex2 < 0 || cfIndex2 >= this.cfRules[sheetId].length) {
                return "InvalidConditionalFormatId" /* CommandResult.InvalidConditionalFormatId */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkEmptyRange(cmd) {
            return cmd.ranges.length ? "Success" /* CommandResult.Success */ : "EmptyRange" /* CommandResult.EmptyRange */;
        }
        checkCFRule(cmd) {
            const rule = cmd.cf.rule;
            switch (rule.type) {
                case "CellIsRule":
                    return this.checkValidations(rule, this.checkOperatorArgsNumber(2, ["Between", "NotBetween"]), this.checkOperatorArgsNumber(1, [
                        "BeginsWith",
                        "ContainsText",
                        "EndsWith",
                        "GreaterThan",
                        "GreaterThanOrEqual",
                        "LessThan",
                        "LessThanOrEqual",
                        "NotContains",
                    ]), this.checkOperatorArgsNumber(0, ["IsEmpty", "IsNotEmpty"]));
                case "ColorScaleRule": {
                    return this.checkValidations(rule, this.chainValidations(this.checkThresholds(this.checkFormulaCompilation)), this.chainValidations(this.checkThresholds(this.checkNaN), this.batchValidations(this.checkMinBiggerThanMax, this.checkMinBiggerThanMid, this.checkMidBiggerThanMax
                    // Those three validations can be factorized further
                    )));
                }
                case "IconSetRule": {
                    return this.checkValidations(rule, this.chainValidations(this.checkInflectionPoints(this.checkNaN), this.checkLowerBiggerThanUpper), this.chainValidations(this.checkInflectionPoints(this.checkFormulaCompilation)));
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        checkOperatorArgsNumber(expectedNumber, operators) {
            if (expectedNumber > 2) {
                throw new Error("Checking more than 2 arguments is currently not supported. Add the appropriate CommandResult if you want to.");
            }
            return (rule) => {
                if (operators.includes(rule.operator)) {
                    const errors = [];
                    const isEmpty = (value) => value === undefined || value === "";
                    if (expectedNumber >= 1 && isEmpty(rule.values[0])) {
                        errors.push("FirstArgMissing" /* CommandResult.FirstArgMissing */);
                    }
                    if (expectedNumber >= 2 && isEmpty(rule.values[1])) {
                        errors.push("SecondArgMissing" /* CommandResult.SecondArgMissing */);
                    }
                    return errors.length ? errors : "Success" /* CommandResult.Success */;
                }
                return "Success" /* CommandResult.Success */;
            };
        }
        checkNaN(threshold, thresholdName) {
            if (["number", "percentage", "percentile"].includes(threshold.type) &&
                (threshold.value === "" || isNaN(threshold.value))) {
                switch (thresholdName) {
                    case "min":
                        return "MinNaN" /* CommandResult.MinNaN */;
                    case "max":
                        return "MaxNaN" /* CommandResult.MaxNaN */;
                    case "mid":
                        return "MidNaN" /* CommandResult.MidNaN */;
                    case "upperInflectionPoint":
                        return "ValueUpperInflectionNaN" /* CommandResult.ValueUpperInflectionNaN */;
                    case "lowerInflectionPoint":
                        return "ValueLowerInflectionNaN" /* CommandResult.ValueLowerInflectionNaN */;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        checkFormulaCompilation(threshold, thresholdName) {
            if (threshold.type !== "formula")
                return "Success" /* CommandResult.Success */;
            try {
                compile(threshold.value || "");
            }
            catch (error) {
                switch (thresholdName) {
                    case "min":
                        return "MinInvalidFormula" /* CommandResult.MinInvalidFormula */;
                    case "max":
                        return "MaxInvalidFormula" /* CommandResult.MaxInvalidFormula */;
                    case "mid":
                        return "MidInvalidFormula" /* CommandResult.MidInvalidFormula */;
                    case "upperInflectionPoint":
                        return "ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */;
                    case "lowerInflectionPoint":
                        return "ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        checkThresholds(check) {
            return this.batchValidations((rule) => check(rule.minimum, "min"), (rule) => check(rule.maximum, "max"), (rule) => (rule.midpoint ? check(rule.midpoint, "mid") : "Success" /* CommandResult.Success */));
        }
        checkInflectionPoints(check) {
            return this.batchValidations((rule) => check(rule.lowerInflectionPoint, "lowerInflectionPoint"), (rule) => check(rule.upperInflectionPoint, "upperInflectionPoint"));
        }
        checkLowerBiggerThanUpper(rule) {
            const minValue = rule.lowerInflectionPoint.value;
            const maxValue = rule.upperInflectionPoint.value;
            if (["number", "percentage", "percentile"].includes(rule.lowerInflectionPoint.type) &&
                rule.lowerInflectionPoint.type === rule.upperInflectionPoint.type &&
                Number(minValue) > Number(maxValue)) {
                return "LowerBiggerThanUpper" /* CommandResult.LowerBiggerThanUpper */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkMinBiggerThanMax(rule) {
            const minValue = rule.minimum.value;
            const maxValue = rule.maximum.value;
            if (["number", "percentage", "percentile"].includes(rule.minimum.type) &&
                rule.minimum.type === rule.maximum.type &&
                stringToNumber(minValue) >= stringToNumber(maxValue)) {
                return "MinBiggerThanMax" /* CommandResult.MinBiggerThanMax */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkMidBiggerThanMax(rule) {
            const midValue = rule.midpoint?.value;
            const maxValue = rule.maximum.value;
            if (rule.midpoint &&
                ["number", "percentage", "percentile"].includes(rule.midpoint.type) &&
                rule.midpoint.type === rule.maximum.type &&
                stringToNumber(midValue) >= stringToNumber(maxValue)) {
                return "MidBiggerThanMax" /* CommandResult.MidBiggerThanMax */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkMinBiggerThanMid(rule) {
            const minValue = rule.minimum.value;
            const midValue = rule.midpoint?.value;
            if (rule.midpoint &&
                ["number", "percentage", "percentile"].includes(rule.midpoint.type) &&
                rule.minimum.type === rule.midpoint.type &&
                stringToNumber(minValue) >= stringToNumber(midValue)) {
                return "MinBiggerThanMid" /* CommandResult.MinBiggerThanMid */;
            }
            return "Success" /* CommandResult.Success */;
        }
        removeConditionalFormatting(id, sheet) {
            const cfIndex = this.cfRules[sheet].findIndex((s) => s.id === id);
            if (cfIndex !== -1) {
                const currentCF = this.cfRules[sheet].slice();
                currentCF.splice(cfIndex, 1);
                this.history.update("cfRules", sheet, currentCF);
            }
        }
        changeCFPriority(cfId, delta, sheetId) {
            const currentIndex = this.cfRules[sheetId].findIndex((s) => s.id === cfId);
            const cf = this.cfRules[sheetId][currentIndex];
            const targetIndex = currentIndex - delta; // priority goes up when index goes down
            const cfRules = [...this.cfRules[sheetId]];
            cfRules.splice(currentIndex, 1);
            cfRules.splice(targetIndex, 0, cf);
            this.history.update("cfRules", sheetId, cfRules);
        }
    }

    class DataValidationPlugin extends CorePlugin {
        static getters = [
            "cellHasListDataValidationIcon",
            "getDataValidationRule",
            "getDataValidationRules",
            "getValidationRuleForCell",
        ];
        rules = {};
        adaptRanges(applyChange, sheetId) {
            const sheetIds = sheetId ? [sheetId] : Object.keys(this.rules);
            for (const sheetId of sheetIds) {
                this.loopThroughRangesOfSheet(sheetId, applyChange);
            }
        }
        loopThroughRangesOfSheet(sheetId, applyChange) {
            const rules = this.rules[sheetId];
            for (let ruleIndex = rules.length - 1; ruleIndex >= 0; ruleIndex--) {
                const rule = this.rules[sheetId][ruleIndex];
                for (let rangeIndex = rule.ranges.length - 1; rangeIndex >= 0; rangeIndex--) {
                    const range = rule.ranges[rangeIndex];
                    const change = applyChange(range);
                    switch (change.changeType) {
                        case "REMOVE":
                            if (rule.ranges.length === 1) {
                                this.removeDataValidationRule(sheetId, rule.id);
                            }
                            else {
                                const copy = rule.ranges.slice();
                                copy.splice(rangeIndex, 1);
                                this.history.update("rules", sheetId, ruleIndex, "ranges", copy);
                            }
                            break;
                        case "RESIZE":
                        case "MOVE":
                        case "CHANGE":
                            this.history.update("rules", sheetId, ruleIndex, "ranges", rangeIndex, change.range);
                            break;
                    }
                }
            }
        }
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "ADD_DATA_VALIDATION_RULE":
                    return this.checkValidations(cmd, this.chainValidations(this.checkCriterionTypeIsValid, this.checkCriterionHasValidNumberOfValues, this.checkCriterionValuesAreValid));
                case "REMOVE_DATA_VALIDATION_RULE":
                    if (!this.rules[cmd.sheetId].find((rule) => rule.id === cmd.id)) {
                        return "UnknownDataValidationRule" /* CommandResult.UnknownDataValidationRule */;
                    }
                    break;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET":
                    this.history.update("rules", cmd.sheetId, []);
                    break;
                case "DUPLICATE_SHEET": {
                    const rules = deepCopy(this.rules[cmd.sheetId]).map((rule) => ({
                        ...rule,
                        ranges: rule.ranges.map((range) => copyRangeWithNewSheetId(cmd.sheetId, cmd.sheetIdTo, range)),
                    }));
                    this.history.update("rules", cmd.sheetIdTo, rules);
                    break;
                }
                case "DELETE_SHEET": {
                    const rules = { ...this.rules };
                    delete rules[cmd.sheetId];
                    this.history.update("rules", rules);
                    break;
                }
                case "REMOVE_DATA_VALIDATION_RULE": {
                    this.removeDataValidationRule(cmd.sheetId, cmd.id);
                    break;
                }
                case "ADD_DATA_VALIDATION_RULE": {
                    const ranges = cmd.ranges.map((range) => this.getters.getRangeFromRangeData(range));
                    this.addDataValidationRule(cmd.sheetId, { ...cmd.rule, ranges });
                    break;
                }
            }
        }
        getDataValidationRules(sheetId) {
            return this.rules[sheetId];
        }
        getDataValidationRule(sheetId, id) {
            return this.rules[sheetId].find((rule) => rule.id === id);
        }
        getValidationRuleForCell({ sheetId, col, row }) {
            if (!this.rules[sheetId]) {
                return undefined;
            }
            for (const rule of this.rules[sheetId]) {
                for (const range of rule.ranges) {
                    if (isInside(col, row, range.zone)) {
                        return rule;
                    }
                }
            }
            return undefined;
        }
        cellHasListDataValidationIcon(cellPosition) {
            const rule = this.getValidationRuleForCell(cellPosition);
            if (!rule)
                return false;
            return ((rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
                rule.criterion.displayStyle === "arrow");
        }
        addDataValidationRule(sheetId, newRule) {
            const rules = this.rules[sheetId];
            if (newRule.criterion.type === "isBoolean") {
                this.setCenterStyleToBooleanCells(newRule);
            }
            const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
            const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
            if (ruleIndex !== -1) {
                adaptedRules[ruleIndex] = newRule;
                this.history.update("rules", sheetId, adaptedRules);
            }
            else {
                this.history.update("rules", sheetId, [...adaptedRules, newRule]);
            }
        }
        removeRangesFromRules(sheetId, ranges, rules) {
            rules = deepCopy(rules);
            const rangesXcs = ranges.map((range) => this.getters.getRangeString(range, sheetId));
            for (const rule of rules) {
                const ruleRanges = rule.ranges.map((range) => this.getters.getRangeString(range, sheetId));
                rule.ranges = recomputeZones(ruleRanges, rangesXcs).map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
            }
            return rules.filter((rule) => rule.ranges.length > 0);
        }
        removeDataValidationRule(sheetId, ruleId) {
            const rules = this.rules[sheetId];
            const newRules = rules.filter((rule) => rule.id !== ruleId);
            this.history.update("rules", sheetId, newRules);
        }
        setCenterStyleToBooleanCells(rule) {
            for (const position of getCellPositionsInRanges(rule.ranges)) {
                const cell = this.getters.getCell(position);
                const { sheetId, col, row } = position;
                const style = {
                    ...cell?.style,
                    align: cell?.style?.align ?? "center",
                    verticalAlign: cell?.style?.verticalAlign ?? "middle",
                };
                this.dispatch("UPDATE_CELL", { sheetId, col, row, style });
            }
        }
        import(data) {
            for (const sheet of data.sheets) {
                this.rules[sheet.id] = [];
                if (!sheet.dataValidationRules) {
                    continue;
                }
                for (const rule of sheet.dataValidationRules) {
                    this.rules[sheet.id].push({
                        ...rule,
                        ranges: rule.ranges.map((range) => this.getters.getRangeFromSheetXC(sheet.id, range)),
                    });
                }
            }
        }
        export(data) {
            if (!data.sheets) {
                return;
            }
            for (const sheet of data.sheets) {
                sheet.dataValidationRules = [];
                for (const rule of this.rules[sheet.id]) {
                    sheet.dataValidationRules.push({
                        ...rule,
                        ranges: rule.ranges.map((range) => this.getters.getRangeString(range, sheet.id)),
                    });
                }
            }
        }
        checkCriterionTypeIsValid(cmd) {
            return dataValidationEvaluatorRegistry.contains(cmd.rule.criterion.type)
                ? "Success" /* CommandResult.Success */
                : "UnknownDataValidationCriterionType" /* CommandResult.UnknownDataValidationCriterionType */;
        }
        checkCriterionHasValidNumberOfValues(cmd) {
            const criterion = cmd.rule.criterion;
            const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);
            const expectedNumberOfValues = evaluator.numberOfValues(criterion);
            if (expectedNumberOfValues !== undefined &&
                criterion.values.length !== expectedNumberOfValues) {
                return "InvalidNumberOfCriterionValues" /* CommandResult.InvalidNumberOfCriterionValues */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkCriterionValuesAreValid(cmd) {
            const criterion = cmd.rule.criterion;
            const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);
            if (criterion.values.some((value) => {
                if (value.startsWith("=")) {
                    return evaluator.allowedValues === "onlyLiterals";
                }
                else if (evaluator.allowedValues === "onlyFormulas") {
                    return true;
                }
                else {
                    return !evaluator.isCriterionValueValid(value);
                }
            })) {
                return "InvalidDataValidationCriterionValue" /* CommandResult.InvalidDataValidationCriterionValue */;
            }
            return "Success" /* CommandResult.Success */;
        }
    }

    class FigurePlugin extends CorePlugin {
        static getters = ["getFigures", "getFigure", "getFigureSheetId"];
        figures = {};
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "CREATE_FIGURE":
                    return this.checkFigureDuplicate(cmd.figure.id);
                case "UPDATE_FIGURE":
                case "DELETE_FIGURE":
                    return this.checkFigureExists(cmd.sheetId, cmd.id);
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET":
                    this.figures[cmd.sheetId] = {};
                    break;
                case "DELETE_SHEET":
                    this.deleteSheet(cmd.sheetId);
                    break;
                case "CREATE_FIGURE":
                    this.addFigure(cmd.figure, cmd.sheetId);
                    break;
                case "UPDATE_FIGURE":
                    const { type, sheetId, ...update } = cmd;
                    const figure = update;
                    this.updateFigure(sheetId, figure);
                    break;
                case "DELETE_FIGURE":
                    this.removeFigure(cmd.id, cmd.sheetId);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                    this.onRowColDelete(cmd.sheetId, cmd.dimension);
            }
        }
        onRowColDelete(sheetId, dimension) {
            dimension === "ROW" ? this.onRowDeletion(sheetId) : this.onColDeletion(sheetId);
        }
        onRowDeletion(sheetId) {
            const numHeader = this.getters.getNumberRows(sheetId);
            let gridHeight = 0;
            for (let i = 0; i < numHeader; i++) {
                // TODO : since the row size is an UI value now, this doesn't work anymore. Using the default cell height is
                // a temporary solution at best, but is broken.
                gridHeight += this.getters.getUserRowSize(sheetId, i) || DEFAULT_CELL_HEIGHT;
            }
            const figures = this.getters.getFigures(sheetId);
            for (const figure of figures) {
                const newY = Math.min(figure.y, gridHeight - figure.height);
                if (newY !== figure.y) {
                    this.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, y: newY });
                }
            }
        }
        onColDeletion(sheetId) {
            const numHeader = this.getters.getNumberCols(sheetId);
            let gridWidth = 0;
            for (let i = 0; i < numHeader; i++) {
                gridWidth += this.getters.getColSize(sheetId, i);
            }
            const figures = this.getters.getFigures(sheetId);
            for (const figure of figures) {
                const newX = Math.min(figure.x, gridWidth - figure.width);
                if (newX !== figure.x) {
                    this.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x: newX });
                }
            }
        }
        updateFigure(sheetId, figure) {
            if (!("id" in figure)) {
                return;
            }
            for (const [key, value] of Object.entries(figure)) {
                switch (key) {
                    case "x":
                    case "y":
                        if (value !== undefined) {
                            this.history.update("figures", sheetId, figure.id, key, Math.max(value, 0));
                        }
                        break;
                    case "width":
                    case "height":
                        if (value !== undefined) {
                            this.history.update("figures", sheetId, figure.id, key, value);
                        }
                        break;
                }
            }
        }
        addFigure(figure, sheetId) {
            this.history.update("figures", sheetId, figure.id, figure);
        }
        deleteSheet(sheetId) {
            this.history.update("figures", sheetId, undefined);
        }
        removeFigure(id, sheetId) {
            this.history.update("figures", sheetId, id, undefined);
        }
        checkFigureExists(sheetId, figureId) {
            if (this.figures[sheetId]?.[figureId] === undefined) {
                return "FigureDoesNotExist" /* CommandResult.FigureDoesNotExist */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkFigureDuplicate(figureId) {
            if (Object.values(this.figures).find((sheet) => sheet?.[figureId])) {
                return "DuplicatedFigureId" /* CommandResult.DuplicatedFigureId */;
            }
            return "Success" /* CommandResult.Success */;
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getFigures(sheetId) {
            return Object.values(this.figures[sheetId] || {}).filter(isDefined$1);
        }
        getFigure(sheetId, figureId) {
            return this.figures[sheetId]?.[figureId];
        }
        getFigureSheetId(figureId) {
            return Object.keys(this.figures).find((sheetId) => this.figures[sheetId]?.[figureId] !== undefined);
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            for (let sheet of data.sheets) {
                const figures = {};
                sheet.figures.forEach((figure) => {
                    figures[figure.id] = figure;
                });
                this.figures[sheet.id] = figures;
            }
        }
        export(data) {
            for (const sheet of data.sheets) {
                for (const figure of this.getFigures(sheet.id)) {
                    const data = undefined;
                    sheet.figures.push({ ...figure, data });
                }
            }
        }
        exportForExcel(data) {
            this.export(data);
        }
    }

    class FilterTable {
        id;
        zone;
        filters;
        constructor(zone) {
            this.filters = [];
            this.zone = zone;
            const uuid = new UuidGenerator();
            this.id = uuid.uuidv4();
            for (const i of range(zone.left, zone.right + 1)) {
                const filterZone = { ...this.zone, left: i, right: i };
                this.filters.push(new Filter(uuid.uuidv4(), filterZone));
            }
        }
        /** Get zone of the table without the headers */
        get contentZone() {
            if (this.zone.bottom === this.zone.top) {
                return undefined;
            }
            return { ...this.zone, top: this.zone.top + 1 };
        }
        getFilterId(col) {
            return this.filters.find((filter) => filter.col === col)?.id;
        }
        clone() {
            return new FilterTable(this.zone);
        }
    }
    class Filter {
        id;
        zoneWithHeaders;
        constructor(id, zone) {
            if (zone.left !== zone.right) {
                throw new Error("Can only define a filter on a single column");
            }
            this.id = id;
            this.zoneWithHeaders = zone;
        }
        get col() {
            return this.zoneWithHeaders.left;
        }
        /** Filtered zone, ie. zone of the filter without the header */
        get filteredZone() {
            const zone = this.zoneWithHeaders;
            if (zone.bottom === zone.top) {
                return undefined;
            }
            return { ...zone, top: zone.top + 1 };
        }
    }

    class FiltersPlugin extends CorePlugin {
        static getters = [
            "doesZonesContainFilter",
            "getFilter",
            "getFilters",
            "getFilterTable",
            "getFilterTables",
            "getFilterTablesInZone",
            "getFilterId",
            "getFilterHeaders",
            "isFilterHeader",
        ];
        tables = {};
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "CREATE_FILTER_TABLE":
                    if (!areZonesContinuous(...cmd.target)) {
                        return "NonContinuousTargets" /* CommandResult.NonContinuousTargets */;
                    }
                    const zone = union(...cmd.target);
                    const checkFilterOverlap = () => {
                        if (this.getFilterTables(cmd.sheetId).some((filter) => overlap(filter.zone, zone))) {
                            return "FilterOverlap" /* CommandResult.FilterOverlap */;
                        }
                        return "Success" /* CommandResult.Success */;
                    };
                    const checkMergeInFilter = () => {
                        const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, zone);
                        for (let merge of mergesInTarget) {
                            if (overlap(zone, merge)) {
                                return "MergeInFilter" /* CommandResult.MergeInFilter */;
                            }
                        }
                        return "Success" /* CommandResult.Success */;
                    };
                    return this.checkValidations(cmd, checkFilterOverlap, checkMergeInFilter);
                case "ADD_MERGE":
                    for (let merge of cmd.target) {
                        for (let filterTable of this.getFilterTables(cmd.sheetId)) {
                            if (overlap(filterTable.zone, merge)) {
                                return "MergeInFilter" /* CommandResult.MergeInFilter */;
                            }
                        }
                    }
                    break;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET":
                    this.history.update("tables", cmd.sheetId, {});
                    break;
                case "DELETE_SHEET":
                    const filterTables = { ...this.tables };
                    delete filterTables[cmd.sheetId];
                    this.history.update("tables", filterTables);
                    break;
                case "DUPLICATE_SHEET":
                    const tables = {};
                    for (const filterTable of Object.values(this.tables[cmd.sheetId] || {})) {
                        if (filterTable) {
                            const newFilterTable = deepCopy(filterTable);
                            tables[newFilterTable.id] = newFilterTable;
                        }
                    }
                    this.history.update("tables", cmd.sheetIdTo, tables);
                    break;
                case "ADD_COLUMNS_ROWS":
                    this.onAddColumnsRows(cmd);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                    this.onDeleteColumnsRows(cmd);
                    break;
                case "CREATE_FILTER_TABLE": {
                    const zone = union(...cmd.target);
                    const newFilterTable = this.createFilterTable(zone);
                    this.history.update("tables", cmd.sheetId, newFilterTable.id, newFilterTable);
                    break;
                }
                case "REMOVE_FILTER_TABLE": {
                    const tables = {};
                    for (const filterTable of this.getFilterTables(cmd.sheetId)) {
                        if (cmd.target.every((zone) => !intersection(zone, filterTable.zone))) {
                            tables[filterTable.id] = filterTable;
                        }
                    }
                    this.history.update("tables", cmd.sheetId, tables);
                    break;
                }
                case "UPDATE_CELL": {
                    const sheetId = cmd.sheetId;
                    for (let table of this.getFilterTables(sheetId)) {
                        if (this.canUpdateCellCmdExtendTable(cmd, table)) {
                            this.extendTableDown(sheetId, table);
                        }
                    }
                    break;
                }
            }
        }
        getFilters(sheetId) {
            return this.getFilterTables(sheetId)
                .map((filterTable) => filterTable.filters)
                .flat();
        }
        getFilterTables(sheetId) {
            return this.tables[sheetId] ? Object.values(this.tables[sheetId]).filter(isDefined$1) : [];
        }
        getFilter(position) {
            return this.getFilterTable(position)?.filters.find((filter) => filter.col === position.col);
        }
        getFilterId(position) {
            return this.getFilter(position)?.id;
        }
        getFilterTable({ sheetId, col, row }) {
            return this.getFilterTables(sheetId).find((filterTable) => isInside(col, row, filterTable.zone));
        }
        /** Get the filter tables that are fully inside the given zone */
        getFilterTablesInZone(sheetId, zone) {
            return this.getFilterTables(sheetId).filter((filterTable) => isZoneInside(filterTable.zone, zone));
        }
        doesZonesContainFilter(sheetId, zones) {
            for (const zone of zones) {
                for (const filterTable of this.getFilterTables(sheetId)) {
                    if (intersection(zone, filterTable.zone)) {
                        return true;
                    }
                }
            }
            return false;
        }
        getFilterHeaders(sheetId) {
            const headers = [];
            for (let filterTable of this.getFilterTables(sheetId)) {
                const zone = filterTable.zone;
                const row = zone.top;
                for (let col = zone.left; col <= zone.right; col++) {
                    headers.push({ col, row });
                }
            }
            return headers;
        }
        isFilterHeader({ sheetId, col, row }) {
            const headers = this.getFilterHeaders(sheetId);
            return headers.some((header) => header.col === col && header.row === row);
        }
        onAddColumnsRows(cmd) {
            for (const filterTable of this.getFilterTables(cmd.sheetId)) {
                const zone = expandZoneOnInsertion(filterTable.zone, cmd.dimension === "COL" ? "left" : "top", cmd.base, cmd.position, cmd.quantity);
                const filters = [];
                for (const filter of filterTable.filters) {
                    const filterZone = expandZoneOnInsertion(filter.zoneWithHeaders, cmd.dimension === "COL" ? "left" : "top", cmd.base, cmd.position, cmd.quantity);
                    filters.push(new Filter(filter.id, filterZone));
                }
                // Add filters for new columns
                if (filters.length < zoneToDimension(zone).numberOfCols) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        if (!filters.find((filter) => filter.col === col)) {
                            filters.push(new Filter(this.uuidGenerator.uuidv4(), { ...zone, left: col, right: col }));
                        }
                    }
                    filters.sort((f1, f2) => f1.col - f2.col);
                }
                this.history.update("tables", cmd.sheetId, filterTable.id, "zone", zone);
                this.history.update("tables", cmd.sheetId, filterTable.id, "filters", filters);
            }
        }
        onDeleteColumnsRows(cmd) {
            for (const table of this.getFilterTables(cmd.sheetId)) {
                // Remove the filter tables whose data filter headers are in the removed rows.
                if (cmd.dimension === "ROW" && cmd.elements.includes(table.zone.top)) {
                    const tables = { ...this.tables[cmd.sheetId] };
                    delete tables[table.id];
                    this.history.update("tables", cmd.sheetId, tables);
                    continue;
                }
                const zone = reduceZoneOnDeletion(table.zone, cmd.dimension === "COL" ? "left" : "top", cmd.elements);
                if (!zone) {
                    const tables = { ...this.tables[cmd.sheetId] };
                    delete tables[table.id];
                    this.history.update("tables", cmd.sheetId, tables);
                }
                else {
                    if (zoneToXc(zone) !== zoneToXc(table.zone)) {
                        const filters = [];
                        for (const filter of table.filters) {
                            const newFilterZone = reduceZoneOnDeletion(filter.zoneWithHeaders, cmd.dimension === "COL" ? "left" : "top", cmd.elements);
                            if (newFilterZone) {
                                filters.push(new Filter(filter.id, newFilterZone));
                            }
                        }
                        this.history.update("tables", cmd.sheetId, table.id, "zone", zone);
                        this.history.update("tables", cmd.sheetId, table.id, "filters", filters);
                    }
                }
            }
        }
        createFilterTable(zone) {
            return new FilterTable(zone);
        }
        /** Extend a table down one row */
        extendTableDown(sheetId, table) {
            const newZone = { ...table.zone, bottom: table.zone.bottom + 1 };
            this.history.update("tables", sheetId, table.id, "zone", newZone);
            for (let filterIndex = 0; filterIndex < table.filters.length; filterIndex++) {
                const filter = table.filters[filterIndex];
                const newFilterZone = {
                    ...filter.zoneWithHeaders,
                    bottom: filter.zoneWithHeaders.bottom + 1,
                };
                this.history.update("tables", sheetId, table.id, "filters", filterIndex, "zoneWithHeaders", newFilterZone);
            }
            return;
        }
        /**
         * Check if an UpdateCell command should cause the given table to be extended by one row.
         *
         * The table should be extended if all of these conditions are true:
         * 1) The updated cell is right below the table
         * 2) The command adds a content to the cell
         * 3) No cell right below the table had any content before the command
         * 4) Extending the table down would not overlap with another filter
         * 5) Extending the table down would not overlap with a merge
         *
         */
        canUpdateCellCmdExtendTable({ content: newCellContent, sheetId, col, row }, table) {
            if (!newCellContent) {
                return;
            }
            const zone = table.zone;
            if (!(zone.bottom + 1 === row && col >= zone.left && col <= zone.right)) {
                return false;
            }
            for (const col of range(zone.left, zone.right + 1)) {
                const position = { sheetId, col, row };
                // Since this plugin is loaded before CellPlugin, the getters still give us the old cell content
                const cellContent = this.getters.getCell(position)?.content;
                if (cellContent) {
                    return false;
                }
                if (this.getters.getFilter(position)) {
                    return false;
                }
                if (this.getters.isInMerge(position)) {
                    return false;
                }
            }
            return true;
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            for (const sheet of data.sheets) {
                for (const filterTableData of sheet.filterTables || []) {
                    const table = this.createFilterTable(toZone(filterTableData.range));
                    this.history.update("tables", sheet.id, table.id, table);
                }
            }
        }
        export(data) {
            for (const sheet of data.sheets) {
                for (const filterTable of this.getFilterTables(sheet.id)) {
                    sheet.filterTables.push({
                        range: zoneToXc(filterTable.zone),
                    });
                }
            }
        }
        exportForExcel(data) {
            this.export(data);
        }
    }

    class HeaderSizePlugin extends CorePlugin {
        static getters = ["getUserRowSize", "getColSize"];
        sizes = {};
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET": {
                    this.history.update("sizes", cmd.sheetId, { COL: [], ROW: [] });
                    break;
                }
                case "DUPLICATE_SHEET":
                    this.history.update("sizes", cmd.sheetIdTo, deepCopy(this.sizes[cmd.sheetId]));
                    break;
                case "DELETE_SHEET":
                    const sizes = { ...this.sizes };
                    delete sizes[cmd.sheetId];
                    this.history.update("sizes", sizes);
                    break;
                case "REMOVE_COLUMNS_ROWS": {
                    const arr = this.sizes[cmd.sheetId][cmd.dimension];
                    const sizes = removeIndexesFromArray(arr, cmd.elements);
                    this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes);
                    break;
                }
                case "ADD_COLUMNS_ROWS": {
                    let sizes = [...this.sizes[cmd.sheetId][cmd.dimension]];
                    const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
                    const baseSize = sizes[cmd.base];
                    sizes.splice(addIndex, 0, ...Array(cmd.quantity).fill(baseSize));
                    this.history.update("sizes", cmd.sheetId, cmd.dimension, sizes);
                    break;
                }
                case "RESIZE_COLUMNS_ROWS":
                    if (cmd.dimension === "ROW") {
                        for (const el of cmd.elements) {
                            this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);
                        }
                    }
                    else {
                        for (const el of cmd.elements) {
                            this.history.update("sizes", cmd.sheetId, cmd.dimension, el, cmd.size || undefined);
                        }
                    }
                    break;
            }
            return;
        }
        getColSize(sheetId, index) {
            return Math.round(this.sizes[sheetId]?.["COL"][index] || DEFAULT_CELL_WIDTH);
        }
        getUserRowSize(sheetId, index) {
            const rowSize = this.sizes[sheetId]?.["ROW"][index];
            return rowSize ? Math.round(rowSize) : undefined;
        }
        import(data) {
            for (let sheet of data.sheets) {
                const sizes = {
                    COL: Array(sheet.colNumber).fill(undefined),
                    ROW: Array(sheet.rowNumber).fill(undefined),
                };
                for (let [rowIndex, row] of Object.entries(sheet.rows)) {
                    if (row.size) {
                        sizes["ROW"][rowIndex] = row.size;
                    }
                }
                for (let [colIndex, col] of Object.entries(sheet.cols)) {
                    if (col.size) {
                        sizes["COL"][colIndex] = col.size;
                    }
                }
                this.sizes[sheet.id] = sizes;
            }
            return;
        }
        exportForExcel(data) {
            this.exportData(data, true);
        }
        export(data) {
            this.exportData(data);
        }
        /**
         * Export the header sizes
         *
         * @param exportDefaults : if true, export column/row sizes even if they have the default size
         */
        exportData(data, exportDefaults = false) {
            for (let sheet of data.sheets) {
                // Export row sizes
                if (sheet.rows === undefined) {
                    sheet.rows = {};
                }
                for (const row of range(0, this.getters.getNumberRows(sheet.id))) {
                    if (exportDefaults || this.sizes[sheet.id]["ROW"][row]) {
                        sheet.rows[row] = {
                            ...sheet.rows[row],
                            size: this.getUserRowSize(sheet.id, row) ?? DEFAULT_CELL_HEIGHT,
                        };
                    }
                }
                // Export col sizes
                if (sheet.cols === undefined) {
                    sheet.cols = {};
                }
                for (let col of range(0, this.getters.getNumberCols(sheet.id))) {
                    if (exportDefaults || this.sizes[sheet.id]["COL"][col]) {
                        sheet.cols[col] = { ...sheet.cols[col], size: this.getColSize(sheet.id, col) };
                    }
                }
            }
        }
    }

    class HeaderVisibilityPlugin extends CorePlugin {
        static getters = [
            "checkElementsIncludeAllVisibleHeaders",
            "getHiddenColsGroups",
            "getHiddenRowsGroups",
            "isHeaderHiddenByUser",
            "isRowHiddenByUser",
            "isColHiddenByUser",
        ];
        hiddenHeaders = {};
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "HIDE_COLUMNS_ROWS": {
                    if (!this.getters.tryGetSheet(cmd.sheetId)) {
                        return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                    }
                    const hiddenGroup = cmd.dimension === "COL"
                        ? this.getHiddenColsGroups(cmd.sheetId)
                        : this.getHiddenRowsGroups(cmd.sheetId);
                    const elements = cmd.dimension === "COL"
                        ? this.getters.getNumberCols(cmd.sheetId)
                        : this.getters.getNumberRows(cmd.sheetId);
                    const hiddenElements = new Set((hiddenGroup || []).flat().concat(cmd.elements));
                    if (hiddenElements.size >= elements) {
                        return "TooManyHiddenElements" /* CommandResult.TooManyHiddenElements */;
                    }
                    else if (Math.min(...cmd.elements) < 0 || Math.max(...cmd.elements) > elements) {
                        return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
                    }
                    else {
                        return "Success" /* CommandResult.Success */;
                    }
                }
                case "REMOVE_COLUMNS_ROWS":
                    if (!this.getters.tryGetSheet(cmd.sheetId)) {
                        return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                    }
                    if (this.checkElementsIncludeAllVisibleHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {
                        return "NotEnoughElements" /* CommandResult.NotEnoughElements */;
                    }
                    return "Success" /* CommandResult.Success */;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET":
                    const hiddenHeaders = {
                        COL: Array(this.getters.getNumberCols(cmd.sheetId)).fill(false),
                        ROW: Array(this.getters.getNumberRows(cmd.sheetId)).fill(false),
                    };
                    this.history.update("hiddenHeaders", cmd.sheetId, hiddenHeaders);
                    break;
                case "DUPLICATE_SHEET":
                    this.history.update("hiddenHeaders", cmd.sheetIdTo, deepCopy(this.hiddenHeaders[cmd.sheetId]));
                    break;
                case "DELETE_SHEET":
                    this.history.update("hiddenHeaders", cmd.sheetId, undefined);
                    break;
                case "REMOVE_COLUMNS_ROWS": {
                    const hiddenHeaders = [...this.hiddenHeaders[cmd.sheetId][cmd.dimension]];
                    for (let el of [...cmd.elements].sort((a, b) => b - a)) {
                        hiddenHeaders.splice(el, 1);
                    }
                    this.history.update("hiddenHeaders", cmd.sheetId, cmd.dimension, hiddenHeaders);
                    break;
                }
                case "ADD_COLUMNS_ROWS": {
                    const hiddenHeaders = [...this.hiddenHeaders[cmd.sheetId][cmd.dimension]];
                    const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
                    hiddenHeaders.splice(addIndex, 0, ...Array(cmd.quantity).fill(false));
                    this.history.update("hiddenHeaders", cmd.sheetId, cmd.dimension, hiddenHeaders);
                    break;
                }
                case "HIDE_COLUMNS_ROWS":
                    for (let el of cmd.elements) {
                        this.history.update("hiddenHeaders", cmd.sheetId, cmd.dimension, el, true);
                    }
                    break;
                case "UNHIDE_COLUMNS_ROWS":
                    for (let el of cmd.elements) {
                        this.history.update("hiddenHeaders", cmd.sheetId, cmd.dimension, el, false);
                    }
                    break;
            }
            return;
        }
        checkElementsIncludeAllVisibleHeaders(sheetId, dimension, elements) {
            const visibleHeaders = this.getAllVisibleHeaders(sheetId, dimension);
            return includesAll(elements, visibleHeaders);
        }
        isHeaderHiddenByUser(sheetId, dimension, index) {
            return dimension === "COL"
                ? this.isColHiddenByUser(sheetId, index)
                : this.isRowHiddenByUser(sheetId, index);
        }
        isRowHiddenByUser(sheetId, index) {
            return this.hiddenHeaders[sheetId].ROW[index] || this.getters.isRowFolded(sheetId, index);
        }
        isColHiddenByUser(sheetId, index) {
            return this.hiddenHeaders[sheetId].COL[index] || this.getters.isColFolded(sheetId, index);
        }
        getHiddenColsGroups(sheetId) {
            const consecutiveIndexes = [[]];
            const hiddenCols = this.hiddenHeaders[sheetId].COL;
            for (let col = 0; col < hiddenCols.length; col++) {
                const isColHidden = hiddenCols[col];
                if (isColHidden) {
                    consecutiveIndexes[consecutiveIndexes.length - 1].push(col);
                }
                else {
                    if (consecutiveIndexes[consecutiveIndexes.length - 1].length !== 0) {
                        consecutiveIndexes.push([]);
                    }
                }
            }
            if (consecutiveIndexes[consecutiveIndexes.length - 1].length === 0) {
                consecutiveIndexes.pop();
            }
            return consecutiveIndexes;
        }
        getHiddenRowsGroups(sheetId) {
            const consecutiveIndexes = [[]];
            const hiddenCols = this.hiddenHeaders[sheetId].ROW;
            for (let row = 0; row < hiddenCols.length; row++) {
                const isRowHidden = hiddenCols[row];
                if (isRowHidden) {
                    consecutiveIndexes[consecutiveIndexes.length - 1].push(row);
                }
                else {
                    if (consecutiveIndexes[consecutiveIndexes.length - 1].length !== 0) {
                        consecutiveIndexes.push([]);
                    }
                }
            }
            if (consecutiveIndexes[consecutiveIndexes.length - 1].length === 0) {
                consecutiveIndexes.pop();
            }
            return consecutiveIndexes;
        }
        getAllVisibleHeaders(sheetId, dimension) {
            const headers = range(0, this.getters.getNumberHeaders(sheetId, dimension));
            const foldedHeaders = [];
            this.getters.getHeaderGroups(sheetId, dimension).forEach((group) => {
                if (group.isFolded) {
                    foldedHeaders.push(...range(group.start, group.end + 1));
                }
            });
            return headers.filter((i) => {
                return !this.hiddenHeaders[sheetId][dimension][i] && !foldedHeaders.includes(i);
            });
        }
        import(data) {
            for (let sheet of data.sheets) {
                this.hiddenHeaders[sheet.id] = { COL: [], ROW: [] };
                for (let row = 0; row < sheet.rowNumber; row++) {
                    this.hiddenHeaders[sheet.id].ROW[row] = Boolean(sheet.rows[row]?.isHidden);
                }
                for (let col = 0; col < sheet.colNumber; col++) {
                    this.hiddenHeaders[sheet.id].COL[col] = Boolean(sheet.cols[col]?.isHidden);
                }
            }
            return;
        }
        exportForExcel(data) {
            this.exportData(data, true);
        }
        export(data) {
            this.exportData(data);
        }
        exportData(data, exportDefaults = false) {
            for (let sheet of data.sheets) {
                if (sheet.rows === undefined) {
                    sheet.rows = {};
                }
                for (let row = 0; row < this.getters.getNumberRows(sheet.id); row++) {
                    if (exportDefaults || this.hiddenHeaders[sheet.id]["ROW"][row]) {
                        if (sheet.rows[row] === undefined) {
                            sheet.rows[row] = {};
                        }
                        sheet.rows[row].isHidden = this.hiddenHeaders[sheet.id]["ROW"][row];
                    }
                }
                if (sheet.cols === undefined) {
                    sheet.cols = {};
                }
                for (let col = 0; col < this.getters.getNumberCols(sheet.id); col++) {
                    if (exportDefaults || this.hiddenHeaders[sheet.id]["COL"][col]) {
                        if (sheet.cols[col] === undefined) {
                            sheet.cols[col] = {};
                        }
                        sheet.cols[col].isHidden = this.hiddenHeaders[sheet.id]["COL"][col];
                    }
                }
            }
        }
    }

    class ImagePlugin extends CorePlugin {
        static getters = ["getImage", "getImagePath", "getImageSize"];
        fileStore;
        images = {};
        /**
         * paths of images synced with the file store server.
         */
        syncedImages = new Set();
        constructor(config) {
            super(config);
            this.fileStore = config.external.fileStore;
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "CREATE_IMAGE":
                    if (this.getters.getFigure(cmd.sheetId, cmd.figureId)) {
                        return "InvalidFigureId" /* CommandResult.InvalidFigureId */;
                    }
                    return "Success" /* CommandResult.Success */;
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_IMAGE":
                    this.addImage(cmd.figureId, cmd.sheetId, cmd.position, cmd.size);
                    this.history.update("images", cmd.sheetId, cmd.figureId, cmd.definition);
                    this.syncedImages.add(cmd.definition.path);
                    break;
                case "DUPLICATE_SHEET": {
                    const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId);
                    for (const fig of sheetFiguresFrom) {
                        if (fig.tag === "image") {
                            const figureIdBase = fig.id.split(FIGURE_ID_SPLITTER).pop();
                            const duplicatedFigureId = `${cmd.sheetIdTo}${FIGURE_ID_SPLITTER}${figureIdBase}`;
                            const image = this.getImage(fig.id);
                            if (image) {
                                const size = { width: fig.width, height: fig.height };
                                this.dispatch("CREATE_IMAGE", {
                                    sheetId: cmd.sheetIdTo,
                                    figureId: duplicatedFigureId,
                                    position: { x: fig.x, y: fig.y },
                                    size,
                                    definition: deepCopy(image),
                                });
                            }
                        }
                    }
                    break;
                }
                case "DELETE_FIGURE":
                    this.history.update("images", cmd.sheetId, cmd.id, undefined);
                    break;
                case "DELETE_SHEET":
                    this.history.update("images", cmd.sheetId, undefined);
                    break;
            }
        }
        /**
         * Delete unused images from the file store
         */
        garbageCollectExternalResources() {
            const images = new Set(this.getAllImages().map((image) => image.path));
            for (const path of this.syncedImages) {
                if (!images.has(path)) {
                    this.fileStore?.delete(path);
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getImage(figureId) {
            for (const sheet of Object.values(this.images)) {
                if (sheet && sheet[figureId]) {
                    return sheet[figureId];
                }
            }
            throw new Error(`There is no image with the given figureId: ${figureId}`);
        }
        getImagePath(figureId) {
            return this.getImage(figureId).path;
        }
        getImageSize(figureId) {
            return this.getImage(figureId).size;
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        addImage(id, sheetId, position, size) {
            const figure = {
                id,
                x: position.x,
                y: position.y,
                width: size.width,
                height: size.height,
                tag: "image",
            };
            this.dispatch("CREATE_FIGURE", { sheetId, figure });
        }
        import(data) {
            for (const sheet of data.sheets) {
                const images = (sheet.figures || []).filter((figure) => figure.tag === "image");
                for (const image of images) {
                    this.history.update("images", sheet.id, image.id, image.data);
                    this.syncedImages.add(image.data.path);
                }
            }
        }
        export(data) {
            for (const sheet of data.sheets) {
                const images = sheet.figures.filter((figure) => figure.tag === "image");
                for (const image of images) {
                    image.data = this.images[sheet.id]?.[image.id];
                }
            }
        }
        exportForExcel(data) {
            for (const sheet of data.sheets) {
                if (!sheet.images) {
                    sheet.images = [];
                }
                const figures = this.getters.getFigures(sheet.id);
                const images = [];
                for (const figure of figures) {
                    if (figure?.tag === "image") {
                        const image = this.getImage(figure.id);
                        if (image) {
                            images.push({
                                ...figure,
                                data: deepCopy(image),
                            });
                        }
                    }
                }
                sheet.images = [...sheet.images, ...images];
            }
        }
        getAllImages() {
            const images = [];
            for (const sheetId in this.images) {
                images.push(...Object.values(this.images[sheetId] || {}).filter(isDefined$1));
            }
            return images;
        }
    }

    class MergePlugin extends CorePlugin {
        static getters = [
            "isInMerge",
            "isInSameMerge",
            "isMergeHidden",
            "getMainCellPosition",
            "getBottomLeftCell",
            "expandZone",
            "doesIntersectMerge",
            "doesColumnsHaveCommonMerges",
            "doesRowsHaveCommonMerges",
            "getMerges",
            "getMerge",
            "getMergesInZone",
            "isSingleCellOrMerge",
            "getSelectionRangeString",
            "isMainCellPosition",
        ];
        nextId = 1;
        merges = {};
        mergeCellMap = {};
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            const force = "force" in cmd ? !!cmd.force : false;
            switch (cmd.type) {
                case "ADD_MERGE":
                    if (force) {
                        return this.checkValidations(cmd, this.checkFrozenPanes);
                    }
                    return this.checkValidations(cmd, this.checkDestructiveMerge, this.checkOverlap, this.checkFrozenPanes);
                case "UPDATE_CELL":
                    return this.checkMergedContentUpdate(cmd);
                case "REMOVE_MERGE":
                    return this.checkMergeExists(cmd);
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET":
                    this.history.update("merges", cmd.sheetId, {});
                    this.history.update("mergeCellMap", cmd.sheetId, {});
                    break;
                case "DELETE_SHEET":
                    this.history.update("merges", cmd.sheetId, {});
                    this.history.update("mergeCellMap", cmd.sheetId, {});
                    break;
                case "DUPLICATE_SHEET":
                    const merges = this.merges[cmd.sheetId];
                    if (!merges)
                        break;
                    for (const range of Object.values(merges).filter(isDefined$1)) {
                        this.addMerge(cmd.sheetIdTo, range.zone);
                    }
                    break;
                case "ADD_MERGE":
                    for (const zone of cmd.target) {
                        this.addMerge(cmd.sheetId, zone);
                    }
                    break;
                case "REMOVE_MERGE":
                    for (const zone of cmd.target) {
                        this.removeMerge(cmd.sheetId, zone);
                    }
                    break;
            }
        }
        adaptRanges(applyChange, sheetId) {
            const sheetIds = sheetId ? [sheetId] : Object.keys(this.merges);
            for (const sheetId of sheetIds) {
                this.applyRangeChangeOnSheet(sheetId, applyChange);
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getMerges(sheetId) {
            return Object.keys(this.merges[sheetId] || {})
                .map((mergeId) => this.getMergeById(sheetId, parseInt(mergeId, 10)))
                .filter(isDefined$1);
        }
        getMerge({ sheetId, col, row }) {
            const sheetMap = this.mergeCellMap[sheetId];
            const mergeId = sheetMap ? col in sheetMap && sheetMap[col]?.[row] : undefined;
            return mergeId ? this.getMergeById(sheetId, mergeId) : undefined;
        }
        getMergesInZone(sheetId, zone) {
            const sheetMap = this.mergeCellMap[sheetId];
            if (!sheetMap)
                return [];
            const mergeIds = new Set();
            for (const { col, row } of positions(zone)) {
                const mergeId = sheetMap[col]?.[row];
                if (mergeId) {
                    mergeIds.add(mergeId);
                }
            }
            return Array.from(mergeIds)
                .map((mergeId) => this.getMergeById(sheetId, mergeId))
                .filter(isDefined$1);
        }
        /**
         * Same as `getRangeString` but add all necessary merge to the range to make it a valid selection
         */
        getSelectionRangeString(range, forSheetId) {
            const rangeImpl = RangeImpl.fromRange(range, this.getters);
            const expandedZone = this.getters.expandZone(rangeImpl.sheetId, rangeImpl.zone);
            const expandedRange = rangeImpl.clone({
                zone: {
                    ...expandedZone,
                    bottom: rangeImpl.isFullCol ? undefined : expandedZone.bottom,
                    right: rangeImpl.isFullRow ? undefined : expandedZone.right,
                },
            });
            const rangeString = this.getters.getRangeString(expandedRange, forSheetId);
            if (this.isSingleCellOrMerge(rangeImpl.sheetId, rangeImpl.zone)) {
                const { sheetName, xc } = splitReference(rangeString);
                return `${sheetName !== undefined ? getCanonicalSheetName(sheetName) + "!" : ""}${xc.split(":")[0]}`;
            }
            return rangeString;
        }
        /**
         * Return true if the zone intersects an existing merge:
         * if they have at least a common cell
         */
        doesIntersectMerge(sheetId, zone) {
            return positions(zone).some(({ col, row }) => this.getMerge({ sheetId, col, row }) !== undefined);
        }
        /**
         * Returns true if two columns have at least one merge in common
         */
        doesColumnsHaveCommonMerges(sheetId, colA, colB) {
            const sheet = this.getters.getSheet(sheetId);
            for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
                if (this.isInSameMerge(sheet.id, colA, row, colB, row)) {
                    return true;
                }
            }
            return false;
        }
        /**
         * Returns true if two rows have at least one merge in common
         */
        doesRowsHaveCommonMerges(sheetId, rowA, rowB) {
            const sheet = this.getters.getSheet(sheetId);
            for (let col = 0; col <= this.getters.getNumberCols(sheetId); col++) {
                if (this.isInSameMerge(sheet.id, col, rowA, col, rowB)) {
                    return true;
                }
            }
            return false;
        }
        /**
         * Add all necessary merge to the current selection to make it valid
         */
        expandZone(sheetId, zone) {
            let { left, right, top, bottom } = zone;
            let result = { left, right, top, bottom };
            for (let id in this.merges[sheetId]) {
                const merge = this.getMergeById(sheetId, parseInt(id));
                if (merge && overlap(merge, result)) {
                    result = union(merge, result);
                }
            }
            return isEqual(result, zone) ? result : this.expandZone(sheetId, result);
        }
        isInSameMerge(sheetId, colA, rowA, colB, rowB) {
            const mergeA = this.getMerge({ sheetId, col: colA, row: rowA });
            const mergeB = this.getMerge({ sheetId, col: colB, row: rowB });
            if (!mergeA || !mergeB) {
                return false;
            }
            return isEqual(mergeA, mergeB);
        }
        isInMerge({ sheetId, col, row }) {
            const sheetMap = this.mergeCellMap[sheetId];
            return sheetMap ? col in sheetMap && Boolean(sheetMap[col]?.[row]) : false;
        }
        getMainCellPosition(position) {
            if (!this.isInMerge(position)) {
                return position;
            }
            const mergeTopLeftPos = this.getMerge(position).topLeft;
            return { sheetId: position.sheetId, col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };
        }
        getBottomLeftCell(position) {
            if (!this.isInMerge(position)) {
                return position;
            }
            const { bottom, left } = this.getMerge(position);
            return { sheetId: position.sheetId, col: left, row: bottom };
        }
        isMergeHidden(sheetId, merge) {
            const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);
            const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);
            for (let group of hiddenColsGroups) {
                if (merge.left >= group[0] && merge.right <= group[group.length - 1]) {
                    return true;
                }
            }
            for (let group of hiddenRowsGroups) {
                if (merge.top >= group[0] && merge.bottom <= group[group.length - 1]) {
                    return true;
                }
            }
            return false;
        }
        /**
         * Check if the zone represents a single cell or a single merge.
         */
        isSingleCellOrMerge(sheetId, zone) {
            const merge = this.getMerge({ sheetId, col: zone.left, row: zone.top });
            if (merge) {
                return isEqual(zone, merge);
            }
            const { numberOfCols, numberOfRows } = zoneToDimension(zone);
            return numberOfCols === 1 && numberOfRows === 1;
        }
        isMainCellPosition(position) {
            return deepEquals(this.getMainCellPosition(position), position);
        }
        // ---------------------------------------------------------------------------
        // Merges
        // ---------------------------------------------------------------------------
        /**
         * Return true if the current selection requires losing state if it is merged.
         * This happens when there is some textual content in other cells than the
         * top left.
         */
        isMergeDestructive(sheetId, zone) {
            let { left, right, top, bottom } = zone;
            right = clip(right, 0, this.getters.getNumberCols(sheetId) - 1);
            bottom = clip(bottom, 0, this.getters.getNumberRows(sheetId) - 1);
            for (let row = top; row <= bottom; row++) {
                for (let col = left; col <= right; col++) {
                    if (col !== left || row !== top) {
                        const cell = this.getters.getCell({ sheetId, col, row });
                        if (cell && cell.content !== "") {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        getMergeById(sheetId, mergeId) {
            const range = this.merges[sheetId]?.[mergeId];
            return range !== undefined ? rangeToMerge(mergeId, range) : undefined;
        }
        checkDestructiveMerge({ sheetId, target }) {
            const sheet = this.getters.tryGetSheet(sheetId);
            if (!sheet)
                return "Success" /* CommandResult.Success */;
            const isDestructive = target.some((zone) => this.isMergeDestructive(sheetId, zone));
            return isDestructive ? "MergeIsDestructive" /* CommandResult.MergeIsDestructive */ : "Success" /* CommandResult.Success */;
        }
        checkOverlap({ target }) {
            for (const zone of target) {
                for (const zone2 of target) {
                    if (zone !== zone2 && overlap(zone, zone2)) {
                        return "MergeOverlap" /* CommandResult.MergeOverlap */;
                    }
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        checkFrozenPanes({ sheetId, target }) {
            const sheet = this.getters.tryGetSheet(sheetId);
            if (!sheet)
                return "Success" /* CommandResult.Success */;
            const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
            for (const zone of target) {
                if ((zone.left < xSplit && zone.right >= xSplit) ||
                    (zone.top < ySplit && zone.bottom >= ySplit)) {
                    return "FrozenPaneOverlap" /* CommandResult.FrozenPaneOverlap */;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        /**
         * The content of a merged cell should always be empty.
         * Except for the top-left cell.
         */
        checkMergedContentUpdate(cmd) {
            const { col, row, content } = cmd;
            if (content === undefined) {
                return "Success" /* CommandResult.Success */;
            }
            const { col: mainCol, row: mainRow } = this.getMainCellPosition(cmd);
            if (mainCol === col && mainRow === row) {
                return "Success" /* CommandResult.Success */;
            }
            return "CellIsMerged" /* CommandResult.CellIsMerged */;
        }
        checkMergeExists(cmd) {
            const { sheetId, target } = cmd;
            for (const zone of target) {
                const { left, top } = zone;
                const merge = this.getMerge({ sheetId, col: left, row: top });
                if (merge === undefined || !isEqual(zone, merge)) {
                    return "InvalidTarget" /* CommandResult.InvalidTarget */;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        /**
         * Merge the current selection. Note that:
         * - it assumes that we have a valid selection (no intersection with other
         *   merges)
         * - it does nothing if the merge is trivial: A1:A1
         */
        addMerge(sheetId, zone) {
            let { left, right, top, bottom } = zone;
            right = clip(right, 0, this.getters.getNumberCols(sheetId) - 1);
            bottom = clip(bottom, 0, this.getters.getNumberRows(sheetId) - 1);
            const tl = toXC(left, top);
            const br = toXC(right, bottom);
            if (tl === br) {
                return;
            }
            const topLeft = this.getters.getCell({ sheetId, col: left, row: top });
            let id = this.nextId++;
            this.history.update("merges", sheetId, id, this.getters.getRangeFromSheetXC(sheetId, zoneToXc({ left, top, right, bottom })));
            let previousMerges = new Set();
            for (let row = top; row <= bottom; row++) {
                for (let col = left; col <= right; col++) {
                    if (col !== left || row !== top) {
                        this.dispatch("UPDATE_CELL", {
                            sheetId,
                            col,
                            row,
                            style: topLeft ? topLeft.style : null,
                            content: "",
                        });
                    }
                    const merge = this.getMerge({ sheetId, col, row });
                    if (merge) {
                        previousMerges.add(merge.id);
                    }
                    this.history.update("mergeCellMap", sheetId, col, row, id);
                }
            }
            for (let mergeId of previousMerges) {
                const { top, bottom, left, right } = this.getMergeById(sheetId, mergeId);
                for (let row = top; row <= bottom; row++) {
                    for (let col = left; col <= right; col++) {
                        const position = { sheetId, col, row };
                        const merge = this.getMerge(position);
                        if (!merge || merge.id !== id) {
                            this.history.update("mergeCellMap", sheetId, col, row, undefined);
                            this.dispatch("CLEAR_CELL", position);
                        }
                    }
                }
                this.history.update("merges", sheetId, mergeId, undefined);
            }
        }
        removeMerge(sheetId, zone) {
            const { left, top, bottom, right } = zone;
            const merge = this.getMerge({ sheetId, col: left, row: top });
            if (merge === undefined || !isEqual(zone, merge)) {
                return;
            }
            this.history.update("merges", sheetId, merge.id, undefined);
            for (let r = top; r <= bottom; r++) {
                for (let c = left; c <= right; c++) {
                    this.history.update("mergeCellMap", sheetId, c, r, undefined);
                }
            }
        }
        /**
         * Apply a range change on merges of a particular sheet.
         */
        applyRangeChangeOnSheet(sheetId, applyChange) {
            const merges = Object.entries(this.merges[sheetId] || {});
            for (const [mergeId, range] of merges) {
                if (range) {
                    const currentZone = range.zone;
                    const result = applyChange(range);
                    switch (result.changeType) {
                        case "NONE":
                            break;
                        case "REMOVE":
                            this.removeMerge(sheetId, currentZone);
                            break;
                        default:
                            const { numberOfCols, numberOfRows } = zoneToDimension(result.range.zone);
                            if (numberOfCols === 1 && numberOfRows === 1) {
                                this.removeMerge(sheetId, currentZone);
                            }
                            else {
                                this.history.update("merges", sheetId, parseInt(mergeId, 10), result.range);
                            }
                            break;
                    }
                }
            }
            this.history.update("mergeCellMap", sheetId, {});
            for (const merge of this.getMerges(sheetId)) {
                for (const { col, row } of positions(merge)) {
                    this.history.update("mergeCellMap", sheetId, col, row, merge.id);
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            const sheets = data.sheets || [];
            for (let sheetData of sheets) {
                this.history.update("merges", sheetData.id, {});
                this.history.update("mergeCellMap", sheetData.id, {});
                if (sheetData.merges) {
                    this.importMerges(sheetData.id, sheetData.merges);
                }
            }
        }
        importMerges(sheetId, merges) {
            for (let merge of merges) {
                this.addMerge(sheetId, toZone(merge));
            }
        }
        export(data) {
            for (let sheetData of data.sheets) {
                const merges = this.merges[sheetData.id];
                if (merges) {
                    sheetData.merges.push(...exportMerges(merges));
                }
            }
        }
        exportForExcel(data) {
            this.export(data);
        }
    }
    function exportMerges(merges) {
        return Object.entries(merges)
            .map(([mergeId, range]) => (range ? rangeToMerge(parseInt(mergeId, 10), range) : undefined))
            .filter(isDefined$1)
            .map((merge) => toXC(merge.left, merge.top) + ":" + toXC(merge.right, merge.bottom));
    }
    function rangeToMerge(mergeId, range) {
        return {
            ...range.zone,
            topLeft: { col: range.zone.left, row: range.zone.top },
            id: mergeId,
        };
    }

    class RangeAdapter {
        getters;
        providers = [];
        constructor(getters) {
            this.getters = getters;
        }
        static getters = [
            "getRangeString",
            "getRangeFromSheetXC",
            "createAdaptedRanges",
            "getRangeDataFromXc",
            "getRangeDataFromZone",
            "getRangeFromRangeData",
            "getRangeFromZone",
        ];
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            if (cmd.type === "MOVE_RANGES") {
                return cmd.target.length === 1 ? "Success" /* CommandResult.Success */ : "InvalidZones" /* CommandResult.InvalidZones */;
            }
            return "Success" /* CommandResult.Success */;
        }
        beforeHandle(command) { }
        handle(cmd) {
            switch (cmd.type) {
                case "REMOVE_COLUMNS_ROWS": {
                    let start = cmd.dimension === "COL" ? "left" : "top";
                    let end = cmd.dimension === "COL" ? "right" : "bottom";
                    let dimension = cmd.dimension === "COL" ? "columns" : "rows";
                    const elements = [...cmd.elements];
                    elements.sort((a, b) => b - a);
                    const groups = groupConsecutive(elements);
                    this.executeOnAllRanges((range) => {
                        if (range.sheetId !== cmd.sheetId) {
                            return { changeType: "NONE" };
                        }
                        let newRange = range;
                        let changeType = "NONE";
                        for (let group of groups) {
                            const min = Math.min(...group);
                            const max = Math.max(...group);
                            if (range.zone[start] <= min && min <= range.zone[end]) {
                                const toRemove = Math.min(range.zone[end], max) - min + 1;
                                changeType = "RESIZE";
                                newRange = this.createAdaptedRange(newRange, dimension, changeType, -toRemove);
                            }
                            else if (range.zone[start] >= min && range.zone[end] <= max) {
                                changeType = "REMOVE";
                                newRange = range.clone({ ...this.getInvalidRange() });
                            }
                            else if (range.zone[start] <= max && range.zone[end] >= max) {
                                const toRemove = max - range.zone[start] + 1;
                                changeType = "RESIZE";
                                newRange = this.createAdaptedRange(newRange, dimension, changeType, -toRemove);
                                newRange = this.createAdaptedRange(newRange, dimension, "MOVE", -(range.zone[start] - min));
                            }
                            else if (min < range.zone[start]) {
                                changeType = "MOVE";
                                newRange = this.createAdaptedRange(newRange, dimension, changeType, -(max - min + 1));
                            }
                        }
                        if (changeType !== "NONE") {
                            return { changeType, range: newRange };
                        }
                        return { changeType: "NONE" };
                    }, cmd.sheetId);
                    break;
                }
                case "ADD_COLUMNS_ROWS": {
                    let start = cmd.dimension === "COL" ? "left" : "top";
                    let end = cmd.dimension === "COL" ? "right" : "bottom";
                    let dimension = cmd.dimension === "COL" ? "columns" : "rows";
                    this.executeOnAllRanges((range) => {
                        if (range.sheetId !== cmd.sheetId) {
                            return { changeType: "NONE" };
                        }
                        if (cmd.position === "after") {
                            if (range.zone[start] <= cmd.base && cmd.base < range.zone[end]) {
                                return {
                                    changeType: "RESIZE",
                                    range: this.createAdaptedRange(range, dimension, "RESIZE", cmd.quantity),
                                };
                            }
                            if (cmd.base < range.zone[start]) {
                                return {
                                    changeType: "MOVE",
                                    range: this.createAdaptedRange(range, dimension, "MOVE", cmd.quantity),
                                };
                            }
                        }
                        else {
                            if (range.zone[start] < cmd.base && cmd.base <= range.zone[end]) {
                                return {
                                    changeType: "RESIZE",
                                    range: this.createAdaptedRange(range, dimension, "RESIZE", cmd.quantity),
                                };
                            }
                            if (cmd.base <= range.zone[start]) {
                                return {
                                    changeType: "MOVE",
                                    range: this.createAdaptedRange(range, dimension, "MOVE", cmd.quantity),
                                };
                            }
                        }
                        return { changeType: "NONE" };
                    }, cmd.sheetId);
                    break;
                }
                case "DELETE_SHEET": {
                    this.executeOnAllRanges((range) => {
                        if (range.sheetId !== cmd.sheetId) {
                            return { changeType: "NONE" };
                        }
                        const invalidSheetName = this.getters.getSheetName(cmd.sheetId);
                        range = range.clone({
                            ...this.getInvalidRange(),
                            invalidSheetName,
                        });
                        return { changeType: "REMOVE", range };
                    }, cmd.sheetId);
                    break;
                }
                case "RENAME_SHEET": {
                    this.executeOnAllRanges((range) => {
                        if (range.sheetId === cmd.sheetId) {
                            return { changeType: "CHANGE", range };
                        }
                        if (cmd.name && range.invalidSheetName === cmd.name) {
                            const invalidSheetName = undefined;
                            const sheetId = cmd.sheetId;
                            const newRange = range.clone({ sheetId, invalidSheetName });
                            return { changeType: "CHANGE", range: newRange };
                        }
                        return { changeType: "NONE" };
                    });
                    break;
                }
                case "MOVE_RANGES": {
                    const originZone = cmd.target[0];
                    this.executeOnAllRanges((range) => {
                        if (range.sheetId !== cmd.sheetId || !isZoneInside(range.zone, originZone)) {
                            return { changeType: "NONE" };
                        }
                        const targetSheetId = cmd.targetSheetId;
                        const offsetX = cmd.col - originZone.left;
                        const offsetY = cmd.row - originZone.top;
                        const adaptedRange = this.createAdaptedRange(range, "both", "MOVE", [offsetX, offsetY]);
                        const prefixSheet = cmd.sheetId === targetSheetId ? adaptedRange.prefixSheet : true;
                        return {
                            changeType: "MOVE",
                            range: adaptedRange.clone({ sheetId: targetSheetId, prefixSheet }),
                        };
                    });
                    break;
                }
            }
        }
        finalize() { }
        /**
         * Return a modified adapting function that verifies that after adapting a range, the range is still valid.
         * Any range that gets adapted by the function adaptRange in parameter does so
         * without caring if the start and end of the range in both row and column
         * direction can be incorrect. This function ensure that an incorrect range gets removed.
         */
        verifyRangeRemoved(adaptRange) {
            return (range) => {
                const result = adaptRange(range);
                if (result.changeType !== "NONE" && !isZoneValid(result.range.zone)) {
                    return { range: result.range, changeType: "REMOVE" };
                }
                return result;
            };
        }
        createAdaptedRange(range, dimension, operation, by) {
            const zone = createAdaptedZone(range.unboundedZone, dimension, operation, by);
            const adaptedRange = range.clone({ zone });
            return adaptedRange;
        }
        executeOnAllRanges(adaptRange, sheetId) {
            const func = this.verifyRangeRemoved(adaptRange);
            for (const provider of this.providers) {
                provider(func, sheetId);
            }
        }
        /**
         * Stores the functions bound to each plugin to be able to iterate over all ranges of the application,
         * without knowing any details of the internal data structure of the plugins and without storing ranges
         * in the range adapter.
         *
         * @param provider a function bound to a plugin that will loop over its internal data structure to find
         * all ranges
         */
        addRangeProvider(provider) {
            this.providers.push(provider);
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        createAdaptedRanges(ranges, offsetX, offsetY, sheetId) {
            const rangesImpl = ranges.map((range) => RangeImpl.fromRange(range, this.getters));
            return rangesImpl.map((range) => {
                if (!isZoneValid(range.zone)) {
                    return range;
                }
                const copySheetId = range.prefixSheet ? range.sheetId : sheetId;
                const unboundZone = {
                    ...range.unboundedZone,
                    // Don't shift left if the range is a full row without header
                    left: range.isFullRow && !range.unboundedZone.hasHeader
                        ? range.unboundedZone.left
                        : range.unboundedZone.left + (range.parts[0].colFixed ? 0 : offsetX),
                    // Don't shift right if the range is a full row
                    right: range.isFullRow
                        ? range.unboundedZone.right
                        : range.unboundedZone.right +
                            ((range.parts[1] || range.parts[0]).colFixed ? 0 : offsetX),
                    // Don't shift up if the range is a column row without header
                    top: range.isFullCol && !range.unboundedZone.hasHeader
                        ? range.unboundedZone.top
                        : range.unboundedZone.top + (range.parts[0].rowFixed ? 0 : offsetY),
                    // Don't shift down if the range is a full column
                    bottom: range.isFullCol
                        ? range.unboundedZone.bottom
                        : range.unboundedZone.bottom +
                            ((range.parts[1] || range.parts[0]).rowFixed ? 0 : offsetY),
                };
                return range.clone({ sheetId: copySheetId, zone: unboundZone }).orderZone();
            });
        }
        /**
         * Creates a range from a XC reference that can contain a sheet reference
         * @param defaultSheetId the sheet to default to if the sheetXC parameter does not contain a sheet reference (usually the active sheet Id)
         * @param sheetXC the string description of a range, in the form SheetName!XC:XC
         */
        getRangeFromSheetXC(defaultSheetId, sheetXC) {
            if (!rangeReference.test(sheetXC)) {
                return new RangeImpl({
                    sheetId: "",
                    zone: { left: -1, top: -1, right: -1, bottom: -1 },
                    parts: [],
                    invalidXc: sheetXC,
                    prefixSheet: false,
                }, this.getters.getSheetSize);
            }
            let sheetName;
            let xc = sheetXC;
            let prefixSheet = false;
            if (sheetXC.includes("!")) {
                ({ xc, sheetName } = splitReference(sheetXC));
                if (sheetName) {
                    prefixSheet = true;
                }
            }
            const zone = toUnboundedZone(xc);
            const parts = RangeImpl.getRangeParts(xc, zone);
            const invalidSheetName = sheetName && !this.getters.getSheetIdByName(sheetName) ? sheetName : undefined;
            const sheetId = this.getters.getSheetIdByName(sheetName) || defaultSheetId;
            const rangeInterface = { prefixSheet, zone, sheetId, invalidSheetName, parts };
            return new RangeImpl(rangeInterface, this.getters.getSheetSize).orderZone();
        }
        /**
         * Gets the string that represents the range as it is at the moment of the call.
         * The string will be prefixed with the sheet name if the call specified a sheet id in `forSheetId`
         * different than the sheet on which the range has been created.
         *
         * @param range the range (received from getRangeFromXC or getRangeFromZone)
         * @param forSheetId the id of the sheet where the range string is supposed to be used.
         * @param options
         * @param options.useFixedReference if true, the range will be returned with fixed row and column
         */
        getRangeString(range, forSheetId, options = { useFixedReference: false }) {
            if (!range) {
                return INCORRECT_RANGE_STRING;
            }
            if (range.invalidXc) {
                return range.invalidXc;
            }
            if (range.zone.bottom - range.zone.top < 0 || range.zone.right - range.zone.left < 0) {
                return INCORRECT_RANGE_STRING;
            }
            if (range.zone.left < 0 || range.zone.top < 0) {
                return INCORRECT_RANGE_STRING;
            }
            const rangeImpl = RangeImpl.fromRange(range, this.getters);
            let prefixSheet = rangeImpl.sheetId !== forSheetId || rangeImpl.invalidSheetName || rangeImpl.prefixSheet;
            let sheetName = "";
            if (prefixSheet) {
                if (rangeImpl.invalidSheetName) {
                    sheetName = rangeImpl.invalidSheetName;
                }
                else {
                    sheetName = getCanonicalSheetName(this.getters.getSheetName(rangeImpl.sheetId));
                }
            }
            if (prefixSheet && !sheetName) {
                return INCORRECT_RANGE_STRING;
            }
            let rangeString = this.getRangePartString(rangeImpl, 0, options);
            if (rangeImpl.parts && rangeImpl.parts.length === 2) {
                // this if converts A2:A2 into A2 except if any part of the original range had fixed row or column (with $)
                if (rangeImpl.zone.top !== rangeImpl.zone.bottom ||
                    rangeImpl.zone.left !== rangeImpl.zone.right ||
                    rangeImpl.parts[0].rowFixed ||
                    rangeImpl.parts[0].colFixed ||
                    rangeImpl.parts[1].rowFixed ||
                    rangeImpl.parts[1].colFixed) {
                    rangeString += ":";
                    rangeString += this.getRangePartString(rangeImpl, 1, options);
                }
            }
            return `${prefixSheet ? sheetName + "!" : ""}${rangeString}`;
        }
        getRangeDataFromXc(sheetId, xc) {
            return this.getters.getRangeFromSheetXC(sheetId, xc).rangeData;
        }
        getRangeDataFromZone(sheetId, zone) {
            return { _sheetId: sheetId, _zone: zone };
        }
        getRangeFromZone(sheetId, zone) {
            return new RangeImpl({
                sheetId,
                zone,
                parts: [
                    { colFixed: false, rowFixed: false },
                    { colFixed: false, rowFixed: false },
                ],
                prefixSheet: false,
            }, this.getters.getSheetSize);
        }
        getRangeFromRangeData(data) {
            const rangeInterface = {
                prefixSheet: false,
                zone: data._zone,
                sheetId: data._sheetId,
                invalidSheetName: undefined,
                parts: [
                    { colFixed: false, rowFixed: false },
                    { colFixed: false, rowFixed: false },
                ],
            };
            return new RangeImpl(rangeInterface, this.getters.getSheetSize);
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        /**
         * Get a Xc string that represent a part of a range
         */
        getRangePartString(range, part, options = { useFixedReference: false }) {
            const colFixed = range.parts && range.parts[part]?.colFixed ? "$" : "";
            const col = part === 0 ? numberToLetters(range.zone.left) : numberToLetters(range.zone.right);
            const rowFixed = range.parts && range.parts[part]?.rowFixed ? "$" : "";
            const row = part === 0 ? String(range.zone.top + 1) : String(range.zone.bottom + 1);
            let str = "";
            if (range.isFullCol && !options.useFixedReference) {
                if (part === 0 && range.unboundedZone.hasHeader) {
                    str = colFixed + col + rowFixed + row;
                }
                else {
                    str = colFixed + col;
                }
            }
            else if (range.isFullRow && !options.useFixedReference) {
                if (part === 0 && range.unboundedZone.hasHeader) {
                    str = colFixed + col + rowFixed + row;
                }
                else {
                    str = rowFixed + row;
                }
            }
            else {
                str = colFixed + col + rowFixed + row;
            }
            return str;
        }
        getInvalidRange() {
            return {
                parts: [],
                prefixSheet: false,
                zone: { left: -1, top: -1, right: -1, bottom: -1 },
                sheetId: "",
                invalidXc: INCORRECT_RANGE_STRING,
            };
        }
    }

    class SheetPlugin extends CorePlugin {
        static getters = [
            "getSheetName",
            "tryGetSheetName",
            "getSheet",
            "tryGetSheet",
            "getSheetIdByName",
            "getSheetIds",
            "getVisibleSheetIds",
            "isSheetVisible",
            "getEvaluationSheets",
            "doesHeaderExist",
            "doesHeadersExist",
            "getCell",
            "getCellPosition",
            "getColsZone",
            "getRowCells",
            "getRowsZone",
            "getNumberCols",
            "getNumberRows",
            "getNumberHeaders",
            "getGridLinesVisibility",
            "getNextSheetName",
            "isEmpty",
            "getSheetSize",
            "getSheetZone",
            "getPaneDivisions",
            "checkZonesExistInSheet",
            "getCommandZones",
            "getUnboundedZone",
            "checkElementsIncludeAllNonFrozenHeaders",
        ];
        sheetIdsMapName = {};
        orderedSheetIds = [];
        sheets = {};
        cellPosition = {};
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            const genericChecks = this.chainValidations(this.checkSheetExists, this.checkZonesAreInSheet)(cmd);
            if (genericChecks !== "Success" /* CommandResult.Success */) {
                return genericChecks;
            }
            switch (cmd.type) {
                case "HIDE_SHEET": {
                    if (this.getVisibleSheetIds().length === 1) {
                        return "NotEnoughSheets" /* CommandResult.NotEnoughSheets */;
                    }
                    return "Success" /* CommandResult.Success */;
                }
                case "CREATE_SHEET": {
                    return this.checkValidations(cmd, this.checkSheetName, this.checkSheetPosition);
                }
                case "MOVE_SHEET":
                    try {
                        const currentIndex = this.orderedSheetIds.findIndex((id) => id === cmd.sheetId);
                        this.findIndexOfTargetSheet(currentIndex, cmd.delta);
                        return "Success" /* CommandResult.Success */;
                    }
                    catch (e) {
                        return "WrongSheetMove" /* CommandResult.WrongSheetMove */;
                    }
                case "RENAME_SHEET":
                    return this.isRenameAllowed(cmd);
                case "DELETE_SHEET":
                    return this.orderedSheetIds.length > 1
                        ? "Success" /* CommandResult.Success */
                        : "NotEnoughSheets" /* CommandResult.NotEnoughSheets */;
                case "ADD_COLUMNS_ROWS":
                    if (!this.doesHeaderExist(cmd.sheetId, cmd.dimension, cmd.base)) {
                        return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
                    }
                    else if (cmd.quantity <= 0) {
                        return "InvalidQuantity" /* CommandResult.InvalidQuantity */;
                    }
                    return "Success" /* CommandResult.Success */;
                case "REMOVE_COLUMNS_ROWS": {
                    const min = Math.min(...cmd.elements);
                    const max = Math.max(...cmd.elements);
                    if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
                        return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
                    }
                    else if (this.checkElementsIncludeAllNonFrozenHeaders(cmd.sheetId, cmd.dimension, cmd.elements)) {
                        return "NotEnoughElements" /* CommandResult.NotEnoughElements */;
                    }
                    else {
                        return "Success" /* CommandResult.Success */;
                    }
                }
                case "FREEZE_ROWS": {
                    return this.checkValidations(cmd, this.checkRowFreezeQuantity, this.checkRowFreezeOverlapMerge);
                }
                case "FREEZE_COLUMNS": {
                    return this.checkValidations(cmd, this.checkColFreezeQuantity, this.checkColFreezeOverlapMerge);
                }
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "SET_GRID_LINES_VISIBILITY":
                    this.setGridLinesVisibility(cmd.sheetId, cmd.areGridLinesVisible);
                    break;
                case "DELETE_CONTENT":
                    this.clearZones(cmd.sheetId, cmd.target);
                    break;
                case "CREATE_SHEET":
                    const sheet = this.createSheet(cmd.sheetId, cmd.name || this.getNextSheetName(), cmd.cols || 26, cmd.rows || 100, cmd.position);
                    this.history.update("sheetIdsMapName", sheet.name, sheet.id);
                    break;
                case "MOVE_SHEET":
                    this.moveSheet(cmd.sheetId, cmd.delta);
                    break;
                case "RENAME_SHEET":
                    this.renameSheet(this.sheets[cmd.sheetId], cmd.name);
                    break;
                case "HIDE_SHEET":
                    this.hideSheet(cmd.sheetId);
                    break;
                case "SHOW_SHEET":
                    this.showSheet(cmd.sheetId);
                    break;
                case "DUPLICATE_SHEET":
                    this.duplicateSheet(cmd.sheetId, cmd.sheetIdTo);
                    break;
                case "DELETE_SHEET":
                    this.deleteSheet(this.sheets[cmd.sheetId]);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                    if (cmd.dimension === "COL") {
                        this.removeColumns(this.sheets[cmd.sheetId], [...cmd.elements]);
                    }
                    else {
                        this.removeRows(this.sheets[cmd.sheetId], [...cmd.elements]);
                    }
                    break;
                case "ADD_COLUMNS_ROWS":
                    if (cmd.dimension === "COL") {
                        this.addColumns(this.sheets[cmd.sheetId], cmd.base, cmd.position, cmd.quantity);
                    }
                    else {
                        this.addRows(this.sheets[cmd.sheetId], cmd.base, cmd.position, cmd.quantity);
                    }
                    break;
                case "UPDATE_CELL_POSITION":
                    this.updateCellPosition(cmd);
                    break;
                case "FREEZE_COLUMNS":
                    this.setPaneDivisions(cmd.sheetId, cmd.quantity, "COL");
                    break;
                case "FREEZE_ROWS":
                    this.setPaneDivisions(cmd.sheetId, cmd.quantity, "ROW");
                    break;
                case "UNFREEZE_ROWS":
                    this.setPaneDivisions(cmd.sheetId, 0, "ROW");
                    break;
                case "UNFREEZE_COLUMNS":
                    this.setPaneDivisions(cmd.sheetId, 0, "COL");
                    break;
                case "UNFREEZE_COLUMNS_ROWS":
                    this.setPaneDivisions(cmd.sheetId, 0, "COL");
                    this.setPaneDivisions(cmd.sheetId, 0, "ROW");
            }
        }
        // ---------------------------------------------------------------------------
        // Import/Export
        // ---------------------------------------------------------------------------
        import(data) {
            // we need to fill the sheetIds mapping first, because otherwise formulas
            // that depends on a sheet not already imported will not be able to be
            // compiled
            for (let sheet of data.sheets) {
                this.sheetIdsMapName[sheet.name] = sheet.id;
            }
            for (let sheetData of data.sheets) {
                const name = sheetData.name || _t("Sheet") + (Object.keys(this.sheets).length + 1);
                const { colNumber, rowNumber } = this.getImportedSheetSize(sheetData);
                const sheet = {
                    id: sheetData.id,
                    name: name,
                    numberOfCols: colNumber,
                    rows: createDefaultRows(rowNumber),
                    areGridLinesVisible: sheetData.areGridLinesVisible === undefined ? true : sheetData.areGridLinesVisible,
                    isVisible: sheetData.isVisible,
                    panes: {
                        xSplit: sheetData.panes?.xSplit || 0,
                        ySplit: sheetData.panes?.ySplit || 0,
                    },
                };
                this.orderedSheetIds.push(sheet.id);
                this.sheets[sheet.id] = sheet;
            }
        }
        exportSheets(data) {
            data.sheets = this.orderedSheetIds.filter(isDefined$1).map((id) => {
                const sheet = this.sheets[id];
                const sheetData = {
                    id: sheet.id,
                    name: sheet.name,
                    colNumber: sheet.numberOfCols,
                    rowNumber: this.getters.getNumberRows(sheet.id),
                    rows: {},
                    cols: {},
                    merges: [],
                    cells: {},
                    conditionalFormats: [],
                    figures: [],
                    filterTables: [],
                    areGridLinesVisible: sheet.areGridLinesVisible === undefined ? true : sheet.areGridLinesVisible,
                    isVisible: sheet.isVisible,
                };
                if (sheet.panes.xSplit || sheet.panes.ySplit) {
                    sheetData.panes = sheet.panes;
                }
                return sheetData;
            });
        }
        export(data) {
            this.exportSheets(data);
        }
        exportForExcel(data) {
            this.exportSheets(data);
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getGridLinesVisibility(sheetId) {
            return this.getSheet(sheetId).areGridLinesVisible;
        }
        tryGetSheet(sheetId) {
            return this.sheets[sheetId];
        }
        getSheet(sheetId) {
            const sheet = this.sheets[sheetId];
            if (!sheet) {
                throw new Error(`Sheet ${sheetId} not found.`);
            }
            return sheet;
        }
        isSheetVisible(sheetId) {
            return this.getSheet(sheetId).isVisible;
        }
        /**
         * Return the sheet name. Throw if the sheet is not found.
         */
        getSheetName(sheetId) {
            return this.getSheet(sheetId).name;
        }
        /**
         * Return the sheet name or undefined if the sheet doesn't exist.
         */
        tryGetSheetName(sheetId) {
            return this.tryGetSheet(sheetId)?.name;
        }
        getSheetIdByName(name) {
            if (name) {
                const unquotedName = getUnquotedSheetName(name);
                for (const key in this.sheetIdsMapName) {
                    if (key.toUpperCase() === unquotedName.toUpperCase()) {
                        return this.sheetIdsMapName[key];
                    }
                }
            }
            return undefined;
        }
        getSheetIds() {
            return this.orderedSheetIds;
        }
        getVisibleSheetIds() {
            return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));
        }
        getEvaluationSheets() {
            return this.sheets;
        }
        doesHeaderExist(sheetId, dimension, index) {
            return dimension === "COL"
                ? index >= 0 && index < this.getNumberCols(sheetId)
                : index >= 0 && index < this.getNumberRows(sheetId);
        }
        doesHeadersExist(sheetId, dimension, headerIndexes) {
            return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));
        }
        getRow(sheetId, index) {
            const row = this.getSheet(sheetId).rows[index];
            if (!row) {
                throw new Error(`Row ${row} not found.`);
            }
            return row;
        }
        getCell({ sheetId, col, row }) {
            const sheet = this.tryGetSheet(sheetId);
            const cellId = sheet?.rows[row]?.cells[col];
            if (cellId === undefined) {
                return undefined;
            }
            return this.getters.getCellById(cellId);
        }
        getColsZone(sheetId, start, end) {
            return {
                top: 0,
                bottom: this.getNumberRows(sheetId) - 1,
                left: start,
                right: end,
            };
        }
        getRowCells(sheetId, row) {
            return Object.values(this.getSheet(sheetId).rows[row]?.cells).filter(isDefined$1);
        }
        getRowsZone(sheetId, start, end) {
            return {
                top: start,
                bottom: end,
                left: 0,
                right: this.getSheet(sheetId).numberOfCols - 1,
            };
        }
        getCellPosition(cellId) {
            const cell = this.cellPosition[cellId];
            if (!cell) {
                throw new Error(`asking for a cell position that doesn't exist, cell id: ${cellId}`);
            }
            return cell;
        }
        getNumberCols(sheetId) {
            return this.getSheet(sheetId).numberOfCols;
        }
        getNumberRows(sheetId) {
            return this.getSheet(sheetId).rows.length;
        }
        getNumberHeaders(sheetId, dimension) {
            return dimension === "COL" ? this.getNumberCols(sheetId) : this.getNumberRows(sheetId);
        }
        getNextSheetName(baseName = "Sheet") {
            let i = 1;
            const names = this.orderedSheetIds.map(this.getSheetName.bind(this));
            let name = `${baseName}${i}`;
            while (names.includes(name)) {
                name = `${baseName}${i}`;
                i++;
            }
            return name;
        }
        getSheetSize(sheetId) {
            return {
                numberOfRows: this.getNumberRows(sheetId),
                numberOfCols: this.getNumberCols(sheetId),
            };
        }
        getSheetZone(sheetId) {
            return {
                top: 0,
                left: 0,
                bottom: this.getNumberRows(sheetId) - 1,
                right: this.getNumberCols(sheetId) - 1,
            };
        }
        getUnboundedZone(sheetId, zone) {
            const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;
            const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;
            return {
                ...zone,
                bottom: isFullCol ? undefined : zone.bottom,
                // cannot be unbounded in the 2 dimensions at once
                right: isFullRow && !isFullCol ? undefined : zone.right,
            };
        }
        getPaneDivisions(sheetId) {
            return this.getSheet(sheetId).panes;
        }
        setPaneDivisions(sheetId, base, dimension) {
            const panes = { ...this.getPaneDivisions(sheetId) };
            if (dimension === "COL") {
                panes.xSplit = base;
            }
            else if (dimension === "ROW") {
                panes.ySplit = base;
            }
            this.history.update("sheets", sheetId, "panes", panes);
        }
        /**
         * Checks if all non-frozen header indices are present in the provided elements of selected rows/columns.
         * This validation ensures that all rows or columns cannot be deleted when frozen panes exist.
         */
        checkElementsIncludeAllNonFrozenHeaders(sheetId, dimension, elements) {
            const paneDivisions = this.getters.getPaneDivisions(sheetId);
            const startIndex = dimension === "ROW" ? paneDivisions.ySplit : paneDivisions.xSplit;
            const endIndex = this.getters.getNumberHeaders(sheetId, dimension);
            if (!startIndex) {
                return false;
            }
            const indicesToCheck = range(startIndex, endIndex);
            return includesAll(elements, indicesToCheck);
        }
        // ---------------------------------------------------------------------------
        // Row/Col manipulation
        // ---------------------------------------------------------------------------
        /**
         * Check if a zone only contains empty cells
         */
        isEmpty(sheetId, zone) {
            return positions(zone)
                .map(({ col, row }) => this.getCell({ sheetId, col, row }))
                .every((cell) => !cell || cell.content === "");
        }
        getCommandZones(cmd) {
            const zones = [];
            if ("zone" in cmd) {
                zones.push(cmd.zone);
            }
            if ("target" in cmd) {
                zones.push(...cmd.target);
            }
            if ("ranges" in cmd) {
                zones.push(...cmd.ranges.map((rangeData) => this.getters.getRangeFromRangeData(rangeData).zone));
            }
            if ("col" in cmd && "row" in cmd) {
                zones.push({ top: cmd.row, left: cmd.col, bottom: cmd.row, right: cmd.col });
            }
            return zones;
        }
        /**
         * Check if zones in the command are well formed and
         * not outside the sheet.
         */
        checkZonesExistInSheet(sheetId, zones) {
            if (!zones.every(isZoneValid))
                return "InvalidRange" /* CommandResult.InvalidRange */;
            if (zones.length) {
                const sheetZone = this.getSheetZone(sheetId);
                return zones.every((zone) => isZoneInside(zone, sheetZone))
                    ? "Success" /* CommandResult.Success */
                    : "TargetOutOfSheet" /* CommandResult.TargetOutOfSheet */;
            }
            return "Success" /* CommandResult.Success */;
        }
        updateCellPosition(cmd) {
            const { sheetId, cellId, col, row } = cmd;
            if (cellId) {
                this.setNewPosition(cellId, sheetId, col, row);
            }
            else {
                this.clearPosition(sheetId, col, row);
            }
        }
        /**
         * Set the cell at a new position and clear its previous position.
         */
        setNewPosition(cellId, sheetId, col, row) {
            const currentPosition = this.cellPosition[cellId];
            if (currentPosition) {
                this.clearPosition(sheetId, currentPosition.col, currentPosition.row);
            }
            this.history.update("cellPosition", cellId, {
                row: row,
                col: col,
                sheetId: sheetId,
            });
            this.history.update("sheets", sheetId, "rows", row, "cells", col, cellId);
        }
        /**
         * Remove the cell at the given position (if there's one)
         */
        clearPosition(sheetId, col, row) {
            const cellId = this.sheets[sheetId]?.rows[row].cells[col];
            if (cellId) {
                this.history.update("cellPosition", cellId, undefined);
                this.history.update("sheets", sheetId, "rows", row, "cells", col, undefined);
            }
        }
        setGridLinesVisibility(sheetId, areGridLinesVisible) {
            this.history.update("sheets", sheetId, "areGridLinesVisible", areGridLinesVisible);
        }
        clearZones(sheetId, zones) {
            for (let zone of zones) {
                for (let col = zone.left; col <= zone.right; col++) {
                    for (let row = zone.top; row <= zone.bottom; row++) {
                        const cell = this.sheets[sheetId].rows[row].cells[col];
                        if (cell) {
                            this.dispatch("UPDATE_CELL", {
                                sheetId: sheetId,
                                content: "",
                                col,
                                row,
                            });
                        }
                    }
                }
            }
        }
        createSheet(id, name, colNumber, rowNumber, position) {
            const sheet = {
                id,
                name,
                numberOfCols: colNumber,
                rows: createDefaultRows(rowNumber),
                areGridLinesVisible: true,
                isVisible: true,
                panes: {
                    xSplit: 0,
                    ySplit: 0,
                },
            };
            const orderedSheetIds = this.orderedSheetIds.slice();
            orderedSheetIds.splice(position, 0, sheet.id);
            const sheets = this.sheets;
            this.history.update("orderedSheetIds", orderedSheetIds);
            this.history.update("sheets", Object.assign({}, sheets, { [sheet.id]: sheet }));
            return sheet;
        }
        moveSheet(sheetId, delta) {
            const orderedSheetIds = this.orderedSheetIds.slice();
            const currentIndex = orderedSheetIds.findIndex((id) => id === sheetId);
            const sheet = orderedSheetIds.splice(currentIndex, 1);
            let index = this.findIndexOfTargetSheet(currentIndex, delta);
            orderedSheetIds.splice(index, 0, sheet[0]);
            this.history.update("orderedSheetIds", orderedSheetIds);
        }
        findIndexOfTargetSheet(currentIndex, deltaIndex) {
            while (deltaIndex != 0 && 0 <= currentIndex && currentIndex <= this.orderedSheetIds.length) {
                if (deltaIndex > 0) {
                    currentIndex++;
                    if (this.isSheetVisible(this.orderedSheetIds[currentIndex])) {
                        deltaIndex--;
                    }
                }
                else if (deltaIndex < 0) {
                    currentIndex--;
                    if (this.isSheetVisible(this.orderedSheetIds[currentIndex])) {
                        deltaIndex++;
                    }
                }
            }
            if (deltaIndex === 0) {
                return currentIndex;
            }
            throw new Error(_t("There is not enough visible sheets"));
        }
        checkSheetName(cmd) {
            const originalSheetName = this.getters.tryGetSheetName(cmd.sheetId);
            if (originalSheetName !== undefined && cmd.name === originalSheetName) {
                return "UnchangedSheetName" /* CommandResult.UnchangedSheetName */;
            }
            const { orderedSheetIds, sheets } = this;
            const name = cmd.name && cmd.name.trim().toLowerCase();
            if (orderedSheetIds.find((id) => sheets[id]?.name.toLowerCase() === name && id !== cmd.sheetId)) {
                return "DuplicatedSheetName" /* CommandResult.DuplicatedSheetName */;
            }
            if (FORBIDDEN_IN_EXCEL_REGEX.test(name)) {
                return "ForbiddenCharactersInSheetName" /* CommandResult.ForbiddenCharactersInSheetName */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkSheetPosition(cmd) {
            const { orderedSheetIds } = this;
            if (cmd.position > orderedSheetIds.length || cmd.position < 0) {
                return "WrongSheetPosition" /* CommandResult.WrongSheetPosition */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkRowFreezeQuantity(cmd) {
            return cmd.quantity >= 1 && cmd.quantity < this.getNumberRows(cmd.sheetId)
                ? "Success" /* CommandResult.Success */
                : "InvalidFreezeQuantity" /* CommandResult.InvalidFreezeQuantity */;
        }
        checkColFreezeQuantity(cmd) {
            return cmd.quantity >= 1 && cmd.quantity < this.getNumberCols(cmd.sheetId)
                ? "Success" /* CommandResult.Success */
                : "InvalidFreezeQuantity" /* CommandResult.InvalidFreezeQuantity */;
        }
        checkRowFreezeOverlapMerge(cmd) {
            const merges = this.getters.getMerges(cmd.sheetId);
            for (let merge of merges) {
                if (merge.top < cmd.quantity && cmd.quantity <= merge.bottom) {
                    return "MergeOverlap" /* CommandResult.MergeOverlap */;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        checkColFreezeOverlapMerge(cmd) {
            const merges = this.getters.getMerges(cmd.sheetId);
            for (let merge of merges) {
                if (merge.left < cmd.quantity && cmd.quantity <= merge.right) {
                    return "MergeOverlap" /* CommandResult.MergeOverlap */;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        isRenameAllowed(cmd) {
            const name = cmd.name && cmd.name.trim().toLowerCase();
            if (!name) {
                return "MissingSheetName" /* CommandResult.MissingSheetName */;
            }
            return this.checkSheetName(cmd);
        }
        renameSheet(sheet, name) {
            const oldName = sheet.name;
            this.history.update("sheets", sheet.id, "name", name.trim());
            const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);
            delete sheetIdsMapName[oldName];
            sheetIdsMapName[name] = sheet.id;
            this.history.update("sheetIdsMapName", sheetIdsMapName);
        }
        hideSheet(sheetId) {
            this.history.update("sheets", sheetId, "isVisible", false);
        }
        showSheet(sheetId) {
            this.history.update("sheets", sheetId, "isVisible", true);
        }
        duplicateSheet(fromId, toId) {
            const sheet = this.getSheet(fromId);
            const toName = this.getDuplicateSheetName(sheet.name);
            const newSheet = deepCopy(sheet);
            newSheet.id = toId;
            newSheet.name = toName;
            for (let col = 0; col <= newSheet.numberOfCols; col++) {
                for (let row = 0; row <= newSheet.rows.length; row++) {
                    if (newSheet.rows[row]) {
                        newSheet.rows[row].cells[col] = undefined;
                    }
                }
            }
            const orderedSheetIds = this.orderedSheetIds.slice();
            const currentIndex = orderedSheetIds.indexOf(fromId);
            orderedSheetIds.splice(currentIndex + 1, 0, newSheet.id);
            this.history.update("orderedSheetIds", orderedSheetIds);
            this.history.update("sheets", Object.assign({}, this.sheets, { [newSheet.id]: newSheet }));
            for (const cell of Object.values(this.getters.getCells(fromId))) {
                const { col, row } = this.getCellPosition(cell.id);
                this.dispatch("UPDATE_CELL", {
                    sheetId: newSheet.id,
                    col,
                    row,
                    content: cell.content,
                    format: cell.format,
                    style: cell.style,
                });
            }
            const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);
            sheetIdsMapName[newSheet.name] = newSheet.id;
            this.history.update("sheetIdsMapName", sheetIdsMapName);
        }
        getDuplicateSheetName(sheetName) {
            let i = 1;
            const names = this.orderedSheetIds.map(this.getSheetName.bind(this));
            const baseName = _t("Copy of %s", sheetName);
            let name = baseName.toString();
            while (names.includes(name)) {
                name = `${baseName} (${i})`;
                i++;
            }
            return name;
        }
        deleteSheet(sheet) {
            const name = sheet.name;
            const sheets = Object.assign({}, this.sheets);
            delete sheets[sheet.id];
            this.history.update("sheets", sheets);
            const orderedSheetIds = this.orderedSheetIds.slice();
            const currentIndex = orderedSheetIds.indexOf(sheet.id);
            orderedSheetIds.splice(currentIndex, 1);
            this.history.update("orderedSheetIds", orderedSheetIds);
            const sheetIdsMapName = Object.assign({}, this.sheetIdsMapName);
            delete sheetIdsMapName[name];
            this.history.update("sheetIdsMapName", sheetIdsMapName);
        }
        /**
         * Delete column. This requires a lot of handling:
         * - Update all the formulas in all sheets
         * - Move the cells
         * - Update the cols/rows (size, number, (cells), ...)
         * - Reevaluate the cells
         *
         * @param sheet ID of the sheet on which deletion should be applied
         * @param columns Columns to delete
         */
        removeColumns(sheet, columns) {
            // This is necessary because we have to delete elements in correct order:
            // begin with the end.
            columns.sort((a, b) => b - a);
            for (let column of columns) {
                // Move the cells.
                this.moveCellOnColumnsDeletion(sheet, column);
            }
            const numberOfCols = this.sheets[sheet.id].numberOfCols;
            this.history.update("sheets", sheet.id, "numberOfCols", numberOfCols - columns.length);
            const count = columns.filter((col) => col < sheet.panes.xSplit).length;
            if (count) {
                this.setPaneDivisions(sheet.id, sheet.panes.xSplit - count, "COL");
            }
        }
        /**
         * Delete row. This requires a lot of handling:
         * - Update the merges
         * - Update all the formulas in all sheets
         * - Move the cells
         * - Update the cols/rows (size, number, (cells), ...)
         * - Reevaluate the cells
         *
         * @param sheet ID of the sheet on which deletion should be applied
         * @param rows Rows to delete
         */
        removeRows(sheet, rows) {
            // This is necessary because we have to delete elements in correct order:
            // begin with the end.
            rows.sort((a, b) => b - a);
            for (let group of groupConsecutive(rows)) {
                // indexes are sorted in the descending order
                const from = group[group.length - 1];
                const to = group[0];
                // Move the cells.
                this.moveCellOnRowsDeletion(sheet, from, to);
                // Effectively delete the rows
                this.updateRowsStructureOnDeletion(sheet, from, to);
            }
            const count = rows.filter((row) => row < sheet.panes.ySplit).length;
            if (count) {
                this.setPaneDivisions(sheet.id, sheet.panes.ySplit - count, "ROW");
            }
        }
        addColumns(sheet, column, position, quantity) {
            const index = position === "before" ? column : column + 1;
            // Move the cells.
            this.moveCellsOnAddition(sheet, index, quantity, "columns");
            const numberOfCols = this.sheets[sheet.id].numberOfCols;
            this.history.update("sheets", sheet.id, "numberOfCols", numberOfCols + quantity);
            if (index < sheet.panes.xSplit) {
                this.setPaneDivisions(sheet.id, sheet.panes.xSplit + quantity, "COL");
            }
        }
        addRows(sheet, row, position, quantity) {
            const index = position === "before" ? row : row + 1;
            this.addEmptyRows(sheet, quantity);
            // Move the cells.
            this.moveCellsOnAddition(sheet, index, quantity, "rows");
            if (index < sheet.panes.ySplit) {
                this.setPaneDivisions(sheet.id, sheet.panes.ySplit + quantity, "ROW");
            }
        }
        moveCellOnColumnsDeletion(sheet, deletedColumn) {
            for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {
                const row = sheet.rows[rowIndex];
                for (let i in row.cells) {
                    const colIndex = Number(i);
                    const cellId = row.cells[i];
                    if (cellId) {
                        if (colIndex === deletedColumn) {
                            this.dispatch("CLEAR_CELL", {
                                sheetId: sheet.id,
                                col: colIndex,
                                row: rowIndex,
                            });
                        }
                        if (colIndex > deletedColumn) {
                            this.dispatch("UPDATE_CELL_POSITION", {
                                sheetId: sheet.id,
                                cellId: cellId,
                                col: colIndex - 1,
                                row: rowIndex,
                            });
                        }
                    }
                }
            }
        }
        /**
         * Move the cells after a column or rows insertion
         */
        moveCellsOnAddition(sheet, addedElement, quantity, dimension) {
            const commands = [];
            for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {
                const row = sheet.rows[rowIndex];
                if (dimension !== "rows" || rowIndex >= addedElement) {
                    for (let i in row.cells) {
                        const colIndex = Number(i);
                        const cellId = row.cells[i];
                        if (cellId) {
                            if (dimension === "rows" || colIndex >= addedElement) {
                                commands.push({
                                    type: "UPDATE_CELL_POSITION",
                                    sheetId: sheet.id,
                                    cellId: cellId,
                                    col: colIndex + (dimension === "columns" ? quantity : 0),
                                    row: rowIndex + (dimension === "rows" ? quantity : 0),
                                });
                            }
                        }
                    }
                }
            }
            for (let cmd of commands.reverse()) {
                this.dispatch(cmd.type, cmd);
            }
        }
        /**
         * Move all the cells that are from the row under `deleteToRow` up to `deleteFromRow`
         *
         * b.e.
         * move vertically with delete from 3 and delete to 5 will first clear all the cells from lines 3 to 5,
         * then take all the row starting at index 6 and add them back at index 3
         *
         */
        moveCellOnRowsDeletion(sheet, deleteFromRow, deleteToRow) {
            const numberRows = deleteToRow - deleteFromRow + 1;
            for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {
                const row = sheet.rows[rowIndex];
                if (rowIndex >= deleteFromRow && rowIndex <= deleteToRow) {
                    for (let i in row.cells) {
                        const colIndex = Number(i);
                        const cellId = row.cells[i];
                        if (cellId) {
                            this.dispatch("CLEAR_CELL", {
                                sheetId: sheet.id,
                                col: colIndex,
                                row: rowIndex,
                            });
                        }
                    }
                }
                if (rowIndex > deleteToRow) {
                    for (let i in row.cells) {
                        const colIndex = Number(i);
                        const cellId = row.cells[i];
                        if (cellId) {
                            this.dispatch("UPDATE_CELL_POSITION", {
                                sheetId: sheet.id,
                                cellId: cellId,
                                col: colIndex,
                                row: rowIndex - numberRows,
                            });
                        }
                    }
                }
            }
        }
        updateRowsStructureOnDeletion(sheet, deleteFromRow, deleteToRow) {
            const rows = [];
            const cellsQueue = sheet.rows.map((row) => row.cells).reverse();
            for (let i in sheet.rows) {
                const row = Number(i);
                if (row >= deleteFromRow && row <= deleteToRow) {
                    continue;
                }
                rows.push({
                    cells: cellsQueue.pop(),
                });
            }
            this.history.update("sheets", sheet.id, "rows", rows);
        }
        /**
         * Add empty rows at the end of the rows
         *
         * @param sheet Sheet
         * @param quantity Number of rows to add
         */
        addEmptyRows(sheet, quantity) {
            const rows = sheet.rows.slice();
            for (let i = 0; i < quantity; i++) {
                rows.push({
                    cells: {},
                });
            }
            this.history.update("sheets", sheet.id, "rows", rows);
        }
        getImportedSheetSize(data) {
            const positions = Object.keys(data.cells).map(toCartesian);
            let rowNumber = data.rowNumber;
            let colNumber = data.colNumber;
            for (let { col, row } of positions) {
                rowNumber = Math.max(rowNumber, row + 1);
                colNumber = Math.max(colNumber, col + 1);
            }
            return { rowNumber, colNumber };
        }
        /**
         * Check that any "sheetId" in the command matches an existing
         * sheet.
         */
        checkSheetExists(cmd) {
            if (cmd.type !== "CREATE_SHEET" && "sheetId" in cmd && this.sheets[cmd.sheetId] === undefined) {
                return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
            }
            else if (cmd.type === "CREATE_SHEET" && this.sheets[cmd.sheetId] !== undefined) {
                return "DuplicatedSheetId" /* CommandResult.DuplicatedSheetId */;
            }
            return "Success" /* CommandResult.Success */;
        }
        /**
         * Check if zones in the command are well formed and
         * not outside the sheet.
         */
        checkZonesAreInSheet(cmd) {
            if (!("sheetId" in cmd))
                return "Success" /* CommandResult.Success */;
            return this.checkZonesExistInSheet(cmd.sheetId, this.getCommandZones(cmd));
        }
    }

    class HeaderGroupingPlugin extends CorePlugin {
        static getters = [
            "getHeaderGroups",
            "getGroupsLayers",
            "getVisibleGroupLayers",
            "getHeaderGroup",
            "getHeaderGroupsInZone",
            "isGroupFolded",
            "isRowFolded",
            "isColFolded",
        ];
        groups = {};
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "GROUP_HEADERS": {
                    const { start, end } = cmd;
                    if (!this.getters.doesHeadersExist(cmd.sheetId, cmd.dimension, [start, end])) {
                        return "InvalidHeaderGroupStartEnd" /* CommandResult.InvalidHeaderGroupStartEnd */;
                    }
                    if (start > end) {
                        return "InvalidHeaderGroupStartEnd" /* CommandResult.InvalidHeaderGroupStartEnd */;
                    }
                    if (this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, start, end)) {
                        return "HeaderGroupAlreadyExists" /* CommandResult.HeaderGroupAlreadyExists */;
                    }
                    break;
                }
                case "UNGROUP_HEADERS": {
                    const { start, end } = cmd;
                    if (!this.getters.doesHeadersExist(cmd.sheetId, cmd.dimension, [start, end])) {
                        return "InvalidHeaderGroupStartEnd" /* CommandResult.InvalidHeaderGroupStartEnd */;
                    }
                    if (start > end) {
                        return "InvalidHeaderGroupStartEnd" /* CommandResult.InvalidHeaderGroupStartEnd */;
                    }
                    break;
                }
                case "UNFOLD_HEADER_GROUP":
                case "FOLD_HEADER_GROUP":
                    const group = this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);
                    if (!group) {
                        return "UnknownHeaderGroup" /* CommandResult.UnknownHeaderGroup */;
                    }
                    const numberOfHeaders = this.getters.getNumberHeaders(cmd.sheetId, cmd.dimension);
                    const willHideAllHeaders = range(0, numberOfHeaders).every((i) => (i >= group.start && i <= group.end) ||
                        this.getters.isHeaderHiddenByUser(cmd.sheetId, cmd.dimension, i));
                    if (willHideAllHeaders) {
                        return "NotEnoughElements" /* CommandResult.NotEnoughElements */;
                    }
                    break;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CREATE_SHEET":
                    this.history.update("groups", cmd.sheetId, { ROW: [], COL: [] });
                    break;
                case "GROUP_HEADERS":
                    this.groupHeaders(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);
                    break;
                case "UNGROUP_HEADERS": {
                    this.unGroupHeaders(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);
                    break;
                }
                case "DUPLICATE_SHEET": {
                    const groups = deepCopy(this.groups[cmd.sheetId]);
                    this.history.update("groups", cmd.sheetIdTo, groups);
                    break;
                }
                case "DELETE_SHEET": {
                    const groups = { ...this.groups };
                    delete groups[cmd.sheetId];
                    this.history.update("groups", groups);
                    break;
                }
                case "ADD_COLUMNS_ROWS":
                    const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
                    this.moveGroupsOnHeaderInsertion(cmd.sheetId, cmd.dimension, addIndex, cmd.quantity);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                    this.moveGroupsOnHeaderDeletion(cmd.sheetId, cmd.dimension, cmd.elements);
                    break;
                case "UNFOLD_HEADER_GROUP": {
                    const group = this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);
                    if (group) {
                        this.unfoldHeaderGroup(cmd.sheetId, cmd.dimension, group);
                    }
                    break;
                }
                case "FOLD_HEADER_GROUP": {
                    const group = this.findGroupWithStartEnd(cmd.sheetId, cmd.dimension, cmd.start, cmd.end);
                    if (group) {
                        this.foldHeaderGroup(cmd.sheetId, cmd.dimension, group);
                    }
                    break;
                }
                case "UNFOLD_ALL_HEADER_GROUPS": {
                    const groups = this.getters.getHeaderGroups(cmd.sheetId, cmd.dimension);
                    for (const group of groups) {
                        this.unfoldHeaderGroup(cmd.sheetId, cmd.dimension, group);
                    }
                    break;
                }
                case "FOLD_ALL_HEADER_GROUPS": {
                    const groups = this.getters.getHeaderGroups(cmd.sheetId, cmd.dimension);
                    for (const group of groups) {
                        this.foldHeaderGroup(cmd.sheetId, cmd.dimension, group);
                    }
                    break;
                }
                case "FOLD_HEADER_GROUPS_IN_ZONE":
                case "UNFOLD_HEADER_GROUPS_IN_ZONE": {
                    const action = cmd.type === "UNFOLD_HEADER_GROUPS_IN_ZONE" ? "unfold" : "fold";
                    const layers = this.getGroupsLayers(cmd.sheetId, cmd.dimension);
                    if (action === "fold") {
                        layers.reverse();
                    }
                    const groups = layers.flat();
                    const start = cmd.dimension === "ROW" ? cmd.zone.top : cmd.zone.left;
                    const end = cmd.dimension === "ROW" ? cmd.zone.bottom : cmd.zone.right;
                    const groupsToToggle = new Set();
                    for (let header = start; header <= end; header++) {
                        const matchedGroups = groups.filter((g) => g.start - 1 <= header && header <= g.end); // -1 to include the group header
                        for (const group of matchedGroups) {
                            if ((action === "fold" && group.isFolded) || (action === "unfold" && !group.isFolded)) {
                                continue;
                            }
                            groupsToToggle.add(group);
                            break;
                        }
                    }
                    for (const group of groupsToToggle) {
                        if (action === "unfold") {
                            this.unfoldHeaderGroup(cmd.sheetId, cmd.dimension, group);
                        }
                        else {
                            this.foldHeaderGroup(cmd.sheetId, cmd.dimension, group);
                        }
                    }
                    break;
                }
            }
        }
        getHeaderGroups(sheetId, dim) {
            return this.groups[sheetId][dim];
        }
        getHeaderGroup(sheetId, dim, start, end) {
            return this.getHeaderGroups(sheetId, dim).find((group) => group.start === start && group.end === end);
        }
        getHeaderGroupsInZone(sheetId, dim, zone) {
            return this.getHeaderGroups(sheetId, dim).filter((group) => {
                const start = dim === "ROW" ? zone.top : zone.left;
                const end = dim === "ROW" ? zone.bottom : zone.right;
                return this.doGroupOverlap(group, start, end);
            });
        }
        /**
         * Get all the groups of a sheet in a dimension, and return an array of layers of those groups.
         *
         * The layering rules are:
         * 1) A group containing another group should be on a layer above the group it contains
         * 2) The widest/highest groups should be on the left/top layer compared to the groups it contains
         * 3) The group should be on the left/top-most layer possible, barring intersections with other groups (see rules 1 and 2)
         */
        getGroupsLayers(sheetId, dimension) {
            const groups = this.getHeaderGroups(sheetId, dimension);
            return this.bricksFallingAlgorithm(groups, 0, 0);
        }
        /**
         * Get all the groups of a sheet in a dimension, and return an array of layers of those groups,
         * excluding the groups that are totally hidden.
         */
        getVisibleGroupLayers(sheetId, dimension) {
            const layers = this.getGroupsLayers(sheetId, dimension);
            for (const layer of layers) {
                for (let k = layer.length - 1; k >= 0; k--) {
                    const group = layer[k];
                    if (group.start === 0) {
                        continue;
                    }
                    const headersInGroup = range(group.start - 1, group.end + 1);
                    if (headersInGroup.every((i) => this.getters.isHeaderHiddenByUser(sheetId, dimension, i))) {
                        layer.splice(k, 1);
                    }
                }
            }
            return layers.filter((layer) => layer.length > 0);
        }
        isGroupFolded(sheetId, dimension, start, end) {
            return this.getHeaderGroup(sheetId, dimension, start, end)?.isFolded || false;
        }
        isRowFolded(sheetId, row) {
            const groups = this.getters.getHeaderGroups(sheetId, "ROW");
            return groups.some((group) => group.start <= row && row <= group.end && group.isFolded);
        }
        isColFolded(sheetId, col) {
            const groups = this.getters.getHeaderGroups(sheetId, "COL");
            // might become a performance issue if there are a lot of groups (this is called by isColHidden).
            return groups.some((group) => group.start <= col && col <= group.end && group.isFolded);
        }
        getGroupId(group) {
            return `${group.start}-${group.end}}`;
        }
        /**
         * To get layers of groups, and to add/remove headers from groups, we can see each header of a group as a brick. Each
         * brick falls down in the pile corresponding to its header, until it hits another brick, or the ground.
         *
         * With this abstraction, we can very simply group/ungroup headers from groups, and get the layers of groups.
         * - grouping headers is done by adding a brick to each header pile
         * - un-grouping headers is done by removing a brick from each header pile
         * - getting the layers of groups is done by simply letting the brick fall and checking the result
         *
         * Example:
         * We have 2 groups ([A=>E] and [C=>D]), and we want to group headers [C=>F]
         *
         * Headers :                 A B C D E F G          A B C D E F G            A B C D E F G
         * Headers to group: [C=>D]:     _ _         [C=>F]:    _ _ _ _
         *                               | |           ==>      | | | |       ==>                    ==> Result: 3 groups
         *                               | |                    ˅ ˅ | |                  _ _                - [C=>D]
         * Groups:                       ˅ ˅                    _ _ ˅ |                  _ _ _              - [C=>E]
         * Groups:                   _ _ _ _ _              _ _ _ _ _ ˅              _ _ _ _ _ _            - [A=>F]
      
         * @param groups
         * @param start start of the range where to add/remove headers
         * @param end end of the range where to add/remove headers
         * @param delta -1: remove headers, 1: add headers, 0: get layers (don't add/remove anything)
         */
        bricksFallingAlgorithm(groups, start, end, delta = 0) {
            const isGroupFolded = {};
            for (const group of groups) {
                isGroupFolded[this.getGroupId(group)] = group.isFolded;
            }
            /** Number of bricks in each header pile */
            const brickPileSize = {};
            for (const group of groups) {
                for (let i = group.start; i <= group.end; i++) {
                    brickPileSize[i] = brickPileSize[i] ? brickPileSize[i] + 1 : 1;
                }
            }
            for (let i = start; i <= end; i++) {
                brickPileSize[i] = brickPileSize[i] ? brickPileSize[i] + delta : delta;
            }
            const numberOfLayers = Math.max(...Object.values(brickPileSize), 0);
            const groupLayers = Array.from({ length: numberOfLayers }, () => []);
            const maxHeader = Math.max(end, ...groups.map((group) => group.end));
            const minHeader = Math.min(start, ...groups.map((group) => group.start));
            for (let header = minHeader; header <= maxHeader; header++) {
                const pileSize = brickPileSize[header] || 0;
                for (let layer = 0; layer < pileSize; layer++) {
                    const currentGroup = groupLayers[layer].at(-1);
                    if (currentGroup && isConsecutive([currentGroup.end, header])) {
                        currentGroup.end++;
                    }
                    else {
                        const newGroup = { start: header, end: header };
                        groupLayers[layer].push(newGroup);
                    }
                }
            }
            for (const layer of groupLayers) {
                for (const group of layer) {
                    group.isFolded = isGroupFolded[this.getGroupId(group)];
                }
            }
            return groupLayers;
        }
        groupHeaders(sheetId, dimension, start, end) {
            const groups = this.getHeaderGroups(sheetId, dimension);
            const newGroups = this.bricksFallingAlgorithm(groups, start, end, +1).flat();
            this.history.update("groups", sheetId, dimension, this.removeDuplicateGroups(newGroups));
        }
        /**
         * Ungroup the given headers. The headers will be taken out of the group they are in. This might split a group into two
         * if the headers were in the middle of a group. If multiple groups contains a header, it will only be taken out of the
         * lowest group in the layering of the groups.
         */
        unGroupHeaders(sheetId, dimension, start, end) {
            const groups = this.getHeaderGroups(sheetId, dimension);
            const newGroups = this.bricksFallingAlgorithm(groups, start, end, -1).flat();
            this.history.update("groups", sheetId, dimension, this.removeDuplicateGroups(newGroups));
        }
        moveGroupsOnHeaderInsertion(sheetId, dim, index, count) {
            const groups = this.groups[sheetId][dim];
            for (let i = 0; i < groups.length; i++) {
                const group = groups[i];
                const [start, end] = moveHeaderIndexesOnHeaderAddition(index, count, [
                    group.start,
                    group.end,
                ]);
                if (start !== group.start || end !== group.end) {
                    this.history.update("groups", sheetId, dim, i, { ...group, start, end });
                }
            }
        }
        moveGroupsOnHeaderDeletion(sheetId, dimension, deletedElements) {
            const groups = this.getHeaderGroups(sheetId, dimension);
            const newGroups = [];
            for (const group of groups) {
                const headersInGroup = range(group.start, group.end + 1);
                const headersAfterDeletion = moveHeaderIndexesOnHeaderDeletion(deletedElements, headersInGroup);
                if (headersAfterDeletion.length === 0) {
                    continue;
                }
                newGroups.push({
                    ...group,
                    start: Math.min(...headersAfterDeletion),
                    end: Math.max(...headersAfterDeletion),
                });
            }
            this.history.update("groups", sheetId, dimension, this.bricksFallingAlgorithm(newGroups, 0, 0).flat());
        }
        doGroupOverlap(group, start, end) {
            return group.start <= end && group.end >= start;
        }
        removeDuplicateGroups(groups) {
            const newGroups = {};
            for (const group of groups) {
                newGroups[this.getGroupId(group)] = group;
            }
            return Object.values(newGroups);
        }
        findGroupWithStartEnd(sheetId, dimension, start, end) {
            return this.getHeaderGroups(sheetId, dimension).find((group) => group.start === start && group.end === end);
        }
        /**
         * Fold the given group, and all the groups starting at the same index that are contained inside the given group.
         */
        foldHeaderGroup(sheetId, dim, groupToFold) {
            const index = this.getGroupIndex(sheetId, dim, groupToFold.start, groupToFold.end);
            if (index === undefined) {
                return;
            }
            this.history.update("groups", sheetId, dim, index, "isFolded", true);
            const groups = this.getters.getHeaderGroups(sheetId, dim);
            for (let i = 0; i < groups.length; i++) {
                const group = groups[i];
                if (group.start === groupToFold.start && group.end <= groupToFold.end) {
                    this.history.update("groups", sheetId, dim, i, "isFolded", true);
                }
            }
        }
        /**
         * Unfold the given group, and all the groups starting at the same index that contain the given group.
         */
        unfoldHeaderGroup(sheetId, dim, groupToUnfold) {
            const index = this.getGroupIndex(sheetId, dim, groupToUnfold.start, groupToUnfold.end);
            if (index === undefined) {
                return;
            }
            this.history.update("groups", sheetId, dim, index, "isFolded", false);
            const groups = this.getters.getHeaderGroups(sheetId, dim);
            for (let i = 0; i < groups.length; i++) {
                const group = groups[i];
                if (group.start === groupToUnfold.start && group.end >= groupToUnfold.end) {
                    this.history.update("groups", sheetId, dim, i, "isFolded", false);
                }
            }
        }
        getGroupIndex(sheetId, dimension, start, end) {
            const index = this.groups[sheetId][dimension].findIndex((group) => group.start === start && group.end === end);
            return index === -1 ? undefined : index;
        }
        import(data) {
            for (const sheet of data.sheets) {
                this.groups[sheet.id] = { ROW: [], COL: [] };
                if (!sheet.headerGroups) {
                    continue;
                }
                for (const dim of ["ROW", "COL"]) {
                    for (const groupData of sheet.headerGroups[dim] || []) {
                        this.groups[sheet.id][dim].push({ ...groupData });
                    }
                }
            }
        }
        export(data) {
            for (const sheet of data.sheets) {
                sheet.headerGroups = this.groups[sheet.id];
            }
        }
    }

    class SettingsPlugin extends CorePlugin {
        static getters = ["getLocale"];
        locale = DEFAULT_LOCALE;
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "UPDATE_LOCALE":
                    return isValidLocale(cmd.locale) ? "Success" /* CommandResult.Success */ : "InvalidLocale" /* CommandResult.InvalidLocale */;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "UPDATE_LOCALE":
                    const oldLocale = this.locale;
                    const newLocale = cmd.locale;
                    this.history.update("locale", newLocale);
                    this.changeCellsDateFormatWithLocale(oldLocale, newLocale);
                    break;
            }
        }
        getLocale() {
            return this.locale;
        }
        changeCellsDateFormatWithLocale(oldLocale, newLocale) {
            for (const sheetId of this.getters.getSheetIds()) {
                for (const [cellId, cell] of Object.entries(this.getters.getCells(sheetId))) {
                    let formatToApply;
                    if (cell.format === oldLocale.dateFormat) {
                        formatToApply = newLocale.dateFormat;
                    }
                    if (cell.format === oldLocale.timeFormat) {
                        formatToApply = newLocale.timeFormat;
                    }
                    if (cell.format === getDateTimeFormat(oldLocale)) {
                        formatToApply = getDateTimeFormat(newLocale);
                    }
                    if (formatToApply) {
                        const { col, row, sheetId } = this.getters.getCellPosition(cellId);
                        this.dispatch("UPDATE_CELL", {
                            col,
                            row,
                            sheetId,
                            format: formatToApply,
                        });
                    }
                }
            }
        }
        import(data) {
            this.locale = data.settings?.locale ?? DEFAULT_LOCALE;
        }
        export(data) {
            data.settings = {
                locale: this.locale,
            };
        }
    }

    /**
     * UI plugins handle any transient data required to display a spreadsheet.
     * They can draw on the grid canvas.
     */
    class UIPlugin extends BasePlugin {
        static layers = [];
        getters;
        ui;
        selection;
        constructor({ getters, stateObserver, dispatch, uiActions, selection }) {
            super(stateObserver, dispatch);
            this.getters = getters;
            this.ui = uiActions;
            this.selection = selection;
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(ctx, layer) { }
    }

    const functionMap = functionRegistry.mapping;
    /**
     * Return all functions necessary to properly evaluate a formula:
     * - a refFn function to read any reference, cell or range of a normalized formula
     * - a range function to convert any reference to a proper value array
     * - an evaluation context
     */
    function buildCompilationParameters(context, getters, computeCell) {
        const builder = new CompilationParametersBuilder(context, getters, computeCell);
        return builder.getParameters();
    }
    class CompilationParametersBuilder {
        getters;
        computeCell;
        evalContext;
        rangeCache = {};
        constructor(context, getters, computeCell) {
            this.getters = getters;
            this.computeCell = computeCell;
            this.evalContext = Object.assign(Object.create(functionMap), context, {
                getters: this.getters,
                locale: this.getters.getLocale(),
            });
        }
        getParameters() {
            return [this.refFn.bind(this), this.range.bind(this), this.evalContext];
        }
        /**
         * Returns the value of the cell(s) used in reference
         *
         * @param range the references used
         * @param isMeta if a reference is supposed to be used in a `meta` parameter as described in the
         *        function for which this parameter is used, we just return the string of the parameter.
         *        The `compute` of the formula's function must process it completely
         */
        refFn(range, isMeta, functionName, paramNumber) {
            this.assertRangeValid(range);
            if (isMeta) {
                // Use zoneToXc of zone instead of getRangeString to avoid sending unbounded ranges
                return { value: zoneToXc(range.zone) };
            }
            // if the formula definition could have accepted a range, we would pass through the _range function and not here
            if (range.zone.bottom !== range.zone.top || range.zone.left !== range.zone.right) {
                throw new Error(paramNumber
                    ? _t("Function %s expects the parameter %s to be a single value or a single cell reference, not a range.", functionName.toString(), paramNumber.toString())
                    : _t("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
            }
            const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
            return this.readCell(position);
        }
        readCell(position) {
            if (!this.getters.tryGetSheet(position.sheetId)) {
                throw new Error(_t("Invalid sheet name"));
            }
            const evaluatedCell = this.getEvaluatedCellIfNotEmpty(position);
            if (evaluatedCell === undefined) {
                return { value: null, format: this.getters.getCell(position)?.format };
            }
            if (evaluatedCell.type === CellValueType.error) {
                throw evaluatedCell.error;
            }
            return evaluatedCell;
        }
        getEvaluatedCellIfNotEmpty(position) {
            const evaluatedCell = this.computeCell(position);
            if (evaluatedCell.type === CellValueType.empty) {
                const cell = this.getters.getCell(position);
                if (!cell || (!cell.isFormula && cell.content === "")) {
                    return undefined;
                }
            }
            return evaluatedCell;
        }
        /**
         * Return the values of the cell(s) used in reference, but always in the format of a range even
         * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
         * range<number> etc.
         *
         * Note that each col is possibly sparse: it only contain the values of cells
         * that are actually present in the grid.
         */
        range(range) {
            this.assertRangeValid(range);
            const sheetId = range.sheetId;
            const zone = range.zone;
            // Performance issue: Avoid fetching data on positions that are out of the spreadsheet
            // e.g. A1:ZZZ9999 in a sheet with 10 cols and 10 rows should ignore everything past J10 and return a 10x10 array
            const sheetZone = this.getters.getSheetZone(sheetId);
            const _zone = intersection(zone, sheetZone);
            if (!_zone) {
                return [[]];
            }
            const { top, left, bottom, right } = zone;
            const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;
            if (cacheKey in this.rangeCache) {
                const result = this.rangeCache[cacheKey];
                if (result instanceof EvaluationError) {
                    throw result;
                }
                return result;
            }
            const height = _zone.bottom - _zone.top + 1;
            const width = _zone.right - _zone.left + 1;
            const matrix = new Array(width);
            // Performance issue: nested loop is faster than a map here
            for (let col = _zone.left; col <= _zone.right; col++) {
                const colIndex = col - _zone.left;
                matrix[colIndex] = new Array(height);
                for (let row = _zone.top; row <= _zone.bottom; row++) {
                    const evaluatedCell = this.getEvaluatedCellIfNotEmpty({ sheetId, col, row });
                    if (evaluatedCell?.type === CellValueType.error) {
                        this.rangeCache[cacheKey] = evaluatedCell.error;
                        throw evaluatedCell.error;
                    }
                    const rowIndex = row - _zone.top;
                    matrix[colIndex][rowIndex] = this.readCell({ sheetId, col, row });
                }
            }
            this.rangeCache[cacheKey] = matrix;
            return matrix;
        }
        assertRangeValid(range) {
            if (!isZoneValid(range.zone)) {
                throw new InvalidReferenceError();
            }
            if (range.invalidSheetName) {
                throw new Error(_t("Invalid sheet name: %s", range.invalidSheetName));
            }
        }
    }

    function quickselect(arr, k, left, right, compare) {
        quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
    }

    function quickselectStep(arr, k, left, right, compare) {

        while (right > left) {
            if (right - left > 600) {
                var n = right - left + 1;
                var m = k - left + 1;
                var z = Math.log(n);
                var s = 0.5 * Math.exp(2 * z / 3);
                var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
                var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
                var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
                quickselectStep(arr, k, newLeft, newRight, compare);
            }

            var t = arr[k];
            var i = left;
            var j = right;

            swap(arr, left, k);
            if (compare(arr[right], t) > 0) swap(arr, left, right);

            while (i < j) {
                swap(arr, i, j);
                i++;
                j--;
                while (compare(arr[i], t) < 0) i++;
                while (compare(arr[j], t) > 0) j--;
            }

            if (compare(arr[left], t) === 0) swap(arr, left, j);
            else {
                j++;
                swap(arr, j, right);
            }

            if (j <= k) left = j + 1;
            if (k <= j) right = j - 1;
        }
    }

    function swap(arr, i, j) {
        var tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    function defaultCompare(a, b) {
        return a < b ? -1 : a > b ? 1 : 0;
    }

    class RBush {
        constructor(maxEntries = 9) {
            // max entries in a node is 9 by default; min node fill is 40% for best performance
            this._maxEntries = Math.max(4, maxEntries);
            this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
            this.clear();
        }

        all() {
            return this._all(this.data, []);
        }

        search(bbox) {
            let node = this.data;
            const result = [];

            if (!intersects(bbox, node)) return result;

            const toBBox = this.toBBox;
            const nodesToSearch = [];

            while (node) {
                for (let i = 0; i < node.children.length; i++) {
                    const child = node.children[i];
                    const childBBox = node.leaf ? toBBox(child) : child;

                    if (intersects(bbox, childBBox)) {
                        if (node.leaf) result.push(child);
                        else if (contains(bbox, childBBox)) this._all(child, result);
                        else nodesToSearch.push(child);
                    }
                }
                node = nodesToSearch.pop();
            }

            return result;
        }

        collides(bbox) {
            let node = this.data;

            if (!intersects(bbox, node)) return false;

            const nodesToSearch = [];
            while (node) {
                for (let i = 0; i < node.children.length; i++) {
                    const child = node.children[i];
                    const childBBox = node.leaf ? this.toBBox(child) : child;

                    if (intersects(bbox, childBBox)) {
                        if (node.leaf || contains(bbox, childBBox)) return true;
                        nodesToSearch.push(child);
                    }
                }
                node = nodesToSearch.pop();
            }

            return false;
        }

        load(data) {
            if (!(data && data.length)) return this;

            if (data.length < this._minEntries) {
                for (let i = 0; i < data.length; i++) {
                    this.insert(data[i]);
                }
                return this;
            }

            // recursively build the tree with the given data from scratch using OMT algorithm
            let node = this._build(data.slice(), 0, data.length - 1, 0);

            if (!this.data.children.length) {
                // save as is if tree is empty
                this.data = node;

            } else if (this.data.height === node.height) {
                // split root if trees have the same height
                this._splitRoot(this.data, node);

            } else {
                if (this.data.height < node.height) {
                    // swap trees if inserted one is bigger
                    const tmpNode = this.data;
                    this.data = node;
                    node = tmpNode;
                }

                // insert the small tree into the large tree at appropriate level
                this._insert(node, this.data.height - node.height - 1, true);
            }

            return this;
        }

        insert(item) {
            if (item) this._insert(item, this.data.height - 1);
            return this;
        }

        clear() {
            this.data = createNode([]);
            return this;
        }

        remove(item, equalsFn) {
            if (!item) return this;

            let node = this.data;
            const bbox = this.toBBox(item);
            const path = [];
            const indexes = [];
            let i, parent, goingUp;

            // depth-first iterative tree traversal
            while (node || path.length) {

                if (!node) { // go up
                    node = path.pop();
                    parent = path[path.length - 1];
                    i = indexes.pop();
                    goingUp = true;
                }

                if (node.leaf) { // check current node
                    const index = findItem(item, node.children, equalsFn);

                    if (index !== -1) {
                        // item found, remove the item and condense tree upwards
                        node.children.splice(index, 1);
                        path.push(node);
                        this._condense(path);
                        return this;
                    }
                }

                if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
                    path.push(node);
                    indexes.push(i);
                    i = 0;
                    parent = node;
                    node = node.children[0];

                } else if (parent) { // go right
                    i++;
                    node = parent.children[i];
                    goingUp = false;

                } else node = null; // nothing found
            }

            return this;
        }

        toBBox(item) { return item; }

        compareMinX(a, b) { return a.minX - b.minX; }
        compareMinY(a, b) { return a.minY - b.minY; }

        toJSON() { return this.data; }

        fromJSON(data) {
            this.data = data;
            return this;
        }

        _all(node, result) {
            const nodesToSearch = [];
            while (node) {
                if (node.leaf) result.push(...node.children);
                else nodesToSearch.push(...node.children);

                node = nodesToSearch.pop();
            }
            return result;
        }

        _build(items, left, right, height) {

            const N = right - left + 1;
            let M = this._maxEntries;
            let node;

            if (N <= M) {
                // reached leaf level; return leaf
                node = createNode(items.slice(left, right + 1));
                calcBBox(node, this.toBBox);
                return node;
            }

            if (!height) {
                // target height of the bulk-loaded tree
                height = Math.ceil(Math.log(N) / Math.log(M));

                // target number of root entries to maximize storage utilization
                M = Math.ceil(N / Math.pow(M, height - 1));
            }

            node = createNode([]);
            node.leaf = false;
            node.height = height;

            // split the items into M mostly square tiles

            const N2 = Math.ceil(N / M);
            const N1 = N2 * Math.ceil(Math.sqrt(M));

            multiSelect(items, left, right, N1, this.compareMinX);

            for (let i = left; i <= right; i += N1) {

                const right2 = Math.min(i + N1 - 1, right);

                multiSelect(items, i, right2, N2, this.compareMinY);

                for (let j = i; j <= right2; j += N2) {

                    const right3 = Math.min(j + N2 - 1, right2);

                    // pack each entry recursively
                    node.children.push(this._build(items, j, right3, height - 1));
                }
            }

            calcBBox(node, this.toBBox);

            return node;
        }

        _chooseSubtree(bbox, node, level, path) {
            while (true) {
                path.push(node);

                if (node.leaf || path.length - 1 === level) break;

                let minArea = Infinity;
                let minEnlargement = Infinity;
                let targetNode;

                for (let i = 0; i < node.children.length; i++) {
                    const child = node.children[i];
                    const area = bboxArea(child);
                    const enlargement = enlargedArea(bbox, child) - area;

                    // choose entry with the least area enlargement
                    if (enlargement < minEnlargement) {
                        minEnlargement = enlargement;
                        minArea = area < minArea ? area : minArea;
                        targetNode = child;

                    } else if (enlargement === minEnlargement) {
                        // otherwise choose one with the smallest area
                        if (area < minArea) {
                            minArea = area;
                            targetNode = child;
                        }
                    }
                }

                node = targetNode || node.children[0];
            }

            return node;
        }

        _insert(item, level, isNode) {
            const bbox = isNode ? item : this.toBBox(item);
            const insertPath = [];

            // find the best node for accommodating the item, saving all nodes along the path too
            const node = this._chooseSubtree(bbox, this.data, level, insertPath);

            // put the item into the node
            node.children.push(item);
            extend(node, bbox);

            // split on node overflow; propagate upwards if necessary
            while (level >= 0) {
                if (insertPath[level].children.length > this._maxEntries) {
                    this._split(insertPath, level);
                    level--;
                } else break;
            }

            // adjust bboxes along the insertion path
            this._adjustParentBBoxes(bbox, insertPath, level);
        }

        // split overflowed node into two
        _split(insertPath, level) {
            const node = insertPath[level];
            const M = node.children.length;
            const m = this._minEntries;

            this._chooseSplitAxis(node, m, M);

            const splitIndex = this._chooseSplitIndex(node, m, M);

            const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
            newNode.height = node.height;
            newNode.leaf = node.leaf;

            calcBBox(node, this.toBBox);
            calcBBox(newNode, this.toBBox);

            if (level) insertPath[level - 1].children.push(newNode);
            else this._splitRoot(node, newNode);
        }

        _splitRoot(node, newNode) {
            // split root node
            this.data = createNode([node, newNode]);
            this.data.height = node.height + 1;
            this.data.leaf = false;
            calcBBox(this.data, this.toBBox);
        }

        _chooseSplitIndex(node, m, M) {
            let index;
            let minOverlap = Infinity;
            let minArea = Infinity;

            for (let i = m; i <= M - m; i++) {
                const bbox1 = distBBox(node, 0, i, this.toBBox);
                const bbox2 = distBBox(node, i, M, this.toBBox);

                const overlap = intersectionArea(bbox1, bbox2);
                const area = bboxArea(bbox1) + bboxArea(bbox2);

                // choose distribution with minimum overlap
                if (overlap < minOverlap) {
                    minOverlap = overlap;
                    index = i;

                    minArea = area < minArea ? area : minArea;

                } else if (overlap === minOverlap) {
                    // otherwise choose distribution with minimum area
                    if (area < minArea) {
                        minArea = area;
                        index = i;
                    }
                }
            }

            return index || M - m;
        }

        // sorts node children by the best axis for split
        _chooseSplitAxis(node, m, M) {
            const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
            const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
            const xMargin = this._allDistMargin(node, m, M, compareMinX);
            const yMargin = this._allDistMargin(node, m, M, compareMinY);

            // if total distributions margin value is minimal for x, sort by minX,
            // otherwise it's already sorted by minY
            if (xMargin < yMargin) node.children.sort(compareMinX);
        }

        // total margin of all possible split distributions where each node is at least m full
        _allDistMargin(node, m, M, compare) {
            node.children.sort(compare);

            const toBBox = this.toBBox;
            const leftBBox = distBBox(node, 0, m, toBBox);
            const rightBBox = distBBox(node, M - m, M, toBBox);
            let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);

            for (let i = m; i < M - m; i++) {
                const child = node.children[i];
                extend(leftBBox, node.leaf ? toBBox(child) : child);
                margin += bboxMargin(leftBBox);
            }

            for (let i = M - m - 1; i >= m; i--) {
                const child = node.children[i];
                extend(rightBBox, node.leaf ? toBBox(child) : child);
                margin += bboxMargin(rightBBox);
            }

            return margin;
        }

        _adjustParentBBoxes(bbox, path, level) {
            // adjust bboxes along the given tree path
            for (let i = level; i >= 0; i--) {
                extend(path[i], bbox);
            }
        }

        _condense(path) {
            // go through the path, removing empty nodes and updating bboxes
            for (let i = path.length - 1, siblings; i >= 0; i--) {
                if (path[i].children.length === 0) {
                    if (i > 0) {
                        siblings = path[i - 1].children;
                        siblings.splice(siblings.indexOf(path[i]), 1);

                    } else this.clear();

                } else calcBBox(path[i], this.toBBox);
            }
        }
    }

    function findItem(item, items, equalsFn) {
        if (!equalsFn) return items.indexOf(item);

        for (let i = 0; i < items.length; i++) {
            if (equalsFn(item, items[i])) return i;
        }
        return -1;
    }

    // calculate node's bbox from bboxes of its children
    function calcBBox(node, toBBox) {
        distBBox(node, 0, node.children.length, toBBox, node);
    }

    // min bounding rectangle of node children from k to p-1
    function distBBox(node, k, p, toBBox, destNode) {
        if (!destNode) destNode = createNode(null);
        destNode.minX = Infinity;
        destNode.minY = Infinity;
        destNode.maxX = -Infinity;
        destNode.maxY = -Infinity;

        for (let i = k; i < p; i++) {
            const child = node.children[i];
            extend(destNode, node.leaf ? toBBox(child) : child);
        }

        return destNode;
    }

    function extend(a, b) {
        a.minX = Math.min(a.minX, b.minX);
        a.minY = Math.min(a.minY, b.minY);
        a.maxX = Math.max(a.maxX, b.maxX);
        a.maxY = Math.max(a.maxY, b.maxY);
        return a;
    }

    function compareNodeMinX(a, b) { return a.minX - b.minX; }
    function compareNodeMinY(a, b) { return a.minY - b.minY; }

    function bboxArea(a)   { return (a.maxX - a.minX) * (a.maxY - a.minY); }
    function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }

    function enlargedArea(a, b) {
        return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
               (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
    }

    function intersectionArea(a, b) {
        const minX = Math.max(a.minX, b.minX);
        const minY = Math.max(a.minY, b.minY);
        const maxX = Math.min(a.maxX, b.maxX);
        const maxY = Math.min(a.maxY, b.maxY);

        return Math.max(0, maxX - minX) *
               Math.max(0, maxY - minY);
    }

    function contains(a, b) {
        return a.minX <= b.minX &&
               a.minY <= b.minY &&
               b.maxX <= a.maxX &&
               b.maxY <= a.maxY;
    }

    function intersects(a, b) {
        return b.minX <= a.maxX &&
               b.minY <= a.maxY &&
               b.maxX >= a.minX &&
               b.maxY >= a.minY;
    }

    function createNode(children) {
        return {
            children,
            height: 1,
            leaf: true,
            minX: Infinity,
            minY: Infinity,
            maxX: -Infinity,
            maxY: -Infinity
        };
    }

    // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
    // combines selection algorithm with binary divide & conquer approach

    function multiSelect(arr, left, right, n, compare) {
        const stack = [left, right];

        while (stack.length) {
            right = stack.pop();
            left = stack.pop();

            if (right - left <= n) continue;

            const mid = left + Math.ceil((right - left) / n / 2) * n;
            quickselect(arr, mid, left, right, compare);

            stack.push(left, mid, mid, right);
        }
    }

    /**
     * R-Tree Data Structure
     *
     * R-Tree is a spatial data structure used for efficient indexing and querying
     * of multi-dimensional objects, particularly in geometric and spatial applications.
     *
     * It organizes objects into a tree hierarchy, grouping nearby objects together
     * in bounding boxes. Each node in the tree represents a bounding box that
     * contains its child nodes or leaf objects. This hierarchical structure allows
     * for faster spatial queries.
     *
     * @see https://en.wikipedia.org/wiki/R-tree
     *
     * Consider a 2D Space with four zones: A, B, C, D
     * +--------------------------+
     * |                          |
     * |   +---+     +-------+    |
     * |   | A |     |   B   |    |
     * |   +---+     +-------+    |
     * |                          |
     * |                          |
     * |          +---+           |
     * |          | C |           |
     * |          +---+           |
     * |      +-----------+       |
     * |      |     D     |       |
     * |      +-----------+       |
     * |                          |
     * +--------------------------+
     *
     * It groups together zones that are spatially close into a minimum bounding box.
     * For example, A and B are grouped together in rectangle R1, and C and D are grouped
     * in R2.
     *
     * R0
     * +--------------------------+
     * |   R1                     |
     * |   +-----------------+    |
     * |   | A |     |   B   |    |
     * |   +-----------------+    |
     * |                          |
     * |      R2                  |
     * |      +---+---+---+       |
     * |      |   | C |   |       |
     * |      |   +---+   |       |
     * |      +-----------+       |
     * |      |     D     |       |
     * |      +-----------+       |
     * |                          |
     * +--------------------------+
     *
     * The tree would look like this:
     *          R0
     *         /  \
     *        /    \
     *       R1     R2
     *       |      |
     *      A,B    C,D

     * Choosing how to group the zones is crucial for the performance of the tree.
     * Key considerations include avoiding excessive empty space coverage and minimizing overlap
     * to reduce the number of subtrees processed during searches.
     *
     * Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
     * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
     * the choice cannot be made based on this criterion due to the same enlargement for different groupings,
     * we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
     *
     * This implementation is tailored for spreadsheet use, indexing objects associated
     * with a zone and a sheet.
     *
     * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
     * @see https://github.com/mourner/rbush
     */
    class SpreadsheetRTree {
        /**
         * One 2D R-tree per sheet
         */
        rTrees = {};
        /**
         * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
         * faster than inserting items one by one. After bulk loading (bulk insertion into
         * an empty tree), subsequent query performance is also ~20-30% better.
         */
        constructor(items = []) {
            const rangesPerSheet = {};
            for (const item of items) {
                const sheetId = item.boundingBox.sheetId;
                if (!rangesPerSheet[sheetId]) {
                    rangesPerSheet[sheetId] = [];
                }
                rangesPerSheet[sheetId].push(item);
            }
            for (const sheetId in rangesPerSheet) {
                this.rTrees[sheetId] = new ZoneRBush();
                this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
            }
        }
        insert(item) {
            const sheetId = item.boundingBox.sheetId;
            if (!this.rTrees[sheetId]) {
                this.rTrees[sheetId] = new ZoneRBush();
            }
            this.rTrees[sheetId].insert(item);
        }
        search({ zone, sheetId }) {
            if (!this.rTrees[sheetId]) {
                return [];
            }
            return this.rTrees[sheetId].search({
                minX: zone.left,
                minY: zone.top,
                maxX: zone.right,
                maxY: zone.bottom,
            });
        }
        remove(item) {
            const sheetId = item.boundingBox.sheetId;
            if (!this.rTrees[sheetId]) {
                return;
            }
            this.rTrees[sheetId].remove(item, this.rtreeItemComparer);
        }
        rtreeItemComparer(left, right) {
            return (left.data == right.data &&
                left.boundingBox.sheetId === right.boundingBox.sheetId &&
                left.boundingBox?.zone.left === right.boundingBox.zone.left &&
                left.boundingBox?.zone.top === right.boundingBox.zone.top &&
                left.boundingBox?.zone.right === right.boundingBox.zone.right &&
                left.boundingBox?.zone.bottom === right.boundingBox.zone.bottom);
        }
    }
    /**
     * RBush extension to use zones as bounding boxes
     */
    class ZoneRBush extends RBush {
        toBBox({ boundingBox }) {
            const zone = boundingBox.zone;
            return {
                minX: zone.left,
                minY: zone.top,
                maxX: zone.right,
                maxY: zone.bottom,
            };
        }
        compareMinX(a, b) {
            return a.boundingBox.zone.left - b.boundingBox.zone.left;
        }
        compareMinY(a, b) {
            return a.boundingBox.zone.top - b.boundingBox.zone.top;
        }
    }

    /**
     * Implementation of a dependency Graph.
     * The graph is used to evaluate the cells in the correct
     * order, and should be updated each time a cell's content is modified
     *
     * It uses an R-Tree data structure to efficiently find dependent cells.
     */
    class FormulaDependencyGraph {
        encoder;
        dependencies = new Map();
        rTree;
        constructor(encoder, data = []) {
            this.encoder = encoder;
            this.rTree = new SpreadsheetRTree(data);
        }
        removeAllDependencies(formulaPositionId) {
            const ranges = this.dependencies.get(formulaPositionId);
            if (!ranges) {
                return;
            }
            for (const range of ranges) {
                this.rTree.remove(range);
            }
            this.dependencies.delete(formulaPositionId);
        }
        addDependencies(formulaPositionId, dependencies) {
            const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
                data: formulaPositionId,
                boundingBox: {
                    zone,
                    sheetId,
                },
            }));
            for (const item of rTreeItems) {
                this.rTree.insert(item);
            }
            const existingDependencies = this.dependencies.get(formulaPositionId);
            if (existingDependencies) {
                existingDependencies.push(...rTreeItems);
            }
            else {
                this.dependencies.set(formulaPositionId, rTreeItems);
            }
        }
        /**
         * Return the cell and all cells that depend on it,
         * in the correct order they should be evaluated.
         * This is called a topological ordering (excluding cycles)
         */
        getCellsDependingOn(ranges) {
            const visited = new JetSet();
            const queue = Array.from(ranges).reverse();
            while (queue.length > 0) {
                const range = queue.pop();
                visited.addMany(this.encoder.encodeBoundingBox(range));
                const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
                for (const positionId of impactedPositionIds) {
                    if (!visited.has(positionId)) {
                        queue.push(this.encoder.decodeToBoundingBox(positionId));
                    }
                }
            }
            visited.deleteMany(ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
            return visited;
        }
    }

    /**
     * Contains, for each cell, the array
     * formulas that could potentially spread on it
     * This is essentially a two way mapping between array formulas
     * and their results.
     *
     * As we don't allow two array formulas to spread on the same cell, this structure
     * is used to force the reevaluation of the potential spreaders of a cell when the
     * content of this cell is modified. This structure should be updated each time
     * an array formula is evaluated and try to spread on another cell.
     *
     */
    class SpreadingRelation {
        /**
         * Internal structure:
         * For something like
         * - A2:'=SPLIT("KAYAK", "A")'
         * - B1:'=TRANSPOSE(SPLIT("COYOTE", "O"))'
         *
         * Resulting in:
         * ```
         * -----------------
         * |   | A | B | C |
         * |---+---+---+---|
         * | 1 |   | C |   |
         * | 2 | K | Y | K |
         * | 3 |   | T |   |
         * | 4 |   | E |   |
         * -----------------
         * ```
         * We will have `resultsToArrayFormulas` looking like:
         * - (B2) --> (A2, B1)  meaning B2 can be the result of A2 OR B1
         * - (C2) --> (A2)      meaning C2 is the result of A2
         * - (B3) --> (B1)      meaning B3 is the result of B1
         * - (B4) --> (B1)      meaning B4 is the result of B1
         *
         * We will have `arrayFormulasToResults` looking like:
         * - (A2) --> (B2, C2)      meaning A2 spreads on B2 and C2
         * - (B1) --> (B2, B3, B4)  meaning B1 spreads on B2, B3 and B4
         *
         */
        resultsToArrayFormulas = new Map();
        arrayFormulasToResults = new Map();
        getFormulaPositionsSpreadingOn(resultPositionId) {
            return this.resultsToArrayFormulas.get(resultPositionId) || EMPTY_ARRAY;
        }
        getArrayResultPositionIds(formulasPositionId) {
            return this.arrayFormulasToResults.get(formulasPositionId) || EMPTY_ARRAY;
        }
        /**
         * Remove a node, also remove it from other nodes adjacency list
         */
        removeNode(positionId) {
            this.resultsToArrayFormulas.delete(positionId);
            this.arrayFormulasToResults.delete(positionId);
        }
        /**
         * Create a spreading relation between two cells
         */
        addRelation({ arrayFormulaPositionId, resultPositionId, }) {
            if (!this.resultsToArrayFormulas.has(resultPositionId)) {
                this.resultsToArrayFormulas.set(resultPositionId, new Set());
            }
            this.resultsToArrayFormulas.get(resultPositionId)?.add(arrayFormulaPositionId);
            if (!this.arrayFormulasToResults.has(arrayFormulaPositionId)) {
                this.arrayFormulasToResults.set(arrayFormulaPositionId, new Set());
            }
            this.arrayFormulasToResults.get(arrayFormulaPositionId)?.add(resultPositionId);
        }
        hasArrayFormulaResult(positionId) {
            return this.resultsToArrayFormulas.has(positionId);
        }
        isArrayFormula(positionId) {
            return this.arrayFormulasToResults.has(positionId);
        }
    }
    const EMPTY_ARRAY = [];

    const MAX_ITERATION = 30;
    class Evaluator {
        context;
        getters;
        compilationParams;
        encoder = new PositionBitsEncoder();
        evaluatedCells = new Map();
        formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
        blockedArrayFormulas = new Set();
        spreadingRelations = new SpreadingRelation();
        constructor(context, getters) {
            this.context = context;
            this.getters = getters;
            this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
        }
        getEvaluatedCell(position) {
            return (this.evaluatedCells.get(this.encoder.encode(position)) ||
                createEvaluatedCell("", { locale: this.getters.getLocale() }));
        }
        getSpreadPositionsOf(position) {
            const positionId = this.encoder.encode(position);
            if (!this.spreadingRelations.isArrayFormula(positionId)) {
                return [];
            }
            return Array.from(this.spreadingRelations.getArrayResultPositionIds(positionId)).map((positionId) => this.encoder.decode(positionId));
        }
        getArrayFormulaSpreadingOn(position) {
            const positionId = this.encoder.encode(position);
            const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
            return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
        }
        getEvaluatedPositions() {
            return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
        }
        getArrayFormulaSpreadingOnId(positionId) {
            if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
                return undefined;
            }
            const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
            return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
        }
        updateDependencies(position) {
            const positionId = this.encoder.encode(position);
            this.formulaDependencies().removeAllDependencies(positionId);
            const dependencies = this.getDirectDependencies(positionId);
            this.formulaDependencies().addDependencies(positionId, dependencies);
        }
        updateCompilationParameters() {
            // rebuild the compilation parameters (with a clean cache)
            this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
        }
        evaluateCells(positions) {
            const cells = positions.map((p) => this.encoder.encode(p));
            const cellsToCompute = new JetSet(cells);
            const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
            cellsToCompute.addMany(this.getCellsDependingOn(cells));
            cellsToCompute.addMany(arrayFormulasPositionIds);
            cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositionIds));
            this.evaluate(cellsToCompute);
        }
        getArrayFormulasImpactedByChangesOf(positionIds) {
            const impactedPositionIds = new JetSet();
            for (const positionId of positionIds) {
                const content = this.getCell(positionId)?.content;
                const arrayFormulaPositionId = this.getArrayFormulaSpreadingOnId(positionId);
                if (arrayFormulaPositionId !== undefined) {
                    // take into account new collisions.
                    impactedPositionIds.add(arrayFormulaPositionId);
                }
                if (!content) {
                    // The previous content could have blocked some array formulas
                    impactedPositionIds.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(positionId));
                }
            }
            return impactedPositionIds;
        }
        buildDependencyGraph() {
            this.blockedArrayFormulas = new Set();
            this.spreadingRelations = new SpreadingRelation();
            this.formulaDependencies = lazy(() => {
                const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
                    data: positionId,
                    boundingBox: {
                        zone: range.zone,
                        sheetId: range.sheetId,
                    },
                })));
                return new FormulaDependencyGraph(this.encoder, dependencies);
            });
        }
        evaluateAllCells() {
            this.evaluatedCells = new Map();
            this.evaluate(this.getAllCells());
        }
        evaluateFormula(sheetId, formulaString) {
            const compiledFormula = compile(formulaString);
            const ranges = compiledFormula.dependencies.map((xc) => this.getters.getRangeFromSheetXC(sheetId, xc));
            this.updateCompilationParameters();
            const array = compiledFormula.execute(ranges, ...this.compilationParams);
            if (isMatrix(array)) {
                return matrixMap(array, (cell) => cell.value);
            }
            return array.value;
        }
        getAllCells() {
            const positionIds = new JetSet();
            for (const sheetId of this.getters.getSheetIds()) {
                const cellIds = this.getters.getCells(sheetId);
                for (const cellId in cellIds) {
                    positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
                }
            }
            return positionIds;
        }
        getArrayFormulasBlockedByOrSpreadingOn(positionId) {
            if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
                return [];
            }
            const arrayFormulas = this.spreadingRelations.getFormulaPositionsSpreadingOn(positionId);
            const cells = new JetSet(arrayFormulas);
            cells.addMany(this.getCellsDependingOn(arrayFormulas));
            return cells;
        }
        nextPositionsToUpdate = new JetSet();
        cellsBeingComputed = new Set();
        evaluate(cells) {
            this.cellsBeingComputed = new Set();
            this.nextPositionsToUpdate = cells;
            let currentIteration = 0;
            while (this.nextPositionsToUpdate.size && currentIteration++ < MAX_ITERATION) {
                this.updateCompilationParameters();
                const positionIds = Array.from(this.nextPositionsToUpdate);
                this.nextPositionsToUpdate.clear();
                for (let i = 0; i < positionIds.length; ++i) {
                    const cell = positionIds[i];
                    this.evaluatedCells.delete(cell);
                }
                for (let i = 0; i < positionIds.length; ++i) {
                    const cell = positionIds[i];
                    this.setEvaluatedCell(cell, this.computeCell(cell));
                }
            }
        }
        setEvaluatedCell(positionId, evaluatedCell) {
            this.evaluatedCells.set(positionId, evaluatedCell);
        }
        computeCell(positionId) {
            const evaluation = this.evaluatedCells.get(positionId);
            if (evaluation) {
                return evaluation; // already computed
            }
            if (!this.blockedArrayFormulas.has(positionId)) {
                this.invalidateSpreading(positionId);
            }
            const cell = this.getCell(positionId);
            if (cell === undefined) {
                return createEvaluatedCell("", { locale: this.getters.getLocale() });
            }
            const cellId = cell.id;
            try {
                if (this.cellsBeingComputed.has(cellId)) {
                    throw new CircularDependencyError();
                }
                this.cellsBeingComputed.add(cellId);
                return cell.isFormula
                    ? this.computeFormulaCell(cell)
                    : evaluateLiteral(cell.content, { format: cell.format, locale: this.getters.getLocale() });
            }
            catch (e) {
                return this.handleError(e, cell);
            }
            finally {
                this.cellsBeingComputed.delete(cellId);
            }
        }
        computeAndSave(position) {
            const positionId = this.encoder.encode(position);
            const evaluatedCell = this.computeCell(positionId);
            if (!this.evaluatedCells.has(positionId)) {
                this.setEvaluatedCell(positionId, evaluatedCell);
            }
            return evaluatedCell;
        }
        handleError(e, cell) {
            if (!(e instanceof EvaluationError)) {
                e = new EvaluationError(CellErrorType.GenericError, e.message);
            }
            const __lastFnCalled = this.compilationParams[2].__lastFnCalled || "";
            e.message = e.message.replace("[[FUNCTION_NAME]]", __lastFnCalled);
            return errorCell(e);
        }
        computeFormulaCell(cellData) {
            const cellId = cellData.id;
            this.compilationParams[2].__originCellXC = () => {
                // compute the value lazily for performance reasons
                const position = this.compilationParams[2].getters.getCellPosition(cellId);
                return toXC(position.col, position.row);
            };
            const formulaReturn = cellData.compiledFormula.execute(cellData.compiledFormula.dependencies, ...this.compilationParams);
            if (!isMatrix(formulaReturn)) {
                return createEvaluatedCell(formulaReturn.value, {
                    format: cellData.format || formulaReturn.format,
                    locale: this.getters.getLocale(),
                });
            }
            const formulaPosition = this.getters.getCellPosition(cellId);
            this.assertSheetHasEnoughSpaceToSpreadFormulaResult(formulaPosition, formulaReturn);
            const nbColumns = formulaReturn.length;
            const nbRows = formulaReturn[0].length;
            forEachSpreadPositionInMatrix(nbColumns, nbRows, this.updateSpreadRelation(formulaPosition));
            forEachSpreadPositionInMatrix(nbColumns, nbRows, this.checkCollision(formulaPosition));
            forEachSpreadPositionInMatrix(nbColumns, nbRows, 
            // thanks to the isMatrix check above, we know that formulaReturn is MatrixFunctionReturn
            this.spreadValues(formulaPosition, formulaReturn));
            return createEvaluatedCell(formulaReturn[0][0].value, {
                format: cellData.format || formulaReturn[0][0]?.format,
                locale: this.getters.getLocale(),
            });
        }
        assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
            const numberOfCols = this.getters.getNumberCols(sheetId);
            const numberOfRows = this.getters.getNumberRows(sheetId);
            const enoughCols = col + matrixResult.length <= numberOfCols;
            const enoughRows = row + matrixResult[0].length <= numberOfRows;
            if (enoughCols && enoughRows) {
                return;
            }
            if (enoughCols) {
                throw new Error(_t("Result couldn't be automatically expanded. Please insert more rows."));
            }
            if (enoughRows) {
                throw new Error(_t("Result couldn't be automatically expanded. Please insert more columns."));
            }
            throw new Error(_t("Result couldn't be automatically expanded. Please insert more columns and rows."));
        }
        updateSpreadRelation({ sheetId, col, row, }) {
            const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
            return (i, j) => {
                const position = { sheetId, col: i + col, row: j + row };
                const resultPositionId = this.encoder.encode(position);
                this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
            };
        }
        checkCollision({ sheetId, col, row }) {
            const formulaPositionId = this.encoder.encode({ sheetId, col, row });
            return (i, j) => {
                const position = { sheetId: sheetId, col: i + col, row: j + row };
                const rawCell = this.getters.getCell(position);
                if (rawCell?.content ||
                    this.getters.getEvaluatedCell(position).type !== CellValueType.empty) {
                    this.blockedArrayFormulas.add(formulaPositionId);
                    throw new Error(_t("Array result was not expanded because it would overwrite data in %s.", toXC(position.col, position.row)));
                }
                this.blockedArrayFormulas.delete(formulaPositionId);
            };
        }
        spreadValues({ sheetId, col, row }, matrixResult) {
            return (i, j) => {
                const position = { sheetId, col: i + col, row: j + row };
                const cell = this.getters.getCell(position);
                const format = cell?.format;
                const evaluatedCell = createEvaluatedCell(matrixResult[i][j].value, {
                    format: format || matrixResult[i][j]?.format,
                    locale: this.getters.getLocale(),
                });
                const positionId = this.encoder.encode(position);
                this.setEvaluatedCell(positionId, evaluatedCell);
                // check if formula dependencies present in the spread zone
                // if so, they need to be recomputed
                this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([positionId]));
            };
        }
        invalidateSpreading(positionId) {
            if (!this.spreadingRelations.isArrayFormula(positionId)) {
                return;
            }
            for (const child of this.spreadingRelations.getArrayResultPositionIds(positionId)) {
                const content = this.getCell(child)?.content;
                if (content) {
                    // there's no point at re-evaluating overlapping array formulas,
                    // there's still a collision
                    continue;
                }
                this.evaluatedCells.delete(child);
                this.nextPositionsToUpdate.addMany(this.getCellsDependingOn([child]));
                this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedByOrSpreadingOn(child));
            }
            this.spreadingRelations.removeNode(positionId);
        }
        // ----------------------------------------------------------
        //                 COMMON FUNCTIONALITY
        // ----------------------------------------------------------
        getDirectDependencies(positionId) {
            const cell = this.getCell(positionId);
            if (!cell?.isFormula) {
                return [];
            }
            return cell.compiledFormula.dependencies;
        }
        getCellsDependingOn(positionIds) {
            const ranges = [];
            for (const positionId of positionIds) {
                ranges.push(this.encoder.decodeToBoundingBox(positionId));
            }
            return this.formulaDependencies().getCellsDependingOn(ranges);
        }
        getCell(positionId) {
            return this.getters.getCell(this.encoder.decode(positionId));
        }
    }
    function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
        for (let i = 0; i < nbColumns; ++i) {
            for (let j = 0; j < nbRows; ++j) {
                if (i === 0 && j === 0) {
                    continue;
                }
                callback(i, j);
            }
        }
    }
    /**
     * Encode (and decode) cell positions { sheetId, col, row }
     * to a single integer.
     *
     * `col` and `row` values are encoded on 21 bits each (max 2^21 = 2_097_152),
     * An incremental integer id is assigned to each different sheet id, starting at 0.
     *
     * e.g.
     * Given { col: 10, row: 4, sheetId: "abcde" }
     * we have:
     *  - row "4" encoded on 21 bits:  000000000000000000100
     *  - col "10" encoded on 21 bits: 000000000000000001010
     *  - sheetId: let's say it's the 4th sheetId met, encoded to: 11
     *
     * The final encoded value is found by concatenating the 3 bit sequences:
     *
     * sheetId: 11
     * col:       000000000000000001010
     * row:                            000000000000000000100
     * =>       11000000000000000001010000000000000000000100
     *
     * this binary sequence is the integer 13194160504836
     */
    class PositionBitsEncoder {
        sheetMapping = {};
        inverseSheetMapping = new Map();
        constructor() {
            try {
                // @ts-ignore
                o_spreadsheet.__DEBUG__ = o_spreadsheet.__DEBUG__ || {};
                // @ts-ignore
                o_spreadsheet.__DEBUG__.decodePosition = this.decode.bind(this);
                // @ts-ignore
                o_spreadsheet.__DEBUG__.encodePosition = this.encode.bind(this);
            }
            catch (error) { }
        }
        /**
         * Encode a cell position to a single integer.
         */
        encode({ sheetId, col, row }) {
            return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
        }
        encodeBoundingBox({ sheetId, zone }) {
            const positions = [];
            forEachPositionsInZone(zone, (col, row) => {
                positions.push(this.encode({ sheetId, col, row }));
            });
            return positions;
        }
        decode(id) {
            // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
            const row = Number(id & 2097151n);
            const col = Number((id >> 21n) & 2097151n);
            const sheetId = this.decodeSheet(id >> 42n);
            return { sheetId, col, row };
        }
        decodeToBoundingBox(id) {
            const { sheetId, col, row } = this.decode(id);
            return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
        }
        encodeSheet(sheetId) {
            const sheetKey = this.sheetMapping[sheetId];
            if (sheetKey === undefined) {
                const newSheetKey = BigInt(Object.keys(this.sheetMapping).length);
                this.sheetMapping[sheetId] = newSheetKey;
                this.inverseSheetMapping.set(newSheetKey, sheetId);
                return newSheetKey;
            }
            return sheetKey;
        }
        decodeSheet(sheetKey) {
            const sheetId = this.inverseSheetMapping.get(sheetKey);
            if (sheetId === undefined) {
                throw new Error("Sheet id not found");
            }
            return sheetId;
        }
    }

    //#region
    // ---------------------------------------------------------------------------
    // INTRODUCTION
    // ---------------------------------------------------------------------------
    // The evaluation plugin is in charge of computing the values of the cells.
    // This is a fairly complex task for several reasons:
    // Reason n°1: Cells can contain formulas that must be interpreted to know
    // the final value of the cell. And these formulas can depend on other cells.
    // ex A1:"=SUM(B1:B2)" we have to evaluate B1:B2 first to be able to evaluate A1.
    // We say here that we have a 'formula dependency' between A1 and B1:B2.
    // Reason n°2: A cell can assign value to other cells that haven't content.
    // This concerns cells containing a formula that returns an array of values.
    // ex A1:"=SPLIT('Odoo','d')" Evaluating A1 must assign the value "O" to A1 and
    // "oo" to B1. We say here that we have a 'spread relation' between A1 and B1.
    // B1 have a spread value from A1.
    // Note that a cell can contain a formula which depends on other cells which
    // themselves can:
    // - contain formulas which depends on other cells (and so on).
    // - contain a spread value from other formulas which depends on other cells
    //   (and so on).
    // I - How to build the evaluation ?
    //    If we had only formulas dependencies to treat, the evaluation would be
    //    simple: the formulas dependencies are directly deduced from the content
    //    of the formulas. With the dependencies we are able to calculate which
    //    formula must be evaluated before another.
    //    Cycles
    //    ------
    //    We can also easily detect if the cells are included in reference cycles
    //    and return an error in this case. ex: A1:"=B1" B1:"=C1" C1:"=A1"
    //    The "#CYCLE" error must be returned for
    //    all three cells.
    //    But there's more! There are formulas referring to other cells but never
    //    use them. This is the case for example
    //    with the "IF" formula. ex:
    //    A1:"=IF(D1,A2,B1)"
    //    A2:"=A1"
    //    In this case it is obvious that we have a cyclic dependency. But in practice
    //    this will only exist if D1 is true.
    //    For this reason, we believe that the evaluation should be **partly recursive**:
    //    The function computing a formula cell starts by marking them as 'being evaluated'
    //    and then call itself on the dependencies of the concerned cell. This allows
    //    to evaluate the dependencies before the cell itself and to detect
    //    if the cell that is being evaluated isn't part of a cycle.
    // II - The spread relation anticipation problem
    //    The biggest difficulty to solve with the evaluation lies in the fact that
    //    we cannot anticipate the spread relations: cells impacted by the result array
    //    of a formula are only determined after the array formula has been
    //    evaluated. In the case where the impacted cells are used in other formulas,
    //    this will require to re-evaluation other formulas (and so on...). ex:
    //    A1:"=B2"
    //    A2:"=SPLIT('Odoo','d')"
    //    in the example above, A2 spreads on B2, but we will know it only after
    //    the evaluation of A2. To be able to evaluate A1 correctly, we must therefore
    //    reevaluate A1 after the evaluation of A2.
    //    We could evaluate which formula spreads first. Except that the array formulas
    //    can themselves depend on the spreads of other formulas. ex:
    //    A1:"=SPLIT(B3,'d')"
    //    A2:="odoo odoo"
    //    A3:"=SPLIT(A2,' ')"
    //    In the example above, A3 must be evaluated before A1 because A1 needs B3 which
    //    can be modified by A3.
    //    Therefore, there would be a spatial evaluation order to be respected between
    //    the array formulas. We could imagine that, when an array formula depends
    //    on a cell, then we evaluate the first formula that spreads located in the upper
    //    left corner of this cell.
    //    Although this possibility has been explored, it remains complicated to spatially
    //    predict which formula should be evaluated before another, especially when
    //    the array formulas are located in different sheets or when the array formulas
    //    depends on the spreads of each other. ex:
    //
    //    A1:"=ARRAY_FORMULA_ALPHA(B2)"
    //    A2:"=ARRAY_FORMULA_BETA(B1)"
    //    In the example above, ARRAY_FORMULA_ALPHA and ARRAY_FORMULA_BETA are some
    //    formulas that could spread depending on the value of B2 and B1. This could be a
    //    cyclic dependency that we cannot anticipate.
    //    And as with the "IF" formula, array formulas may not use their dependency.
    //    It then becomes very difficult to manage...
    //    Also, if we have a cycle, that doesn't mean it's bad. The cycle can converge to
    //    a stable state at the scale of the sheets. Functionally, we don't want to forbid
    //    convergent cycles. It is an interesting feature but which requires to re-evaluate
    //    the cycle as many times as convergence is not reached.
    // Thus, in order to respect the relations between the cells (formula dependencies and
    // spread relations), the evaluation of the cells must:
    // - respect a precise order (cells values used by another must be evaluated first) : As
    //   we cannot anticipate which dependencies are really used by the formulas, we must
    //   evaluate the cells in a recursive way;
    // - be done as many times as necessary to ensure that all the cells have been correctly
    //   evaluated in the correct order (in case of, for example, spreading relation cycles).
    // The chosen solution is to reevaluate the formulas impacted by spreads as many times
    // as necessary in several iterations, where evaluated cells can trigger the evaluation
    // of other cells depending on it, at the next iteration.
    //#endregion
    class EvaluationPlugin extends UIPlugin {
        static getters = [
            "evaluateFormula",
            "getCorrespondingFormulaCell",
            "getRangeFormattedValues",
            "getRangeValues",
            "getRangeFormats",
            "getEvaluatedCell",
            "getEvaluatedCells",
            "getEvaluatedCellsInZone",
            "getSpreadPositionsOf",
            "getArrayFormulaSpreadingOn",
        ];
        shouldRebuildDependenciesGraph = true;
        evaluator;
        positionsToUpdate = [];
        constructor(config) {
            super(config);
            this.evaluator = new Evaluator(config.custom, this.getters);
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        beforeHandle(cmd) {
            if (invalidateDependenciesCommands.has(cmd.type)) {
                this.shouldRebuildDependenciesGraph = true;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "UPDATE_CELL":
                    if (!("content" in cmd || "format" in cmd) || this.shouldRebuildDependenciesGraph) {
                        return;
                    }
                    this.positionsToUpdate.push(cmd);
                    if ("content" in cmd) {
                        this.evaluator.updateDependencies(cmd);
                    }
                    break;
                case "EVALUATE_CELLS":
                    this.evaluator.evaluateAllCells();
                    break;
            }
        }
        finalize() {
            if (this.shouldRebuildDependenciesGraph) {
                this.evaluator.buildDependencyGraph();
                this.evaluator.evaluateAllCells();
                this.shouldRebuildDependenciesGraph = false;
            }
            else if (this.positionsToUpdate.length) {
                this.evaluator.evaluateCells(this.positionsToUpdate);
            }
            this.positionsToUpdate = [];
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        evaluateFormula(sheetId, formulaString) {
            try {
                return this.evaluator.evaluateFormula(sheetId, formulaString);
            }
            catch (error) {
                return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
            }
        }
        /**
         * Return the value of each cell in the range as they are displayed in the grid.
         */
        getRangeFormattedValues(range) {
            const sheet = this.getters.tryGetSheet(range.sheetId);
            if (sheet === undefined)
                return [];
            return this.getters
                .getEvaluatedCellsInZone(sheet.id, range.zone)
                .map((cell) => cell.formattedValue);
        }
        /**
         * Return the value of each cell in the range.
         */
        getRangeValues(range) {
            const sheet = this.getters.tryGetSheet(range.sheetId);
            if (sheet === undefined)
                return [];
            return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.value);
        }
        /**
         * Return the format of each cell in the range.
         */
        getRangeFormats(range) {
            const sheet = this.getters.tryGetSheet(range.sheetId);
            if (sheet === undefined)
                return [];
            return this.getters.getEvaluatedCellsInZone(sheet.id, range.zone).map((cell) => cell.format);
        }
        getEvaluatedCell(position) {
            return this.evaluator.getEvaluatedCell(position);
        }
        getEvaluatedCells(sheetId) {
            const rawCells = this.getters.getCells(sheetId) || {};
            const record = {};
            for (let cellId of Object.keys(rawCells)) {
                const position = this.getters.getCellPosition(cellId);
                record[cellId] = this.getEvaluatedCell(position);
            }
            return record;
        }
        getEvaluatedCellsInZone(sheetId, zone) {
            return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
        }
        getSpreadPositionsOf(position) {
            return this.evaluator.getSpreadPositionsOf(position);
        }
        getArrayFormulaSpreadingOn(position) {
            return this.evaluator.getArrayFormulaSpreadingOn(position);
        }
        // ---------------------------------------------------------------------------
        // Export
        // ---------------------------------------------------------------------------
        exportForExcel(data) {
            for (const position of this.evaluator.getEvaluatedPositions()) {
                const evaluatedCell = this.evaluator.getEvaluatedCell(position);
                const xc = toXC(position.col, position.row);
                const value = evaluatedCell.value;
                let isFormula = false;
                let newContent = undefined;
                let newFormat = undefined;
                let isExported = true;
                const formulaCell = this.getCorrespondingFormulaCell(position);
                if (formulaCell) {
                    isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
                    isFormula = isExported;
                    if (!isExported) {
                        newContent = value.toString();
                        newFormat = evaluatedCell.format;
                    }
                }
                const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
                const exportedCellData = exportedSheetData.cells[xc] || {};
                const format = newFormat
                    ? getItemId(newFormat, data.formats)
                    : exportedCellData.format;
                let content;
                if (formulaCell instanceof FormulaCellWithDependencies) {
                    content = this.getters.getFormulaCellContent(exportedSheetData.id, formulaCell.compiledFormula, formulaCell.compiledFormula.dependencies, true);
                }
                else {
                    content = !isExported ? newContent : exportedCellData.content;
                }
                exportedSheetData.cells[xc] = { ...exportedCellData, value, isFormula, content, format };
            }
        }
        /**
         * Returns the corresponding formula cell of a given cell
         * It could be the formula present in the cell itself or the
         * formula of the array formula that spreads to the cell
         */
        getCorrespondingFormulaCell(position) {
            const cell = this.getters.getCell(position);
            if (cell && cell.isFormula) {
                return isBadExpression(cell.compiledFormula.tokens) ? undefined : cell;
            }
            else if (cell && cell.content) {
                return undefined;
            }
            const spreadingFormulaPosition = this.getArrayFormulaSpreadingOn(position);
            if (spreadingFormulaPosition === undefined) {
                return undefined;
            }
            const spreadingFormulaCell = this.getters.getCell(spreadingFormulaPosition);
            if (spreadingFormulaCell?.isFormula) {
                return spreadingFormulaCell;
            }
            return undefined;
        }
    }
    function isBadExpression(tokens) {
        try {
            compileTokens(tokens);
            return false;
        }
        catch (error) {
            return true;
        }
    }

    /**
     * https://tomekdev.com/posts/sorting-colors-in-js
     */
    function sortWithClusters(colorsToSort) {
        const clusters = [
            { leadColor: rgba(255, 0, 0), colors: [] },
            { leadColor: rgba(255, 128, 0), colors: [] },
            { leadColor: rgba(128, 128, 0), colors: [] },
            { leadColor: rgba(128, 255, 0), colors: [] },
            { leadColor: rgba(0, 255, 0), colors: [] },
            { leadColor: rgba(0, 255, 128), colors: [] },
            { leadColor: rgba(0, 255, 255), colors: [] },
            { leadColor: rgba(0, 127, 255), colors: [] },
            { leadColor: rgba(0, 0, 255), colors: [] },
            { leadColor: rgba(127, 0, 255), colors: [] },
            { leadColor: rgba(128, 0, 128), colors: [] },
            { leadColor: rgba(255, 0, 128), colors: [] }, // rose
        ];
        for (const color of colorsToSort.map(colorToRGBA)) {
            let currentDistance = 500; //max distance is 441;
            let currentIndex = 0;
            clusters.forEach((cluster, clusterIndex) => {
                const distance = colorDistance(color, cluster.leadColor);
                if (currentDistance > distance) {
                    currentDistance = distance;
                    currentIndex = clusterIndex;
                }
            });
            clusters[currentIndex].colors.push(color);
        }
        return clusters
            .map((cluster) => cluster.colors.sort((a, b) => rgbaToHSLA(a).s - rgbaToHSLA(b).s))
            .flat()
            .map(rgbaToHex);
    }
    function colorDistance(color1, color2) {
        return Math.sqrt(Math.pow(color1.r - color2.r, 2) +
            Math.pow(color1.g - color2.g, 2) +
            Math.pow(color1.b - color2.b, 2));
    }
    /**
     * CustomColors plugin
     * This plugins aims to compute and keep to custom colors used in the
     * current spreadsheet
     */
    class CustomColorsPlugin extends UIPlugin {
        customColors = {};
        shouldUpdateColors = false;
        static getters = ["getCustomColors"];
        handle(cmd) {
            switch (cmd.type) {
                case "UPDATE_CELL":
                case "UPDATE_CHART":
                case "CREATE_CHART":
                case "ADD_CONDITIONAL_FORMAT":
                case "SET_BORDER":
                case "SET_ZONE_BORDERS":
                case "SET_FORMATTING":
                    this.history.update("shouldUpdateColors", true);
            }
        }
        finalize() {
            if (this.shouldUpdateColors) {
                this.history.update("shouldUpdateColors", false);
                for (const color of this.getCustomColors()) {
                    this.tryToAddColor(color);
                }
            }
        }
        getCustomColors() {
            let usedColors = [];
            for (const sheetId of this.getters.getSheetIds()) {
                usedColors = usedColors.concat(this.getColorsFromCells(sheetId), this.getFormattingColors(sheetId), this.getChartColors(sheetId));
            }
            return sortWithClusters([
                ...new Set(
                // remove duplicates first to check validity on a reduced
                // set of colors, then normalize to HEX and remove duplicates
                // again
                [...new Set([...usedColors, ...Object.keys(this.customColors)])]
                    .filter(isColorValid)
                    .map(toHex)),
            ]).filter((color) => !COLOR_PICKER_DEFAULTS.includes(color));
        }
        getColorsFromCells(sheetId) {
            const cells = Object.values(this.getters.getCells(sheetId));
            const colors = new Set();
            for (const cell of cells) {
                if (cell.style?.textColor) {
                    colors.add(cell.style.textColor);
                }
                if (cell.style?.fillColor) {
                    colors.add(cell.style.fillColor);
                }
            }
            for (const color of this.getters.getBordersColors(sheetId)) {
                colors.add(color);
            }
            return [...colors];
        }
        getFormattingColors(sheetId) {
            const formats = this.getters.getConditionalFormats(sheetId);
            const formatColors = [];
            for (const format of formats) {
                const rule = format.rule;
                if (rule.type === "CellIsRule") {
                    formatColors.push(rule.style.textColor);
                    formatColors.push(rule.style.fillColor);
                }
                else if (rule.type === "ColorScaleRule") {
                    formatColors.push(colorNumberString(rule.minimum.color));
                    formatColors.push(rule.midpoint ? colorNumberString(rule.midpoint.color) : undefined);
                    formatColors.push(colorNumberString(rule.maximum.color));
                }
            }
            return formatColors.filter(isDefined$1);
        }
        getChartColors(sheetId) {
            const charts = this.getters.getChartIds(sheetId).map((cid) => this.getters.getChart(cid));
            let chartsColors = new Set();
            for (let chart of charts) {
                if (chart === undefined) {
                    continue;
                }
                const background = chart.getDefinition().background;
                if (background !== undefined) {
                    chartsColors.add(background);
                }
                switch (chart.type) {
                    case "gauge":
                        const colors = chart.sectionRule.colors;
                        chartsColors.add(colors.lowerColor);
                        chartsColors.add(colors.middleColor);
                        chartsColors.add(colors.upperColor);
                        break;
                    case "scorecard":
                        const scoreChart = chart;
                        chartsColors.add(scoreChart.baselineColorDown);
                        chartsColors.add(scoreChart.baselineColorUp);
                        break;
                }
            }
            return [...chartsColors];
        }
        tryToAddColor(color) {
            const formattedColor = toHex(color);
            if (color && !COLOR_PICKER_DEFAULTS.includes(formattedColor)) {
                this.history.update("customColors", formattedColor, true);
            }
        }
    }

    class EvaluationChartPlugin extends UIPlugin {
        static getters = ["getChartRuntime", "getStyleOfSingleCellChart"];
        charts = {};
        createRuntimeChart = chartRuntimeFactory(this.getters);
        handle(cmd) {
            if (invalidateEvaluationCommands.has(cmd.type) ||
                invalidateCFEvaluationCommands.has(cmd.type) ||
                cmd.type === "EVALUATE_CELLS" ||
                cmd.type === "UPDATE_CELL") {
                for (const chartId in this.charts) {
                    this.charts[chartId] = undefined;
                }
            }
            switch (cmd.type) {
                case "UPDATE_CHART":
                case "CREATE_CHART":
                case "DELETE_FIGURE":
                    this.charts[cmd.id] = undefined;
                    break;
                case "DELETE_SHEET":
                    for (let chartId in this.charts) {
                        if (!this.getters.isChartDefined(chartId)) {
                            this.charts[chartId] = undefined;
                        }
                    }
                    break;
            }
        }
        getChartRuntime(figureId) {
            if (!this.charts[figureId]) {
                const chart = this.getters.getChart(figureId);
                if (!chart) {
                    throw new Error(`No chart for the given id: ${figureId}`);
                }
                this.charts[figureId] = this.createRuntimeChart(chart);
            }
            return this.charts[figureId];
        }
        /**
         * Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.
         */
        getStyleOfSingleCellChart(chartBackground, mainRange) {
            if (chartBackground)
                return { background: chartBackground, fontColor: chartFontColor(chartBackground) };
            if (!mainRange) {
                return {
                    background: BACKGROUND_CHART_COLOR,
                    fontColor: chartFontColor(BACKGROUND_CHART_COLOR),
                };
            }
            const col = mainRange.zone.left;
            const row = mainRange.zone.top;
            const sheetId = mainRange.sheetId;
            const style = this.getters.getCellComputedStyle({ sheetId, col, row });
            const background = style.fillColor || BACKGROUND_CHART_COLOR;
            return {
                background,
                fontColor: style.textColor || chartFontColor(background),
            };
        }
        exportForExcel(data) {
            for (const sheet of data.sheets) {
                if (!sheet.images) {
                    sheet.images = [];
                }
                const sheetFigures = this.getters.getFigures(sheet.id);
                const figures = [];
                for (const figure of sheetFigures) {
                    if (!figure || figure.tag !== "chart") {
                        continue;
                    }
                    const figureId = figure.id;
                    const figureData = this.getters.getChart(figureId)?.getDefinitionForExcel();
                    if (figureData) {
                        figures.push({
                            ...figure,
                            data: figureData,
                        });
                    }
                    else {
                        const chart = this.getters.getChart(figureId);
                        if (!chart) {
                            continue;
                        }
                        const type = this.getters.getChartType(figureId);
                        const runtime = this.getters.getChartRuntime(figureId);
                        const img = chartToImage(runtime, figure, type);
                        sheet.images.push({
                            ...figure,
                            tag: "image",
                            data: {
                                mimetype: "image/png",
                                path: img,
                                size: { width: figure.width, height: figure.height },
                            },
                        });
                    }
                }
                sheet.charts = figures;
            }
        }
    }

    class EvaluationConditionalFormatPlugin extends UIPlugin {
        static getters = ["getConditionalIcon", "getCellComputedStyle"];
        isStale = true;
        // stores the computed styles in the format of computedStyles.sheetName[col][row] = Style
        computedStyles = {};
        computedIcons = {};
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handle(cmd) {
            if (invalidateCFEvaluationCommands.has(cmd.type) ||
                (cmd.type === "UPDATE_CELL" && "content" in cmd)) {
                this.isStale = true;
            }
        }
        finalize() {
            if (this.isStale) {
                for (const sheetId of this.getters.getSheetIds()) {
                    this.computedStyles[sheetId] = lazy(() => this.getComputedStyles(sheetId));
                    this.computedIcons[sheetId] = lazy(() => this.getComputedIcons(sheetId));
                }
                this.isStale = false;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getCellComputedStyle(position) {
            // TODO move this getter out of CF: it also depends on filters and link
            const { sheetId, col, row } = position;
            const cell = this.getters.getCell(position);
            const styles = this.computedStyles[sheetId]();
            const cfStyle = styles && styles[col]?.[row];
            const computedStyle = {
                ...cell?.style,
                ...cfStyle,
            };
            const evaluatedCell = this.getters.getEvaluatedCell(position);
            if (evaluatedCell.link && !computedStyle.textColor) {
                computedStyle.textColor = LINK_COLOR;
            }
            if (this.getters.isFilterHeader(position)) {
                computedStyle.bold = true;
            }
            return computedStyle;
        }
        getConditionalIcon({ sheetId, col, row }) {
            const icons = this.computedIcons[sheetId]();
            return icons && icons[col]?.[row];
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        /**
         * Compute the styles according to the conditional formatting.
         * This computation must happen after the cell values are computed if they change
         *
         * This result of the computation will be in the state.cell[XC].conditionalStyle and will be the union of all the style
         * properties of the rules applied (in order).
         * So if a cell has multiple conditional formatting applied to it, and each affect a different value of the style,
         * the resulting style will have the combination of all those values.
         * If multiple conditional formatting use the same style value, they will be applied in order so that the last applied wins
         */
        getComputedStyles(sheetId) {
            const computedStyle = {};
            for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
                switch (cf.rule.type) {
                    case "ColorScaleRule":
                        for (let range of cf.ranges) {
                            this.applyColorScale(sheetId, range, cf.rule, computedStyle);
                        }
                        break;
                    case "CellIsRule":
                        const formulas = cf.rule.values.map((value) => value.startsWith("=") ? compile(value) : undefined);
                        for (let ref of cf.ranges) {
                            const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
                            for (let row = zone.top; row <= zone.bottom; row++) {
                                for (let col = zone.left; col <= zone.right; col++) {
                                    const predicate = this.rulePredicate[cf.rule.type];
                                    const target = { sheetId, col, row };
                                    const values = cf.rule.values.map((value, i) => {
                                        const compiledFormula = formulas[i];
                                        if (compiledFormula) {
                                            return this.getters.getTranslatedCellFormula(sheetId, col - zone.left, row - zone.top, {
                                                ...compiledFormula,
                                                dependencies: compiledFormula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
                                            });
                                        }
                                        return value;
                                    });
                                    if (predicate && predicate(target, { ...cf.rule, values })) {
                                        if (!computedStyle[col])
                                            computedStyle[col] = [];
                                        // we must combine all the properties of all the CF rules applied to the given cell
                                        computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
                                    }
                                }
                            }
                        }
                        break;
                }
            }
            return computedStyle;
        }
        getComputedIcons(sheetId) {
            const computedIcons = {};
            for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
                if (cf.rule.type !== "IconSetRule")
                    continue;
                for (let range of cf.ranges) {
                    this.applyIcon(sheetId, range, cf.rule, computedIcons);
                }
            }
            return computedIcons;
        }
        parsePoint(sheetId, range, threshold, functionName) {
            const rangeValues = this.getters
                .getEvaluatedCellsInZone(sheetId, this.getters.getRangeFromSheetXC(sheetId, range).zone)
                .filter((cell) => cell.type === CellValueType.number)
                .map((cell) => cell.value);
            switch (threshold.type) {
                case "value":
                    const result = functionName === "max" ? Math.max(...rangeValues) : Math.min(...rangeValues);
                    return result;
                case "number":
                    return Number(threshold.value);
                case "percentage":
                    const min = Math.min(...rangeValues);
                    const max = Math.max(...rangeValues);
                    const delta = max - min;
                    return min + (delta * Number(threshold.value)) / 100;
                case "percentile":
                    return percentile(rangeValues, Number(threshold.value) / 100, true);
                case "formula":
                    const value = threshold.value && this.getters.evaluateFormula(sheetId, threshold.value);
                    return typeof value === "number" ? value : null;
                default:
                    return null;
            }
        }
        /** Compute the CF icons for the given range and CF rule, and apply in in the given computedIcons object */
        applyIcon(sheetId, range, rule, computedIcons) {
            const lowerInflectionPoint = this.parsePoint(sheetId, range, rule.lowerInflectionPoint);
            const upperInflectionPoint = this.parsePoint(sheetId, range, rule.upperInflectionPoint);
            if (lowerInflectionPoint === null ||
                upperInflectionPoint === null ||
                lowerInflectionPoint > upperInflectionPoint) {
                return;
            }
            const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
            const iconSet = [rule.icons.upper, rule.icons.middle, rule.icons.lower];
            for (let row = zone.top; row <= zone.bottom; row++) {
                for (let col = zone.left; col <= zone.right; col++) {
                    const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
                    if (cell.type !== CellValueType.number) {
                        continue;
                    }
                    const icon = this.computeIcon(cell.value, upperInflectionPoint, rule.upperInflectionPoint.operator, lowerInflectionPoint, rule.lowerInflectionPoint.operator, iconSet);
                    if (!computedIcons[col]) {
                        computedIcons[col] = [];
                    }
                    computedIcons[col][row] = icon;
                }
            }
        }
        computeIcon(value, upperInflectionPoint, upperOperator, lowerInflectionPoint, lowerOperator, icons) {
            if ((upperOperator === "ge" && value >= upperInflectionPoint) ||
                (upperOperator === "gt" && value > upperInflectionPoint)) {
                return icons[0];
            }
            else if ((lowerOperator === "ge" && value >= lowerInflectionPoint) ||
                (lowerOperator === "gt" && value > lowerInflectionPoint)) {
                return icons[1];
            }
            return icons[2];
        }
        /** Compute the color scale for the given range and CF rule, and apply in in the given computedStyle object */
        applyColorScale(sheetId, range, rule, computedStyle) {
            const minValue = this.parsePoint(sheetId, range, rule.minimum, "min");
            const midValue = rule.midpoint
                ? this.parsePoint(sheetId, range, rule.midpoint)
                : null;
            const maxValue = this.parsePoint(sheetId, range, rule.maximum, "max");
            if (minValue === null ||
                maxValue === null ||
                minValue >= maxValue ||
                (midValue && (minValue >= midValue || midValue >= maxValue))) {
                return;
            }
            const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
            const colorCellArgs = [];
            if (rule.midpoint && midValue) {
                colorCellArgs.push({
                    minValue,
                    minColor: rule.minimum.color,
                    colorDiffUnit: this.computeColorDiffUnits(minValue, midValue, rule.minimum.color, rule.midpoint.color),
                });
                colorCellArgs.push({
                    minValue: midValue,
                    minColor: rule.midpoint.color,
                    colorDiffUnit: this.computeColorDiffUnits(midValue, maxValue, rule.midpoint.color, rule.maximum.color),
                });
            }
            else {
                colorCellArgs.push({
                    minValue,
                    minColor: rule.minimum.color,
                    colorDiffUnit: this.computeColorDiffUnits(minValue, maxValue, rule.minimum.color, rule.maximum.color),
                });
            }
            for (let row = zone.top; row <= zone.bottom; row++) {
                for (let col = zone.left; col <= zone.right; col++) {
                    const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
                    if (cell.type === CellValueType.number) {
                        const value = clip(cell.value, minValue, maxValue);
                        let color;
                        if (colorCellArgs.length === 2 && midValue) {
                            color =
                                value <= midValue
                                    ? this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit)
                                    : this.colorCell(value, colorCellArgs[1].minValue, colorCellArgs[1].minColor, colorCellArgs[1].colorDiffUnit);
                        }
                        else {
                            color = this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit);
                        }
                        if (!computedStyle[col])
                            computedStyle[col] = [];
                        computedStyle[col][row] = computedStyle[col]?.[row] || {};
                        computedStyle[col][row].fillColor = colorNumberString(color);
                    }
                }
            }
        }
        computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
            const deltaValue = maxValue - minValue;
            const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
            const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
            const deltaColorB = (minColor % 256) - (maxColor % 256);
            const colorDiffUnitR = deltaColorR / deltaValue;
            const colorDiffUnitG = deltaColorG / deltaValue;
            const colorDiffUnitB = deltaColorB / deltaValue;
            return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
        }
        colorCell(value, minValue, minColor, colorDiffUnit) {
            const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
            const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
            const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
            const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
            return (r << 16) | (g << 8) | b;
        }
        /**
         * Execute the predicate to know if a conditional formatting rule should be applied to a cell
         */
        rulePredicate = {
            CellIsRule: (target, rule) => {
                const cell = this.getters.getEvaluatedCell(target);
                if (cell.type === CellValueType.error) {
                    return false;
                }
                const [value0, value1] = rule.values.map((val) => {
                    if (val.startsWith("=")) {
                        return this.getters.evaluateFormula(target.sheetId, val) ?? "";
                    }
                    return parseLiteral(val, DEFAULT_LOCALE);
                });
                if (isMatrix(value0) || isMatrix(value1)) {
                    return false;
                }
                switch (rule.operator) {
                    case "IsEmpty":
                        return cell.value.toString().trim() === "";
                    case "IsNotEmpty":
                        return cell.value.toString().trim() !== "";
                    case "BeginsWith":
                        if (value0 === "") {
                            return false;
                        }
                        return cell.value.toString().startsWith(value0.toString());
                    case "EndsWith":
                        if (value0 === "") {
                            return false;
                        }
                        return cell.value.toString().endsWith(value0.toString());
                    case "Between":
                        return cell.value >= value0 && cell.value <= value1;
                    case "NotBetween":
                        return !(cell.value >= value0 && cell.value <= value1);
                    case "ContainsText":
                        return cell.value.toString().indexOf(value0.toString()) > -1;
                    case "NotContains":
                        return !cell.value || cell.value.toString().indexOf(value0.toString()) === -1;
                    case "GreaterThan":
                        return cell.value > value0;
                    case "GreaterThanOrEqual":
                        return cell.value >= value0;
                    case "LessThan":
                        return cell.value < value0;
                    case "LessThanOrEqual":
                        return cell.value <= value0;
                    case "NotEqual":
                        if (value0 === "") {
                            return false;
                        }
                        return cell.value !== value0;
                    case "Equal":
                        if (value0 === "") {
                            return true;
                        }
                        return cell.value === value0;
                    default:
                        console.warn(_t("Not implemented operator %s for kind of conditional formatting:  %s", rule.operator, rule.type));
                }
                return false;
            },
        };
    }

    const VALID_RESULT = { isValid: true };
    class EvaluationDataValidationPlugin extends UIPlugin {
        static getters = [
            "getDataValidationInvalidCriterionValueMessage",
            "getDataValidationCheckBoxCellPositions",
            "getDataValidationListCellsPositions",
            "getInvalidDataValidationMessage",
            "getValidationResultForCellValue",
            "isCellValidCheckbox",
            "isDataValidationInvalid",
        ];
        validationResults = {};
        handle(cmd) {
            if (invalidateEvaluationCommands.has(cmd.type) ||
                cmd.type === "EVALUATE_CELLS" ||
                (cmd.type === "UPDATE_CELL" && "content" in cmd)) {
                this.validationResults = {};
                return;
            }
            switch (cmd.type) {
                case "ADD_DATA_VALIDATION_RULE":
                case "REMOVE_DATA_VALIDATION_RULE":
                    delete this.validationResults[cmd.sheetId];
                    break;
            }
        }
        isDataValidationInvalid(cellPosition) {
            return !this.getValidationResultForCell(cellPosition).isValid;
        }
        getInvalidDataValidationMessage(cellPosition) {
            const validationResult = this.getValidationResultForCell(cellPosition);
            return validationResult.isValid ? undefined : validationResult.error;
        }
        /**
         * Check if the value is valid for the given criterion, and return an error message if not.
         *
         * The value must be canonicalized.
         */
        getDataValidationInvalidCriterionValueMessage(criterionType, value) {
            const evaluator = dataValidationEvaluatorRegistry.get(criterionType);
            if (value.startsWith("=")) {
                return evaluator.allowedValues === "onlyLiterals"
                    ? _t("The value must not be a formula")
                    : undefined;
            }
            else if (evaluator.allowedValues === "onlyFormulas") {
                return _t("The value must be a formula");
            }
            return evaluator.isCriterionValueValid(value) ? undefined : evaluator.criterionValueErrorString;
        }
        isCellValidCheckbox(cellPosition) {
            if (!this.getters.isMainCellPosition(cellPosition)) {
                return false;
            }
            const rule = this.getters.getValidationRuleForCell(cellPosition);
            if (!rule || rule.criterion.type !== "isBoolean") {
                return false;
            }
            return this.getValidationResultForCell(cellPosition).isValid;
        }
        /** Get the validation result if the cell on the given position had the given value */
        getValidationResultForCellValue(cellValue, cellPosition) {
            const rule = this.getters.getValidationRuleForCell(cellPosition);
            if (!rule) {
                return VALID_RESULT;
            }
            const error = this.getRuleErrorForCellValue(cellValue, cellPosition, rule);
            return error ? { error, rule, isValid: false } : VALID_RESULT;
        }
        getDataValidationCheckBoxCellPositions() {
            const rules = this.getters
                .getDataValidationRules(this.getters.getActiveSheetId())
                .filter((rule) => rule.criterion.type === "isBoolean");
            return getCellPositionsInRanges(rules.map((rule) => rule.ranges).flat()).filter((position) => this.isCellValidCheckbox(position));
        }
        getDataValidationListCellsPositions() {
            const rules = this.getters
                .getDataValidationRules(this.getters.getActiveSheetId())
                .filter((rule) => (rule.criterion.type === "isValueInList" || rule.criterion.type === "isValueInRange") &&
                rule.criterion.displayStyle === "arrow");
            return getCellPositionsInRanges(rules.map((rule) => rule.ranges).flat());
        }
        getValidationResultForCell(cellPosition) {
            const { col, row, sheetId } = cellPosition;
            if (!this.validationResults[sheetId]) {
                this.validationResults[sheetId] = this.computeSheetValidationResults(sheetId);
            }
            return this.validationResults[sheetId][col]?.[row]?.() || VALID_RESULT;
        }
        computeSheetValidationResults(sheetId) {
            const validationResults = {};
            const ranges = this.getters.getDataValidationRules(sheetId).map((rule) => rule.ranges);
            for (const cellPosition of getCellPositionsInRanges(ranges.flat())) {
                const { col, row } = cellPosition;
                if (!validationResults[col]) {
                    validationResults[col] = [];
                }
                validationResults[col][row] = lazy(() => {
                    const evaluatedCell = this.getters.getEvaluatedCell(cellPosition);
                    if (evaluatedCell.type === CellValueType.empty) {
                        return VALID_RESULT;
                    }
                    return this.getValidationResultForCellValue(evaluatedCell.value, cellPosition);
                });
            }
            return validationResults;
        }
        getRuleErrorForCellValue(cellValue, cellPosition, rule) {
            const { sheetId } = cellPosition;
            const criterion = rule.criterion;
            const evaluator = dataValidationEvaluatorRegistry.get(criterion.type);
            const offset = this.getCellOffsetInRule(cellPosition, rule);
            const evaluatedCriterionValues = this.getEvaluatedCriterionValues(sheetId, offset, criterion);
            const evaluatedCriterion = { ...criterion, values: evaluatedCriterionValues };
            if (evaluator.isValueValid(cellValue, evaluatedCriterion, this.getters, sheetId)) {
                return undefined;
            }
            return evaluator.getErrorString(evaluatedCriterion, this.getters, sheetId);
        }
        /** Get the offset of the cell inside the ranges of the rule. Throws an error if the cell isn't inside the rule. */
        getCellOffsetInRule(cellPosition, rule) {
            const range = rule.ranges.find((range) => isInside(cellPosition.col, cellPosition.row, range.zone));
            if (!range) {
                throw new Error("The cell is not in any range of the rule");
            }
            return {
                col: cellPosition.col - range.zone.left,
                row: cellPosition.row - range.zone.top,
            };
        }
        getEvaluatedCriterionValues(sheetId, offset, criterion) {
            return criterion.values.map((value) => {
                if (!value.startsWith("=")) {
                    return value;
                }
                try {
                    const formula = compile(value);
                    const translatedFormula = this.getters.getTranslatedCellFormula(sheetId, offset.col, offset.row, {
                        ...formula,
                        dependencies: formula.dependencies.map((d) => this.getters.getRangeFromSheetXC(sheetId, d)),
                    });
                    const evaluated = this.getters.evaluateFormula(sheetId, translatedFormula);
                    return evaluated && !isMatrix(evaluated) ? evaluated.toString() : "";
                }
                catch (e) {
                    return e instanceof EvaluationError ? e.errorType : CellErrorType.GenericError;
                }
            });
        }
    }

    class HeaderSizeUIPlugin extends UIPlugin {
        static getters = ["getRowSize", "getHeaderSize"];
        tallestCellInRow = {};
        ctx = document.createElement("canvas").getContext("2d");
        handle(cmd) {
            switch (cmd.type) {
                case "START":
                    for (const sheetId of this.getters.getSheetIds()) {
                        this.initializeSheet(sheetId);
                    }
                    break;
                case "CREATE_SHEET": {
                    this.initializeSheet(cmd.sheetId);
                    break;
                }
                case "DUPLICATE_SHEET": {
                    const tallestCells = deepCopy(this.tallestCellInRow[cmd.sheetId]);
                    this.history.update("tallestCellInRow", cmd.sheetIdTo, tallestCells);
                    break;
                }
                case "DELETE_SHEET":
                    const tallestCells = { ...this.tallestCellInRow };
                    delete tallestCells[cmd.sheetId];
                    this.history.update("tallestCellInRow", tallestCells);
                    break;
                case "REMOVE_COLUMNS_ROWS": {
                    if (cmd.dimension === "COL") {
                        return;
                    }
                    const tallestCells = removeIndexesFromArray(this.tallestCellInRow[cmd.sheetId], cmd.elements);
                    this.history.update("tallestCellInRow", cmd.sheetId, tallestCells);
                    break;
                }
                case "ADD_COLUMNS_ROWS": {
                    if (cmd.dimension === "COL") {
                        return;
                    }
                    const addIndex = getAddHeaderStartIndex(cmd.position, cmd.base);
                    const newCells = Array(cmd.quantity).fill(undefined);
                    const newTallestCells = insertItemsAtIndex(this.tallestCellInRow[cmd.sheetId], newCells, addIndex);
                    this.history.update("tallestCellInRow", cmd.sheetId, newTallestCells);
                    break;
                }
                case "RESIZE_COLUMNS_ROWS":
                    {
                        const sheetId = cmd.sheetId;
                        if (cmd.dimension === "ROW") {
                            for (const row of cmd.elements) {
                                const tallestCell = this.getRowTallestCell(sheetId, row);
                                this.history.update("tallestCellInRow", sheetId, row, tallestCell);
                            }
                        }
                        else {
                            // Recompute row heights on col size change, they might have changed because of wrapped text
                            for (const row of range(0, this.getters.getNumberRows(sheetId))) {
                                for (const col of cmd.elements) {
                                    this.updateRowSizeForCellChange(sheetId, row, col);
                                }
                            }
                        }
                    }
                    break;
                case "UPDATE_CELL":
                    this.updateRowSizeForCellChange(cmd.sheetId, cmd.row, cmd.col);
                    break;
                case "ADD_MERGE":
                case "REMOVE_MERGE":
                    for (const target of cmd.target) {
                        for (const position of positions(target)) {
                            this.updateRowSizeForCellChange(cmd.sheetId, position.row, position.col);
                        }
                    }
            }
            return;
        }
        getRowSize(sheetId, row) {
            return Math.round(this.getters.getUserRowSize(sheetId, row) ??
                this.tallestCellInRow[sheetId][row]?.size ??
                DEFAULT_CELL_HEIGHT);
        }
        getHeaderSize(sheetId, dimension, index) {
            if (this.getters.isHeaderHidden(sheetId, dimension, index)) {
                return 0;
            }
            return dimension === "ROW"
                ? this.getRowSize(sheetId, index)
                : this.getters.getColSize(sheetId, index);
        }
        updateRowSizeForCellChange(sheetId, row, col) {
            const tallestCellInRow = this.tallestCellInRow[sheetId]?.[row];
            if (tallestCellInRow?.cell.col === col) {
                const newTallestCell = this.getRowTallestCell(sheetId, row);
                this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
            }
            const updatedCellHeight = this.getCellHeight({ sheetId, col, row });
            if (updatedCellHeight <= DEFAULT_CELL_HEIGHT) {
                return;
            }
            if ((!tallestCellInRow && updatedCellHeight > DEFAULT_CELL_HEIGHT) ||
                (tallestCellInRow && updatedCellHeight > tallestCellInRow.size)) {
                const newTallestCell = { cell: { sheetId, col, row }, size: updatedCellHeight };
                this.history.update("tallestCellInRow", sheetId, row, newTallestCell);
            }
        }
        initializeSheet(sheetId) {
            const tallestCells = [];
            for (let row = 0; row < this.getters.getNumberRows(sheetId); row++) {
                const tallestCell = this.getRowTallestCell(sheetId, row);
                tallestCells.push(tallestCell);
            }
            this.history.update("tallestCellInRow", sheetId, tallestCells);
        }
        /**
         * Return the height the cell should have in the sheet, which is either DEFAULT_CELL_HEIGHT if the cell is in a multi-row
         * merge, or the height of the cell computed based on its style/content.
         */
        getCellHeight(position) {
            if (this.isInMultiRowMerge(position)) {
                return DEFAULT_CELL_HEIGHT;
            }
            const cell = this.getters.getCell(position);
            const colSize = this.getters.getColSize(position.sheetId, position.col);
            return getDefaultCellHeight(this.ctx, cell, colSize);
        }
        isInMultiRowMerge(position) {
            const merge = this.getters.getMerge(position);
            return !!merge && merge.bottom !== merge.top;
        }
        /**
         * Get the tallest cell of a row and its size.
         */
        getRowTallestCell(sheetId, row) {
            const userRowSize = this.getters.getUserRowSize(sheetId, row);
            if (userRowSize !== undefined) {
                return undefined;
            }
            const cellIds = this.getters.getRowCells(sheetId, row);
            let maxHeight = 0;
            let tallestCell = undefined;
            for (let i = 0; i < cellIds.length; i++) {
                const cell = this.getters.getCellById(cellIds[i]);
                if (!cell) {
                    continue;
                }
                const position = this.getters.getCellPosition(cell.id);
                const cellHeight = this.getCellHeight(position);
                if (cellHeight > maxHeight && cellHeight > DEFAULT_CELL_HEIGHT) {
                    maxHeight = cellHeight;
                    tallestCell = { cell: position, size: cellHeight };
                }
            }
            if (tallestCell && tallestCell.size > DEFAULT_CELL_HEIGHT) {
                return tallestCell;
            }
            return undefined;
        }
    }

    /**
     * This plugin manage the autofill.
     *
     * The way it works is the next one:
     * For each line (row if the direction is left/right, col otherwise), we create
     * a "AutofillGenerator" object which is used to compute the cells to
     * autofill.
     *
     * When we need to autofill a cell, we compute the origin cell in the source.
     *  EX: from A1:A2, autofill A3->A6.
     *      Target | Origin cell
     *        A3   |   A1
     *        A4   |   A2
     *        A5   |   A1
     *        A6   |   A2
     * When we have the origin, we take the associated cell in the AutofillGenerator
     * and we apply the modifier (AutofillModifier) associated to the content of the
     * cell.
     */
    /**
     * This class is used to generate the next values to autofill.
     * It's done from a selection (the source) and describe how the next values
     * should be computed.
     */
    class AutofillGenerator {
        cells;
        getters;
        index = 0;
        direction;
        constructor(cells, getters, direction) {
            this.cells = cells;
            this.getters = getters;
            this.direction = direction;
        }
        /**
         * Get the next value to autofill
         */
        next() {
            const genCell = this.cells[this.index++ % this.cells.length];
            const rule = genCell.rule;
            const { cellData, tooltip } = autofillModifiersRegistry
                .get(rule.type)
                .apply(rule, genCell.data, this.getters, this.direction);
            return {
                cellData,
                tooltip,
                origin: {
                    col: genCell.data.col,
                    row: genCell.data.row,
                },
            };
        }
    }
    /**
     * Autofill Plugin
     *
     */
    class AutofillPlugin extends UIPlugin {
        static layers = [5 /* LAYERS.Autofill */];
        static getters = ["getAutofillTooltip"];
        autofillZone;
        steps;
        lastCellSelected = {};
        direction;
        tooltip;
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "AUTOFILL_SELECT":
                    const sheetId = this.getters.getActiveSheetId();
                    this.lastCellSelected.col =
                        cmd.col === -1
                            ? this.lastCellSelected.col
                            : clip(cmd.col, 0, this.getters.getNumberCols(sheetId));
                    this.lastCellSelected.row =
                        cmd.row === -1
                            ? this.lastCellSelected.row
                            : clip(cmd.row, 0, this.getters.getNumberRows(sheetId));
                    if (this.lastCellSelected.col !== undefined && this.lastCellSelected.row !== undefined) {
                        return "Success" /* CommandResult.Success */;
                    }
                    return "InvalidAutofillSelection" /* CommandResult.InvalidAutofillSelection */;
                case "AUTOFILL_AUTO":
                    const zone = this.getters.getSelectedZone();
                    return zone.top === zone.bottom
                        ? "Success" /* CommandResult.Success */
                        : "CancelledForUnknownReason" /* CommandResult.CancelledForUnknownReason */;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "AUTOFILL":
                    this.autofill(true);
                    break;
                case "AUTOFILL_SELECT":
                    this.select(cmd.col, cmd.row);
                    break;
                case "AUTOFILL_AUTO":
                    this.autofillAuto();
                    break;
                case "AUTOFILL_CELL":
                    this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
                    const sheetId = this.getters.getActiveSheetId();
                    this.dispatch("UPDATE_CELL", {
                        sheetId,
                        col: cmd.col,
                        row: cmd.row,
                        style: cmd.style || null,
                        content: cmd.content || "",
                        format: cmd.format || "",
                    });
                    this.dispatch("SET_BORDER", {
                        sheetId,
                        col: cmd.col,
                        row: cmd.row,
                        border: cmd.border,
                    });
                    this.autofillCF(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
                    this.autofillDV(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getAutofillTooltip() {
            return this.tooltip;
        }
        // ---------------------------------------------------------------------------
        // Private methods
        // ---------------------------------------------------------------------------
        /**
         * Autofill the autofillZone from the current selection
         * @param apply Flag set to true to apply the autofill in the model. It's
         *              useful to set it to false when we need to fill the tooltip
         */
        autofill(apply) {
            if (!this.autofillZone || !this.steps || this.direction === undefined) {
                this.tooltip = undefined;
                return;
            }
            const source = this.getters.getSelectedZone();
            const target = this.autofillZone;
            switch (this.direction) {
                case "down" /* DIRECTION.DOWN */:
                    for (let col = source.left; col <= source.right; col++) {
                        const xcs = [];
                        for (let row = source.top; row <= source.bottom; row++) {
                            xcs.push(toXC(col, row));
                        }
                        const generator = this.createGenerator(xcs);
                        for (let row = target.top; row <= target.bottom; row++) {
                            this.computeNewCell(generator, col, row, apply);
                        }
                    }
                    break;
                case "up" /* DIRECTION.UP */:
                    for (let col = source.left; col <= source.right; col++) {
                        const xcs = [];
                        for (let row = source.bottom; row >= source.top; row--) {
                            xcs.push(toXC(col, row));
                        }
                        const generator = this.createGenerator(xcs);
                        for (let row = target.bottom; row >= target.top; row--) {
                            this.computeNewCell(generator, col, row, apply);
                        }
                    }
                    break;
                case "left" /* DIRECTION.LEFT */:
                    for (let row = source.top; row <= source.bottom; row++) {
                        const xcs = [];
                        for (let col = source.right; col >= source.left; col--) {
                            xcs.push(toXC(col, row));
                        }
                        const generator = this.createGenerator(xcs);
                        for (let col = target.right; col >= target.left; col--) {
                            this.computeNewCell(generator, col, row, apply);
                        }
                    }
                    break;
                case "right" /* DIRECTION.RIGHT */:
                    for (let row = source.top; row <= source.bottom; row++) {
                        const xcs = [];
                        for (let col = source.left; col <= source.right; col++) {
                            xcs.push(toXC(col, row));
                        }
                        const generator = this.createGenerator(xcs);
                        for (let col = target.left; col <= target.right; col++) {
                            this.computeNewCell(generator, col, row, apply);
                        }
                    }
                    break;
            }
            if (apply) {
                this.autofillZone = undefined;
                this.selection.resizeAnchorZone(this.direction, this.steps);
                this.lastCellSelected = {};
                this.direction = undefined;
                this.steps = 0;
                this.tooltip = undefined;
            }
        }
        /**
         * Select a cell which becomes the last cell of the autofillZone
         */
        select(col, row) {
            const source = this.getters.getSelectedZone();
            if (isInside(col, row, source)) {
                this.autofillZone = undefined;
                return;
            }
            this.direction = this.getDirection(col, row);
            switch (this.direction) {
                case "up" /* DIRECTION.UP */:
                    this.saveZone(row, source.top - 1, source.left, source.right);
                    this.steps = source.top - row;
                    break;
                case "down" /* DIRECTION.DOWN */:
                    this.saveZone(source.bottom + 1, row, source.left, source.right);
                    this.steps = row - source.bottom;
                    break;
                case "left" /* DIRECTION.LEFT */:
                    this.saveZone(source.top, source.bottom, col, source.left - 1);
                    this.steps = source.left - col;
                    break;
                case "right" /* DIRECTION.RIGHT */:
                    this.saveZone(source.top, source.bottom, source.right + 1, col);
                    this.steps = col - source.right;
                    break;
            }
            this.autofill(false);
        }
        /**
         * Computes the autofillZone to autofill when the user double click on the
         * autofiller
         */
        autofillAuto() {
            const zone = this.getters.getSelectedZone();
            const sheetId = this.getters.getActiveSheetId();
            let col = zone.left;
            let row = zone.bottom;
            if (col > 0) {
                let leftPosition = { sheetId, col: col - 1, row };
                while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
                    this.getters.getCell(leftPosition)?.content) {
                    row += 1;
                    leftPosition = { sheetId, col: col - 1, row };
                }
            }
            if (row === zone.bottom) {
                col = zone.right;
                if (col <= this.getters.getNumberCols(sheetId)) {
                    let rightPosition = { sheetId, col: col + 1, row };
                    while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
                        this.getters.getCell(rightPosition)?.content) {
                        row += 1;
                        rightPosition = { sheetId, col: col + 1, row };
                    }
                }
            }
            if (row !== zone.bottom) {
                this.select(zone.left, row - 1);
                this.autofill(true);
            }
        }
        /**
         * Generate the next cell
         */
        computeNewCell(generator, col, row, apply) {
            const { cellData, tooltip, origin } = generator.next();
            const { content, style, border, format } = cellData;
            this.tooltip = tooltip;
            if (apply) {
                this.dispatch("AUTOFILL_CELL", {
                    originCol: origin.col,
                    originRow: origin.row,
                    col,
                    row,
                    content,
                    style,
                    border,
                    format,
                });
            }
        }
        /**
         * Get the rule associated to the current cell
         */
        getRule(cell, cells) {
            const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
            const rule = rules.find((rule) => rule.condition(cell, cells));
            return rule && rule.generateRule(cell, cells);
        }
        /**
         * Create the generator to be able to autofill the next cells.
         */
        createGenerator(source) {
            const nextCells = [];
            const cellsData = [];
            const sheetId = this.getters.getActiveSheetId();
            for (let xc of source) {
                const { col, row } = toCartesian(xc);
                const cell = this.getters.getCell({ sheetId, col, row });
                cellsData.push({
                    col,
                    row,
                    cell,
                    sheetId,
                });
            }
            const cells = cellsData.map((cellData) => cellData.cell);
            for (let cellData of cellsData) {
                let rule = { type: "COPY_MODIFIER" };
                if (cellData && cellData.cell) {
                    const newRule = this.getRule(cellData.cell, cells);
                    rule = newRule || rule;
                }
                const border = this.getters.getCellBorder(cellData) || undefined;
                nextCells.push({
                    data: { ...cellData, border },
                    rule,
                });
            }
            return new AutofillGenerator(nextCells, this.getters, this.direction);
        }
        saveZone(top, bottom, left, right) {
            this.autofillZone = { top, bottom, left, right };
        }
        /**
         * Compute the direction of the autofill from the last selected zone and
         * a given cell (col, row)
         */
        getDirection(col, row) {
            const source = this.getters.getSelectedZone();
            const position = {
                up: { number: source.top - row, value: "up" /* DIRECTION.UP */ },
                down: { number: row - source.bottom, value: "down" /* DIRECTION.DOWN */ },
                left: { number: source.left - col, value: "left" /* DIRECTION.LEFT */ },
                right: { number: col - source.right, value: "right" /* DIRECTION.RIGHT */ },
            };
            if (Object.values(position)
                .map((x) => (x.number > 0 ? 1 : 0))
                .reduce((acc, value) => acc + value) === 1) {
                return Object.values(position).find((x) => (x.number > 0 ? 1 : 0)).value;
            }
            const first = position.up.number > 0 ? "up" : "down";
            const second = position.left.number > 0 ? "left" : "right";
            return Math.abs(position[first].number) >= Math.abs(position[second].number)
                ? position[first].value
                : position[second].value;
        }
        autoFillMerge(originCol, originRow, col, row) {
            const sheetId = this.getters.getActiveSheetId();
            const position = { sheetId, col, row };
            const originPosition = { sheetId, col: originCol, row: originRow };
            if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {
                const zone = this.getters.getMerge(position);
                if (zone) {
                    this.dispatch("REMOVE_MERGE", {
                        sheetId,
                        target: [zone],
                    });
                }
            }
            const originMerge = this.getters.getMerge(originPosition);
            if (originMerge?.topLeft.col === originCol && originMerge?.topLeft.row === originRow) {
                this.dispatch("ADD_MERGE", {
                    sheetId,
                    target: [
                        {
                            top: row,
                            bottom: row + originMerge.bottom - originMerge.top,
                            left: col,
                            right: col + originMerge.right - originMerge.left,
                        },
                    ],
                });
            }
        }
        autofillCF(originCol, originRow, col, row) {
            const sheetId = this.getters.getActiveSheetId();
            const cfOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
            for (const cf of cfOrigin) {
                const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, [toXC(col, row)], []);
                if (newCfRanges) {
                    this.dispatch("ADD_CONDITIONAL_FORMAT", {
                        cf: deepCopy(cf),
                        ranges: newCfRanges.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
                        sheetId,
                    });
                }
            }
        }
        autofillDV(originCol, originRow, col, row) {
            const sheetId = this.getters.getActiveSheetId();
            const cellPosition = { sheetId, col: originCol, row: originRow };
            const dvOrigin = this.getters.getValidationRuleForCell(cellPosition);
            if (!dvOrigin) {
                return;
            }
            const dvRangesXcs = dvOrigin.ranges.map((range) => this.getters.getRangeString(range, sheetId));
            const newDvRanges = recomputeZones(dvRangesXcs.concat(toXC(col, row)), []);
            this.dispatch("ADD_DATA_VALIDATION_RULE", {
                rule: dvOrigin,
                ranges: newDvRanges.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
                sheetId,
            });
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(renderingContext) {
            if (!this.autofillZone) {
                return;
            }
            const { ctx, thinLineWidth } = renderingContext;
            const { x, y, width, height } = this.getters.getVisibleRect(this.autofillZone);
            if (width > 0 && height > 0) {
                ctx.strokeStyle = "black";
                ctx.lineWidth = thinLineWidth;
                ctx.setLineDash([3]);
                ctx.strokeRect(x, y, width, height);
                ctx.setLineDash([]);
            }
        }
    }

    class AutomaticSumPlugin extends UIPlugin {
        static getters = ["getAutomaticSums"];
        handle(cmd) {
            switch (cmd.type) {
                case "SUM_SELECTION":
                    const sheetId = this.getters.getActiveSheetId();
                    const { zones, anchor } = this.getters.getSelection();
                    for (const zone of zones) {
                        const sums = this.getAutomaticSums(sheetId, zone, anchor.cell);
                        this.dispatchCellUpdates(sheetId, sums);
                    }
                    break;
            }
        }
        getAutomaticSums(sheetId, zone, anchor) {
            return this.shouldFindData(sheetId, zone)
                ? this.sumAdjacentData(sheetId, zone, anchor)
                : this.sumData(sheetId, zone);
        }
        // ---------------------------------------------------------------------------
        // Private methods
        // ---------------------------------------------------------------------------
        sumData(sheetId, zone) {
            const dimensions = this.dimensionsToSum(sheetId, zone);
            const sums = this.sumDimensions(sheetId, zone, dimensions).filter(({ zone }) => !this.getters.isEmpty(sheetId, zone));
            if (dimensions.has("ROW") && dimensions.has("COL")) {
                sums.push(this.sumTotal(zone));
            }
            return sums;
        }
        sumAdjacentData(sheetId, zone, anchor) {
            const { col, row } = isInside(anchor.col, anchor.row, zone)
                ? anchor
                : { col: zone.left, row: zone.top };
            const dataZone = this.findAdjacentData(sheetId, col, row);
            if (!dataZone) {
                return [];
            }
            if (this.getters.isSingleCellOrMerge(sheetId, zone) ||
                isOneDimensional(union(dataZone, zone))) {
                return [{ position: { col, row }, zone: dataZone }];
            }
            else {
                return this.sumDimensions(sheetId, union(dataZone, zone), this.transpose(this.dimensionsToSum(sheetId, zone)));
            }
        }
        /**
         * Find a zone to automatically sum a column or row of numbers.
         *
         * We first decide which direction will be summed (column or row).
         * Here is the strategy:
         *  1. If the left cell is a number and the top cell is not: choose horizontal
         *  2. Try to find a valid vertical zone. If it's valid: choose vertical
         *  3. Try to find a valid horizontal zone. If it's valid: choose horizontal
         *  4. Otherwise, no zone is returned
         *
         * Now, how to find a valid zone?
         * The zone starts directly above or on the left of the starting point
         * (depending on the direction).
         * The zone ends where the first continuous sequence of numbers ends.
         * Empty or text cells can be part of the zone while no number has been found.
         * Other kind of cells (boolean, dates, etc.) are not valid in the zone and the
         * search stops immediately if one is found.
         *
         *  -------                                       -------
         * |   1   |                                     |   1   |
         *  -------                                       -------
         * |       |                                     |       |
         *  -------  <= end of the sequence, stop here    -------
         * |   2   |                                     |   2   |
         *  -------                                       -------
         * |   3   | <= start of the number sequence     |   3   |
         *  -------                                       -------
         * |       | <= ignored                          | FALSE | <= invalid, no zone is found
         *  -------                                       -------
         * |   A   | <= ignored                          |   A   | <= ignored
         *  -------                                       -------
         */
        findAdjacentData(sheetId, col, row) {
            const sheet = this.getters.getSheet(sheetId);
            const mainCellPosition = this.getters.getMainCellPosition({ sheetId, col, row });
            const zone = this.findSuitableZoneToSum(sheet, mainCellPosition.col, mainCellPosition.row);
            if (zone) {
                return this.getters.expandZone(sheetId, zone);
            }
            return undefined;
        }
        /**
         * Return the zone to sum if a valid one is found.
         * @see getAutomaticSumZone
         */
        findSuitableZoneToSum(sheet, col, row) {
            const topCell = this.getters.getEvaluatedCell({ sheetId: sheet.id, col, row: row - 1 });
            const leftCell = this.getters.getEvaluatedCell({ sheetId: sheet.id, col: col - 1, row });
            if (this.isNumber(leftCell) && !this.isNumber(topCell)) {
                return this.findHorizontalZone(sheet, col, row);
            }
            const verticalZone = this.findVerticalZone(sheet, col, row);
            if (this.isZoneValid(verticalZone)) {
                return verticalZone;
            }
            const horizontalZone = this.findHorizontalZone(sheet, col, row);
            if (this.isZoneValid(horizontalZone)) {
                return horizontalZone;
            }
            return undefined;
        }
        findVerticalZone(sheet, col, row) {
            const zone = {
                top: 0,
                bottom: row - 1,
                left: col,
                right: col,
            };
            const top = this.reduceZoneStart(sheet, zone, zone.bottom);
            return { ...zone, top };
        }
        findHorizontalZone(sheet, col, row) {
            const zone = {
                top: row,
                bottom: row,
                left: 0,
                right: col - 1,
            };
            const left = this.reduceZoneStart(sheet, zone, zone.right);
            return { ...zone, left };
        }
        /**
         * Reduces a column or row zone to a valid zone for the automatic sum.
         * @see getAutomaticSumZone
         * @param sheet
         * @param zone one dimensional zone (a single row or a single column). The zone is
         *             assumed to start at the beginning of the column (top=0) or the row (left=0)
         * @param end end index of the zone (`bottom` or `right` depending on the dimension)
         * @returns the starting position of the valid zone or Infinity if the zone is not valid.
         */
        reduceZoneStart(sheet, zone, end) {
            const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
            const cellPositions = range(end, -1, -1);
            const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
            const maxValidPosition = Math.max(...invalidCells);
            const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
            const firstSequence = numberSequences[0] || [];
            if (Math.max(...firstSequence) < maxValidPosition) {
                return Infinity;
            }
            return Math.min(...firstSequence);
        }
        shouldFindData(sheetId, zone) {
            return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
        }
        isNumber(cell) {
            return cell.type === CellValueType.number && !(cell.format && isDateTimeFormat(cell.format));
        }
        isZoneValid(zone) {
            return zone.bottom >= zone.top && zone.right >= zone.left;
        }
        lastColIsEmpty(sheetId, zone) {
            return this.getters.isEmpty(sheetId, { ...zone, left: zone.right });
        }
        lastRowIsEmpty(sheetId, zone) {
            return this.getters.isEmpty(sheetId, { ...zone, top: zone.bottom });
        }
        /**
         * Decides which dimensions (columns or rows) should be summed
         * based on its shape and what's inside the zone.
         */
        dimensionsToSum(sheetId, zone) {
            const dimensions = new Set();
            if (isOneDimensional(zone)) {
                dimensions.add(zoneToDimension(zone).numberOfCols === 1 ? "COL" : "ROW");
                return dimensions;
            }
            if (this.lastColIsEmpty(sheetId, zone)) {
                dimensions.add("ROW");
            }
            if (this.lastRowIsEmpty(sheetId, zone)) {
                dimensions.add("COL");
            }
            if (dimensions.size === 0) {
                dimensions.add("COL");
            }
            return dimensions;
        }
        /**
         * Sum each column and/or row in the zone in the appropriate cells,
         * depending on the available space.
         */
        sumDimensions(sheetId, zone, dimensions) {
            return [
                ...(dimensions.has("COL") ? this.sumColumns(zone, sheetId) : []),
                ...(dimensions.has("ROW") ? this.sumRows(zone, sheetId) : []),
            ];
        }
        /**
         * Sum the total of the zone in the bottom right cell, assuming
         * the last row contains summed columns.
         */
        sumTotal(zone) {
            const { bottom, right } = zone;
            return {
                position: { col: right, row: bottom },
                zone: { ...zone, top: bottom, right: right - 1 },
            };
        }
        sumColumns(zone, sheetId) {
            const target = this.nextEmptyRow(sheetId, { ...zone, bottom: zone.bottom - 1 });
            zone = { ...zone, bottom: Math.min(zone.bottom, target.bottom - 1) };
            return positions(target).map((position) => ({
                position,
                zone: { ...zone, right: position.col, left: position.col },
            }));
        }
        sumRows(zone, sheetId) {
            const target = this.nextEmptyCol(sheetId, { ...zone, right: zone.right - 1 });
            zone = { ...zone, right: Math.min(zone.right, target.right - 1) };
            return positions(target).map((position) => ({
                position,
                zone: { ...zone, top: position.row, bottom: position.row },
            }));
        }
        dispatchCellUpdates(sheetId, sums) {
            for (const sum of sums) {
                const { col, row } = sum.position;
                this.dispatch("UPDATE_CELL", {
                    sheetId,
                    col,
                    row,
                    content: `=SUM(${this.getters.zoneToXC(sheetId, sum.zone)})`,
                });
            }
        }
        /**
         * Find the first row where all cells below the zone are empty.
         */
        nextEmptyRow(sheetId, zone) {
            let start = zone.bottom + 1;
            const { left, right } = zone;
            while (!this.getters.isEmpty(sheetId, { bottom: start, top: start, left, right })) {
                start++;
            }
            return {
                ...zone,
                top: start,
                bottom: start,
            };
        }
        /**
         * Find the first column where all cells right of the zone are empty.
         */
        nextEmptyCol(sheetId, zone) {
            let start = zone.right + 1;
            const { top, bottom } = zone;
            while (!this.getters.isEmpty(sheetId, { left: start, right: start, top, bottom })) {
                start++;
            }
            return {
                ...zone,
                left: start,
                right: start,
            };
        }
        /**
         * Transpose the given dimensions.
         * COL becomes ROW
         * ROW becomes COL
         */
        transpose(dimensions) {
            return new Set([...dimensions.values()].map((dimension) => (dimension === "COL" ? "ROW" : "COL")));
        }
    }

    /**
     * Plugin managing the display of components next to cells.
     */
    class CellPopoverPlugin extends UIPlugin {
        static getters = [
            "getCellPopover",
            "getPersistentPopoverTypeAtPosition",
            "hasOpenedPopover",
        ];
        persistentPopover;
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "OPEN_CELL_POPOVER":
                    try {
                        cellPopoverRegistry.get(cmd.popoverType);
                    }
                    catch (error) {
                        return "InvalidCellPopover" /* CommandResult.InvalidCellPopover */;
                    }
                    return "Success" /* CommandResult.Success */;
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "ACTIVATE_SHEET":
                    this.persistentPopover = undefined;
                    break;
                case "OPEN_CELL_POPOVER":
                    this.persistentPopover = {
                        col: cmd.col,
                        row: cmd.row,
                        sheetId: this.getters.getActiveSheetId(),
                        type: cmd.popoverType,
                    };
                    break;
                case "CLOSE_CELL_POPOVER":
                    this.persistentPopover = undefined;
                    break;
            }
        }
        getCellPopover({ col, row }) {
            const sheetId = this.getters.getActiveSheetId();
            if (this.persistentPopover && this.getters.isVisibleInViewport(this.persistentPopover)) {
                const position = this.getters.getMainCellPosition(this.persistentPopover);
                const popover = cellPopoverRegistry
                    .get(this.persistentPopover.type)
                    .onOpen?.(position, this.getters);
                return !popover?.isOpen
                    ? { isOpen: false }
                    : {
                        ...popover,
                        anchorRect: this.computePopoverAnchorRect(this.persistentPopover),
                    };
            }
            if (col === undefined ||
                row === undefined ||
                !this.getters.isVisibleInViewport({ sheetId, col, row })) {
                return { isOpen: false };
            }
            const position = this.getters.getMainCellPosition({ sheetId, col, row });
            const popover = cellPopoverRegistry
                .getAll()
                .map((matcher) => matcher.onHover?.(position, this.getters))
                .find((popover) => popover?.isOpen);
            return !popover?.isOpen
                ? { isOpen: false }
                : {
                    ...popover,
                    anchorRect: this.computePopoverAnchorRect(position),
                };
        }
        hasOpenedPopover() {
            return this.persistentPopover !== undefined;
        }
        getPersistentPopoverTypeAtPosition({ col, row }) {
            if (this.persistentPopover &&
                this.persistentPopover.col === col &&
                this.persistentPopover.row === row) {
                return this.persistentPopover.type;
            }
            return undefined;
        }
        computePopoverAnchorRect({ col, row }) {
            const sheetId = this.getters.getActiveSheetId();
            const merge = this.getters.getMerge({ sheetId, col, row });
            if (merge) {
                return this.getters.getVisibleRect(merge);
            }
            return this.getters.getVisibleRect(positionToZone({ col, row }));
        }
    }

    /**
     * This is a generic event bus based on the Owl event bus.
     * This bus however ensures type safety across events and subscription callbacks.
     */
    class EventBus {
        subscriptions = {};
        /**
         * Add a listener for the 'eventType' events.
         *
         * Note that the 'owner' of this event can be anything, but will more likely
         * be a component or a class. The idea is that the callback will be called with
         * the proper owner bound.
         *
         * Also, the owner should be kind of unique. This will be used to remove the
         * listener.
         */
        on(type, owner, callback) {
            if (!callback) {
                throw new Error("Missing callback");
            }
            if (!this.subscriptions[type]) {
                this.subscriptions[type] = [];
            }
            this.subscriptions[type].push({
                owner,
                callback,
            });
        }
        /**
         * Emit an event of type 'eventType'.  Any extra arguments will be passed to
         * the listeners callback.
         */
        trigger(type, payload) {
            const subs = this.subscriptions[type] || [];
            for (let i = 0, iLen = subs.length; i < iLen; i++) {
                const sub = subs[i];
                sub.callback.call(sub.owner, payload);
            }
        }
        /**
         * Remove a listener
         */
        off(eventType, owner) {
            const subs = this.subscriptions[eventType];
            if (subs) {
                this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner);
            }
        }
        /**
         * Remove all subscriptions.
         */
        clear() {
            this.subscriptions = {};
        }
    }

    /*
     * This file contains the specifics transformations
     */
    otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["ADD_COLUMNS_ROWS"], addHeadersTransformation);
    otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["ADD_COLUMNS_ROWS"], addHeadersTransformation);
    otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["CREATE_CHART", "UPDATE_CHART"], updateChartRangesTransformation);
    otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["CREATE_CHART", "UPDATE_CHART"], updateChartRangesTransformation);
    otRegistry.addTransformation("DELETE_SHEET", ["MOVE_RANGES"], transformTargetSheetId);
    otRegistry.addTransformation("DELETE_FIGURE", ["UPDATE_FIGURE", "UPDATE_CHART"], updateChartFigure);
    otRegistry.addTransformation("CREATE_SHEET", ["CREATE_SHEET"], createSheetTransformation);
    otRegistry.addTransformation("ADD_MERGE", ["ADD_MERGE", "REMOVE_MERGE", "CREATE_FILTER_TABLE"], mergeTransformation);
    otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["FREEZE_COLUMNS", "FREEZE_ROWS"], freezeTransformation);
    otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["FREEZE_COLUMNS", "FREEZE_ROWS"], freezeTransformation);
    otRegistry.addTransformation("CREATE_FILTER_TABLE", ["CREATE_FILTER_TABLE", "ADD_MERGE"], createTableTransformation);
    otRegistry.addTransformation("ADD_COLUMNS_ROWS", ["GROUP_HEADERS", "UNGROUP_HEADERS", "FOLD_HEADER_GROUP", "UNFOLD_HEADER_GROUP"], groupHeadersTransformation);
    otRegistry.addTransformation("REMOVE_COLUMNS_ROWS", ["GROUP_HEADERS", "UNGROUP_HEADERS", "FOLD_HEADER_GROUP", "UNFOLD_HEADER_GROUP"], groupHeadersTransformation);
    function transformTargetSheetId(toTransform, executed) {
        const deletedSheetId = executed.sheetId;
        if (toTransform.targetSheetId === deletedSheetId || toTransform.sheetId === deletedSheetId) {
            return undefined;
        }
        return toTransform;
    }
    function updateChartFigure(toTransform, executed) {
        if (toTransform.id === executed.id) {
            return undefined;
        }
        return toTransform;
    }
    function updateChartRangesTransformation(toTransform, executed) {
        return {
            ...toTransform,
            definition: transformDefinition(toTransform.definition, executed),
        };
    }
    function createSheetTransformation(toTransform, executed) {
        if (toTransform.name === executed.name) {
            return {
                ...toTransform,
                name: toTransform.name?.match(/\d+/)
                    ? toTransform.name.replace(/\d+/, (n) => (parseInt(n) + 1).toString())
                    : `${toTransform.name}~`,
                position: toTransform.position + 1,
            };
        }
        return toTransform;
    }
    function mergeTransformation(toTransform, executed) {
        if (toTransform.sheetId !== executed.sheetId) {
            return toTransform;
        }
        const target = [];
        for (const zone1 of toTransform.target) {
            for (const zone2 of executed.target) {
                if (!overlap(zone1, zone2)) {
                    target.push({ ...zone1 });
                }
            }
        }
        if (target.length) {
            return { ...toTransform, target };
        }
        return undefined;
    }
    function freezeTransformation(toTransform, executed) {
        if (toTransform.sheetId !== executed.sheetId) {
            return toTransform;
        }
        const dimension = toTransform.type === "FREEZE_COLUMNS" ? "COL" : "ROW";
        if (dimension !== executed.dimension) {
            return toTransform;
        }
        let quantity = toTransform["quantity"];
        if (executed.type === "REMOVE_COLUMNS_ROWS") {
            const executedElements = [...executed.elements].sort((a, b) => b - a);
            for (let removedElement of executedElements) {
                if (quantity > removedElement) {
                    quantity--;
                }
            }
        }
        if (executed.type === "ADD_COLUMNS_ROWS") {
            const executedBase = executed.position === "before" ? executed.base - 1 : executed.base;
            quantity = quantity > executedBase ? quantity + executed.quantity : quantity;
        }
        return quantity > 0 ? { ...toTransform, quantity } : undefined;
    }
    /**
     * Cancel CREATE_FILTER_TABLE and ADD_MERGE commands if they overlap a filter
     */
    function createTableTransformation(toTransform, executed) {
        if (toTransform.sheetId !== executed.sheetId) {
            return toTransform;
        }
        for (const cmdTarget of toTransform.target) {
            for (const executedCmdTarget of executed.target) {
                if (overlap(executedCmdTarget, cmdTarget)) {
                    return undefined;
                }
            }
        }
        return toTransform;
    }
    /**
     * Transform ADD_COLUMNS_ROWS command if some headers were added/removed
     */
    function addHeadersTransformation(toTransform, executed) {
        if (toTransform.sheetId !== executed.sheetId || toTransform.dimension !== executed.dimension) {
            return toTransform;
        }
        let result = undefined;
        if (executed.type === "REMOVE_COLUMNS_ROWS") {
            result = moveHeaderIndexesOnHeaderDeletion(executed.elements, [toTransform.base])[0];
        }
        else if (executed.type === "ADD_COLUMNS_ROWS") {
            const base = getAddHeaderStartIndex(executed.position, executed.base);
            result = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, [toTransform.base])[0];
        }
        if (result === undefined) {
            return undefined;
        }
        return { ...toTransform, base: result };
    }
    /**
     * Transform header group command if some headers were added/removed
     */
    function groupHeadersTransformation(toTransform, executed) {
        if (toTransform.sheetId !== executed.sheetId || toTransform.dimension !== executed.dimension) {
            return toTransform;
        }
        const elementsToTransform = range(toTransform.start, toTransform.end + 1);
        let results = [];
        if (executed.type === "REMOVE_COLUMNS_ROWS") {
            results = moveHeaderIndexesOnHeaderDeletion(executed.elements, elementsToTransform);
        }
        else if (executed.type === "ADD_COLUMNS_ROWS") {
            const base = getAddHeaderStartIndex(executed.position, executed.base);
            results = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, elementsToTransform);
        }
        if (results.length === 0) {
            return undefined;
        }
        return { ...toTransform, start: Math.min(...results), end: Math.max(...results) };
    }

    const transformations = [
        { match: isSheetDependent, fn: transformSheetId },
        { match: isTargetDependent, fn: transformTarget },
        { match: isZoneDependent, fn: transformZoneDependentCommand },
        { match: isPositionDependent, fn: transformPosition },
        { match: isHeadersDependant, fn: transformHeaders },
        { match: isRangeDependant, fn: transformRangesDependentCommand },
    ];
    /**
     * Get the result of applying the operation transformations on the given command
     * to transform based on the executed command.
     * Let's see a small example:
     * Given
     *  - command A: set the content of C1 to "Hello"
     *  - command B: add a column after A
     *
     * If command B has been executed locally and not transmitted (yet) to
     * other clients, and command A arrives from an other client to be executed locally.
     * Command A is no longer valid and no longer reflects the user intention.
     * It needs to be transformed knowing that command B is already executed.
     * transform(A, B) => set the content of D1 to "Hello"
     */
    function transform(toTransform, executed) {
        const specificTransform = otRegistry.getTransformation(toTransform.type, executed.type);
        return specificTransform
            ? specificTransform(toTransform, executed)
            : genericTransform(toTransform, executed);
    }
    /**
     * Get the result of applying the operation transformations on all the given
     * commands to transform for each executed commands.
     */
    function transformAll(toTransform, executed) {
        let transformedCommands = [...toTransform];
        for (const executedCommand of executed) {
            transformedCommands = transformedCommands
                .map((cmd) => transform(cmd, executedCommand))
                .filter(isDefined$1);
        }
        return transformedCommands;
    }
    /**
     * Apply all generic transformation based on the characteristic of the given commands.
     */
    function genericTransform(cmd, executed) {
        for (const { match, fn } of transformations) {
            if (match(cmd)) {
                const result = fn(cmd, executed);
                if (result === "SKIP_TRANSFORMATION") {
                    continue;
                }
                if (result === "IGNORE_COMMAND") {
                    return undefined;
                }
                cmd = result;
            }
        }
        return cmd;
    }
    function transformSheetId(toTransform, executed) {
        if (!("sheetId" in executed)) {
            return toTransform;
        }
        const deleteSheet = executed.type === "DELETE_SHEET" && executed.sheetId;
        if (toTransform.sheetId === deleteSheet) {
            return "IGNORE_COMMAND";
        }
        else if (toTransform.type === "CREATE_SHEET" ||
            executed.type === "CREATE_SHEET" ||
            toTransform.sheetId !== executed.sheetId) {
            return toTransform;
        }
        return "SKIP_TRANSFORMATION";
    }
    function transformTarget(cmd, executed) {
        const transformSheetResult = transformSheetId(cmd, executed);
        if (transformSheetResult !== "SKIP_TRANSFORMATION") {
            return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : cmd;
        }
        const target = [];
        for (const zone of cmd.target) {
            const newZone = transformZone(zone, executed);
            if (newZone) {
                target.push(newZone);
            }
        }
        if (!target.length) {
            return "IGNORE_COMMAND";
        }
        return { ...cmd, target };
    }
    function transformZoneDependentCommand(cmd, executed) {
        const transformSheetResult = transformSheetId(cmd, executed);
        if (transformSheetResult !== "SKIP_TRANSFORMATION") {
            return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : cmd;
        }
        const newZone = transformZone(cmd.zone, executed);
        if (newZone) {
            return { ...cmd, zone: newZone };
        }
        return "IGNORE_COMMAND";
    }
    function transformRangesDependentCommand(toTransform, executed) {
        if (!("sheetId" in executed)) {
            return toTransform;
        }
        const ranges = toTransform.ranges
            .map((range) => transformRangeData(range, executed))
            .filter(isDefined$1);
        if (!ranges.length) {
            return "IGNORE_COMMAND";
        }
        return { ...toTransform, ranges };
    }
    function transformHeaders(toTransform, executed) {
        const transformSheetResult = transformSheetId(toTransform, executed);
        if (transformSheetResult !== "SKIP_TRANSFORMATION") {
            return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : toTransform;
        }
        if (executed.type !== "ADD_COLUMNS_ROWS" && executed.type !== "REMOVE_COLUMNS_ROWS") {
            return "SKIP_TRANSFORMATION";
        }
        if (executed.dimension !== toTransform.dimension) {
            return toTransform;
        }
        let result = [];
        if (executed.type === "REMOVE_COLUMNS_ROWS") {
            result = moveHeaderIndexesOnHeaderDeletion(executed.elements, toTransform.elements);
        }
        else if (executed.type === "ADD_COLUMNS_ROWS") {
            const base = getAddHeaderStartIndex(executed.position, executed.base);
            result = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, toTransform.elements);
        }
        if (result.length === 0) {
            return "IGNORE_COMMAND";
        }
        return { ...toTransform, elements: result };
    }
    /**
     * Transform a PositionDependentCommand. It could be impacted by a grid command
     * (Add/remove cols/rows) and a merge
     */
    function transformPosition(toTransform, executed) {
        const transformSheetResult = transformSheetId(toTransform, executed);
        if (transformSheetResult !== "SKIP_TRANSFORMATION") {
            return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : toTransform;
        }
        if (executed.type === "ADD_COLUMNS_ROWS" || executed.type === "REMOVE_COLUMNS_ROWS") {
            return transformPositionWithGrid(toTransform, executed);
        }
        if (executed.type === "ADD_MERGE") {
            return transformPositionWithMerge(toTransform, executed);
        }
        return "SKIP_TRANSFORMATION";
    }
    /**
     * Transform a PositionDependentCommand after a grid shape modification. This
     * transformation consists of updating the position.
     */
    function transformPositionWithGrid(toTransform, executed) {
        const field = executed.dimension === "COL" ? "col" : "row";
        let base = toTransform[field];
        if (executed.type === "REMOVE_COLUMNS_ROWS") {
            const elements = [...executed.elements].sort((a, b) => b - a);
            if (elements.includes(base)) {
                return "IGNORE_COMMAND";
            }
            for (let removedElement of elements) {
                if (base >= removedElement) {
                    base--;
                }
            }
        }
        if (executed.type === "ADD_COLUMNS_ROWS") {
            if (base > executed.base || (base === executed.base && executed.position === "before")) {
                base = base + executed.quantity;
            }
        }
        return { ...toTransform, [field]: base };
    }
    /**
     * Transform a PositionDependentCommand after a merge. This transformation
     * consists of checking that the position is not inside the merged zones
     */
    function transformPositionWithMerge(toTransform, executed) {
        for (const zone of executed.target) {
            const sameTopLeft = toTransform.col === zone.left && toTransform.row === zone.top;
            if (!sameTopLeft && isInside(toTransform.col, toTransform.row, zone)) {
                return "IGNORE_COMMAND";
            }
        }
        return toTransform;
    }

    class Revision {
        rootCommand;
        timestamp;
        id;
        clientId;
        _commands = [];
        _changes = [];
        /**
         * A revision represents a whole client action (Create a sheet, merge a Zone, Undo, ...).
         * A revision contains the following information:
         *  - id: ID of the revision
         *  - commands: CoreCommands that are linked to the action, and should be
         *              dispatched in other clients
         *  - clientId: Client who initiated the action
         *  - changes: List of changes applied on the state.
         */
        constructor(id, clientId, commands, rootCommand, changes, timestamp) {
            this.rootCommand = rootCommand;
            this.timestamp = timestamp;
            this.id = id;
            this.clientId = clientId;
            this._commands = [...commands];
            this._changes = changes ? [...changes] : [];
        }
        setChanges(changes) {
            this._changes = changes;
        }
        get commands() {
            return this._commands;
        }
        get changes() {
            return this._changes;
        }
    }

    class ClientDisconnectedError extends Error {
    }
    class Session extends EventBus {
        revisions;
        transportService;
        serverRevisionId;
        /**
         * Positions of the others client.
         */
        clients = {};
        clientId = "local";
        /**
         * Id of the server revision
         */
        debouncedMove;
        pendingMessages = [];
        waitingAck = false;
        /**
         * Flag used to block all commands when an undo or redo is triggered, until
         * it is accepted on the server
         */
        waitingUndoRedoAck = false;
        isReplayingInitialRevisions = false;
        processedRevisions = new Set();
        uuidGenerator = new UuidGenerator();
        lastLocalOperation;
        /**
         * Manages the collaboration between multiple users on the same spreadsheet.
         * It can forward local state changes to other users to ensure they all eventually
         * reach the same state.
         * It also manages the positions of each clients in the spreadsheet to provide
         * a visual indication of what other users are doing in the spreadsheet.
         *
         * @param revisions
         * @param transportService communication channel used to send and receive messages
         * between all connected clients
         * @param client the client connected locally
         * @param serverRevisionId
         */
        constructor(revisions, transportService, serverRevisionId = DEFAULT_REVISION_ID) {
            super();
            this.revisions = revisions;
            this.transportService = transportService;
            this.serverRevisionId = serverRevisionId;
            this.debouncedMove = debounce(this._move.bind(this), DEBOUNCE_TIME);
        }
        canApplyOptimisticUpdate() {
            return !this.waitingUndoRedoAck;
        }
        /**
         * Add a new revision to the collaborative session.
         * It will be transmitted to all other connected clients.
         */
        save(rootCommand, commands, changes) {
            if (!commands.length || !changes.length || !this.canApplyOptimisticUpdate())
                return;
            const revision = new Revision(this.uuidGenerator.uuidv4(), this.clientId, commands, rootCommand, changes, Date.now());
            this.revisions.append(revision.id, revision);
            // REQUEST_REDO just repeats the last operation, the
            // last operation is still the same and should not change.
            if (rootCommand.type !== "REQUEST_REDO") {
                this.lastLocalOperation = revision;
            }
            this.trigger("new-local-state-update", { id: revision.id });
            this.sendUpdateMessage({
                type: "REMOTE_REVISION",
                version: MESSAGE_VERSION,
                serverRevisionId: this.serverRevisionId,
                nextRevisionId: revision.id,
                clientId: revision.clientId,
                commands: revision.commands,
            });
        }
        undo(revisionId) {
            this.waitingUndoRedoAck = true;
            this.sendUpdateMessage({
                type: "REVISION_UNDONE",
                version: MESSAGE_VERSION,
                serverRevisionId: this.serverRevisionId,
                nextRevisionId: this.uuidGenerator.uuidv4(),
                undoneRevisionId: revisionId,
            });
        }
        redo(revisionId) {
            this.waitingUndoRedoAck = true;
            this.sendUpdateMessage({
                type: "REVISION_REDONE",
                version: MESSAGE_VERSION,
                serverRevisionId: this.serverRevisionId,
                nextRevisionId: this.uuidGenerator.uuidv4(),
                redoneRevisionId: revisionId,
            });
        }
        /**
         * Notify that the position of the client has changed
         */
        move(position) {
            this.debouncedMove(position);
        }
        join(client) {
            if (client) {
                this.clients[client.id] = client;
                this.clientId = client.id;
            }
            else {
                this.clients["local"] = { id: "local", name: "local" };
                this.clientId = "local";
            }
            this.transportService.onNewMessage(this.clientId, this.onMessageReceived.bind(this));
        }
        loadInitialMessages(messages) {
            this.isReplayingInitialRevisions = true;
            for (const message of messages) {
                this.onMessageReceived(message);
            }
            this.isReplayingInitialRevisions = false;
        }
        /**
         * Notify the server that the user client left the collaborative session
         */
        leave() {
            delete this.clients[this.clientId];
            this.transportService.leave(this.clientId);
            this.transportService.sendMessage({
                type: "CLIENT_LEFT",
                clientId: this.clientId,
                version: MESSAGE_VERSION,
            });
        }
        /**
         * Send a snapshot of the spreadsheet to the collaboration server
         */
        snapshot(data) {
            const snapshotId = this.uuidGenerator.uuidv4();
            this.transportService.sendMessage({
                type: "SNAPSHOT",
                nextRevisionId: snapshotId,
                serverRevisionId: this.serverRevisionId,
                data: { ...data, revisionId: snapshotId },
                version: MESSAGE_VERSION,
            });
        }
        getClient() {
            const client = this.clients[this.clientId];
            if (!client) {
                throw new ClientDisconnectedError("The client left the session");
            }
            return client;
        }
        getConnectedClients() {
            return new Set(Object.values(this.clients).filter(isDefined$1));
        }
        getRevisionId() {
            return this.serverRevisionId;
        }
        isFullySynchronized() {
            return this.pendingMessages.length === 0;
        }
        /**
         * Get the last local revision
         * */
        getLastLocalNonEmptyRevision() {
            return this.lastLocalOperation;
        }
        _move(position) {
            // this method is debounced and might be called after the client
            // left the session.
            if (!this.clients[this.clientId])
                return;
            const currentPosition = this.clients[this.clientId]?.position;
            if (currentPosition?.col === position.col &&
                currentPosition.row === position.row &&
                currentPosition.sheetId === position.sheetId) {
                return;
            }
            const type = currentPosition ? "CLIENT_MOVED" : "CLIENT_JOINED";
            const client = this.getClient();
            this.clients[this.clientId] = { ...client, position };
            this.transportService.sendMessage({
                type,
                version: MESSAGE_VERSION,
                client: { ...client, position },
            });
        }
        /**
         * Handles messages received from other clients in the collaborative
         * session.
         */
        onMessageReceived(message) {
            if (this.isAlreadyProcessed(message))
                return;
            if (this.isWrongServerRevisionId(message)) {
                this.trigger("unexpected-revision-id");
                return;
            }
            switch (message.type) {
                case "CLIENT_MOVED":
                    this.onClientMoved(message);
                    break;
                case "CLIENT_JOINED":
                    this.onClientJoined(message);
                    break;
                case "CLIENT_LEFT":
                    this.onClientLeft(message);
                    break;
                case "REVISION_REDONE": {
                    this.revisions.redo(message.redoneRevisionId, message.nextRevisionId, message.serverRevisionId);
                    this.trigger("revision-redone", {
                        revisionId: message.redoneRevisionId,
                        commands: this.revisions.get(message.redoneRevisionId).commands,
                    });
                    break;
                }
                case "REVISION_UNDONE":
                    this.revisions.undo(message.undoneRevisionId, message.nextRevisionId, message.serverRevisionId);
                    this.trigger("revision-undone", {
                        revisionId: message.undoneRevisionId,
                        commands: this.revisions.get(message.undoneRevisionId).commands,
                    });
                    break;
                case "REMOTE_REVISION":
                    const { clientId, commands, timestamp } = message;
                    const revision = new Revision(message.nextRevisionId, clientId, commands, undefined, undefined, timestamp);
                    if (revision.clientId !== this.clientId) {
                        this.revisions.insert(revision.id, revision, message.serverRevisionId);
                        const pendingCommands = this.pendingMessages
                            .filter((msg) => msg.type === "REMOTE_REVISION")
                            .map((msg) => msg.commands)
                            .flat();
                        this.trigger("remote-revision-received", {
                            commands: transformAll(commands, pendingCommands),
                        });
                    }
                    break;
                case "SNAPSHOT_CREATED": {
                    const revision = new Revision(message.nextRevisionId, "server", [], undefined, undefined, Date.now());
                    this.revisions.insert(revision.id, revision, message.serverRevisionId);
                    this.dropPendingHistoryMessages();
                    this.trigger("snapshot");
                    this.lastLocalOperation = undefined;
                    break;
                }
            }
            this.acknowledge(message);
            this.trigger("collaborative-event-received");
        }
        onClientMoved(message) {
            if (message.client.id !== this.clientId) {
                this.clients[message.client.id] = message.client;
            }
        }
        /**
         * Register the new client and send your
         * own position back.
         */
        onClientJoined(message) {
            if (message.client.id !== this.clientId) {
                this.clients[message.client.id] = message.client;
                const client = this.clients[this.clientId];
                if (client) {
                    const { position } = client;
                    if (position) {
                        this.transportService.sendMessage({
                            type: "CLIENT_MOVED",
                            version: MESSAGE_VERSION,
                            client: { ...client, position },
                        });
                    }
                }
            }
        }
        onClientLeft(message) {
            if (message.clientId !== this.clientId) {
                delete this.clients[message.clientId];
            }
        }
        sendUpdateMessage(message) {
            this.pendingMessages.push(message);
            if (this.waitingAck) {
                return;
            }
            this.waitingAck = true;
            this.sendPendingMessage();
        }
        /**
         * Send the next pending message
         */
        sendPendingMessage() {
            let message = this.pendingMessages[0];
            if (!message)
                return;
            if (message.type === "REMOTE_REVISION") {
                const revision = this.revisions.get(message.nextRevisionId);
                if (revision.commands.length === 0) {
                    /**
                     * The command is empty, we have to drop all the next local revisions
                     * to avoid issues with undo/redo
                     */
                    this.revisions.drop(revision.id);
                    const revisionIds = this.pendingMessages
                        .filter((message) => message.type === "REMOTE_REVISION")
                        .map((message) => message.nextRevisionId);
                    this.trigger("pending-revisions-dropped", { revisionIds });
                    this.waitingAck = false;
                    this.waitingUndoRedoAck = false;
                    this.pendingMessages = [];
                    return;
                }
                message = {
                    ...message,
                    clientId: revision.clientId,
                    commands: revision.commands,
                };
            }
            if (this.isReplayingInitialRevisions) {
                throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.
      ${JSON.stringify(message)}`);
            }
            this.transportService.sendMessage({
                ...message,
                serverRevisionId: this.serverRevisionId,
            });
        }
        acknowledge(message) {
            if (message.type === "REVISION_UNDONE" || message.type === "REVISION_REDONE") {
                this.waitingUndoRedoAck = false;
            }
            switch (message.type) {
                case "REMOTE_REVISION":
                case "REVISION_REDONE":
                case "REVISION_UNDONE":
                case "SNAPSHOT_CREATED":
                    this.waitingAck = false;
                    this.pendingMessages = this.pendingMessages.filter((msg) => msg.nextRevisionId !== message.nextRevisionId);
                    this.serverRevisionId = message.nextRevisionId;
                    this.processedRevisions.add(message.nextRevisionId);
                    this.sendPendingMessage();
                    break;
            }
        }
        isAlreadyProcessed(message) {
            if (message.type === "CLIENT_MOVED" && message.client.id === this.clientId) {
                return true;
            }
            switch (message.type) {
                case "REMOTE_REVISION":
                case "REVISION_REDONE":
                case "REVISION_UNDONE":
                    return this.processedRevisions.has(message.nextRevisionId);
                default:
                    return false;
            }
        }
        isWrongServerRevisionId(message) {
            switch (message.type) {
                case "REMOTE_REVISION":
                case "REVISION_REDONE":
                case "REVISION_UNDONE":
                case "SNAPSHOT_CREATED":
                    return message.serverRevisionId !== this.serverRevisionId;
                default:
                    return false;
            }
        }
        dropPendingHistoryMessages() {
            this.waitingUndoRedoAck = false;
            this.pendingMessages = this.pendingMessages.filter(({ type }) => type !== "REVISION_REDONE" && type !== "REVISION_UNDONE");
        }
    }

    function randomChoice(arr) {
        return arr[Math.floor(Math.random() * arr.length)];
    }
    const colors = [
        "#ff851b",
        "#0074d9",
        "#7fdbff",
        "#b10dc9",
        "#39cccc",
        "#f012be",
        "#3d9970",
        "#111111",
        "#ff4136",
        "#aaaaaa",
        "#85144b",
        "#001f3f",
    ];
    class CollaborativePlugin extends UIPlugin {
        static getters = [
            "getClientsToDisplay",
            "getClient",
            "getConnectedClients",
            "isFullySynchronized",
        ];
        static layers = [6 /* LAYERS.Selection */];
        availableColors = new Set(colors);
        colors = {};
        session;
        constructor(config) {
            super(config);
            this.session = config.session;
        }
        isPositionValid(position) {
            return (position.row < this.getters.getNumberRows(position.sheetId) &&
                position.col < this.getters.getNumberCols(position.sheetId));
        }
        chooseNewColor() {
            if (this.availableColors.size === 0) {
                this.availableColors = new Set(colors);
            }
            const color = randomChoice([...this.availableColors.values()]);
            this.availableColors.delete(color);
            return color;
        }
        getClient() {
            return this.session.getClient();
        }
        getConnectedClients() {
            return this.session.getConnectedClients();
        }
        isFullySynchronized() {
            return this.session.isFullySynchronized();
        }
        /**
         * Get the list of others connected clients which are present in the same sheet
         * and with a valid position
         */
        getClientsToDisplay() {
            try {
                this.getters.getClient();
            }
            catch (e) {
                if (e instanceof ClientDisconnectedError) {
                    return [];
                }
                else {
                    throw e;
                }
            }
            const sheetId = this.getters.getActiveSheetId();
            const clients = [];
            for (const client of this.getters.getConnectedClients()) {
                if (client.id !== this.getters.getClient().id &&
                    client.position &&
                    client.position.sheetId === sheetId &&
                    this.isPositionValid(client.position)) {
                    const position = client.position;
                    if (!this.colors[client.id]) {
                        this.colors[client.id] = this.chooseNewColor();
                    }
                    const color = this.colors[client.id];
                    clients.push({ ...client, position, color });
                }
            }
            return clients;
        }
        drawGrid(renderingContext) {
            if (this.getters.isDashboard()) {
                return;
            }
            const { ctx, thinLineWidth } = renderingContext;
            const activeSheetId = this.getters.getActiveSheetId();
            for (const client of this.getClientsToDisplay()) {
                const { row, col } = client.position;
                const zone = this.getters.expandZone(activeSheetId, {
                    top: row,
                    bottom: row,
                    left: col,
                    right: col,
                });
                const { x, y, width, height } = this.getters.getVisibleRect(zone);
                if (width <= 0 || height <= 0) {
                    continue;
                }
                const color = client.color;
                /* Cell background */
                const cellBackgroundColor = `${color}10`;
                ctx.fillStyle = cellBackgroundColor;
                ctx.lineWidth = 4 * thinLineWidth;
                ctx.strokeStyle = color;
                ctx.globalCompositeOperation = "multiply";
                ctx.fillRect(x, y, width, height);
                /* Cell border */
                ctx.globalCompositeOperation = "source-over";
                ctx.strokeRect(x, y, width, height);
                /* client name background */
                ctx.font = `bold ${DEFAULT_FONT_SIZE + 1}px ${DEFAULT_FONT}`;
            }
        }
    }

    /** Abstract state of the clipboard when copying/cutting content that is pasted in cells of the sheet */
    class ClipboardCellsAbstractState {
        getters;
        dispatch;
        selection;
        operation;
        sheetId;
        constructor(operation, getters, dispatch, selection) {
            this.getters = getters;
            this.dispatch = dispatch;
            this.selection = selection;
            this.operation = operation;
            this.sheetId = getters.getActiveSheetId();
        }
        isCutAllowed(target) {
            return "Success" /* CommandResult.Success */;
        }
        isPasteAllowed(target, clipboardOption) {
            return "Success" /* CommandResult.Success */;
        }
        /**
         * Add columns and/or rows to ensure that col + width and row + height are still
         * in the sheet
         */
        addMissingDimensions(width, height, col, row) {
            const sheetId = this.getters.getActiveSheetId();
            const missingRows = height + row - this.getters.getNumberRows(sheetId);
            if (missingRows > 0) {
                this.dispatch("ADD_COLUMNS_ROWS", {
                    dimension: "ROW",
                    base: this.getters.getNumberRows(sheetId) - 1,
                    sheetId,
                    quantity: missingRows,
                    position: "after",
                });
            }
            const missingCols = width + col - this.getters.getNumberCols(sheetId);
            if (missingCols > 0) {
                this.dispatch("ADD_COLUMNS_ROWS", {
                    dimension: "COL",
                    base: this.getters.getNumberCols(sheetId) - 1,
                    sheetId,
                    quantity: missingCols,
                    position: "after",
                });
            }
        }
        isColRowDirtyingClipboard(position, dimension) {
            return false;
        }
        drawClipboard(renderingContext) { }
    }

    /** State of the clipboard when copying/cutting cells */
    class ClipboardCellsState extends ClipboardCellsAbstractState {
        cells;
        copiedTables;
        zones;
        uuidGenerator = new UuidGenerator();
        constructor(zones, operation, getters, dispatch, selection) {
            super(operation, getters, dispatch, selection);
            if (!zones.length) {
                this.cells = [[]];
                this.zones = [];
                this.copiedTables = [];
                return;
            }
            const lefts = new Set(zones.map((z) => z.left));
            const rights = new Set(zones.map((z) => z.right));
            const tops = new Set(zones.map((z) => z.top));
            const bottoms = new Set(zones.map((z) => z.bottom));
            const areZonesCompatible = (tops.size === 1 && bottoms.size === 1) || (lefts.size === 1 && rights.size === 1);
            // In order to don't paste several times the same cells in intersected zones
            // --> we merge zones that have common cells
            const clippedZones = areZonesCompatible
                ? mergeOverlappingZones(zones)
                : [zones[zones.length - 1]];
            const cellsPosition = clippedZones.map((zone) => positions(zone)).flat();
            const columnsIndex = [...new Set(cellsPosition.map((p) => p.col))].sort((a, b) => a - b);
            const rowsIndex = [...new Set(cellsPosition.map((p) => p.row))].sort((a, b) => a - b);
            const cellsInClipboard = [];
            const sheetId = getters.getActiveSheetId();
            for (let row of rowsIndex) {
                let cellsInRow = [];
                for (let col of columnsIndex) {
                    const position = { col, row, sheetId };
                    const spreader = getters.getArrayFormulaSpreadingOn(position);
                    let cell = getters.getCell(position);
                    const evaluatedCell = getters.getEvaluatedCell(position);
                    if (spreader) {
                        const isSpreaderCopied = rowsIndex.includes(spreader.row) && columnsIndex.includes(spreader.col);
                        const content = isSpreaderCopied
                            ? ""
                            : formatValue(evaluatedCell.value, { locale: getters.getLocale() });
                        cell = {
                            id: cell?.id || "",
                            style: cell?.style,
                            format: evaluatedCell.format,
                            content,
                            isFormula: false,
                        };
                    }
                    cellsInRow.push({
                        cell,
                        border: getters.getCellBorder(position) || undefined,
                        evaluatedCell,
                        position,
                    });
                }
                cellsInClipboard.push(cellsInRow);
            }
            const tables = [];
            for (const zone of zones) {
                for (const table of this.getters.getFilterTablesInZone(sheetId, zone)) {
                    const values = [];
                    for (const col of range(table.zone.left, table.zone.right + 1)) {
                        values.push(this.getters.getFilterValues({ sheetId, col, row: table.zone.top }));
                    }
                    tables.push({ filtersValues: values, zone: table.zone });
                }
            }
            this.cells = cellsInClipboard;
            this.zones = clippedZones;
            this.copiedTables = tables;
        }
        isCutAllowed(target) {
            if (target.length !== 1) {
                return "WrongCutSelection" /* CommandResult.WrongCutSelection */;
            }
            return "Success" /* CommandResult.Success */;
        }
        isPasteAllowed(target, clipboardOption) {
            const sheetId = this.getters.getActiveSheetId();
            if (this.operation === "CUT" && clipboardOption?.pasteOption !== undefined) {
                // cannot paste only format or only value if the previous operation is a CUT
                return "WrongPasteOption" /* CommandResult.WrongPasteOption */;
            }
            if (target.length > 1) {
                // cannot paste if we have a clipped zone larger than a cell and multiple
                // zones selected
                if (this.cells.length > 1 || this.cells[0].length > 1) {
                    return "WrongPasteSelection" /* CommandResult.WrongPasteSelection */;
                }
            }
            const clipboardHeight = this.cells.length;
            const clipboardWidth = this.cells[0].length;
            for (let zone of this.getPasteZones(target)) {
                if (this.getters.doesIntersectMerge(sheetId, zone)) {
                    if (target.length > 1 ||
                        !this.getters.isSingleCellOrMerge(sheetId, target[0]) ||
                        clipboardHeight * clipboardWidth !== 1) {
                        return "WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */;
                    }
                }
            }
            const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
            for (const zone of this.getPasteZones(target)) {
                if ((zone.left < xSplit && zone.right >= xSplit) ||
                    (zone.top < ySplit && zone.bottom >= ySplit)) {
                    return "FrozenPaneOverlap" /* CommandResult.FrozenPaneOverlap */;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        /**
         * Paste the clipboard content in the given target
         */
        paste(target, options) {
            if (this.operation === "COPY") {
                this.pasteFromCopy(target, options);
            }
            else {
                this.pasteFromCut(target, options);
            }
            const height = this.cells.length;
            const width = this.cells[0].length;
            const isCutOperation = this.operation === "CUT";
            if (options?.selectTarget) {
                this.selectPastedZone(width, height, isCutOperation, target);
            }
        }
        pasteFromCopy(target, options) {
            if (target.length === 1) {
                // in this specific case, due to the isPasteAllowed function:
                // state.cells can contains several cells.
                // So if the target zone is larger than the copied zone,
                // we duplicate each cells as many times as possible to fill the zone.
                const height = this.cells.length;
                const width = this.cells[0].length;
                const pasteZones = this.pastedZones(target, width, height);
                for (const zone of pasteZones) {
                    this.pasteZone(zone.left, zone.top, options);
                }
            }
            else {
                // in this case, due to the isPasteAllowed function: state.cells contains
                // only one cell
                for (const zone of target) {
                    for (let col = zone.left; col <= zone.right; col++) {
                        for (let row = zone.top; row <= zone.bottom; row++) {
                            this.pasteZone(col, row, options);
                        }
                    }
                }
            }
            if (options?.pasteOption === undefined) {
                this.pasteCopiedTables(target);
            }
        }
        pasteFromCut(target, options) {
            this.clearClippedZones();
            const selection = target[0];
            this.pasteZone(selection.left, selection.top, options);
            this.dispatch("MOVE_RANGES", {
                target: this.zones,
                sheetId: this.sheetId,
                targetSheetId: this.getters.getActiveSheetId(),
                col: selection.left,
                row: selection.top,
            });
            for (const filterTable of this.copiedTables) {
                this.dispatch("REMOVE_FILTER_TABLE", {
                    sheetId: this.sheetId,
                    target: [filterTable.zone],
                });
            }
            this.pasteCopiedTables(target);
        }
        /**
         * The clipped zone is copied as many times as it fits in the target.
         * This returns the list of zones where the clipped zone is copy-pasted.
         */
        pastedZones(target, originWidth, originHeight) {
            const selection = target[0];
            const repeatHorizontally = Math.max(1, Math.floor((selection.right + 1 - selection.left) / originWidth));
            const repeatVertically = Math.max(1, Math.floor((selection.bottom + 1 - selection.top) / originHeight));
            const zones = [];
            for (let x = 0; x < repeatHorizontally; x++) {
                for (let y = 0; y < repeatVertically; y++) {
                    const top = selection.top + y * originHeight;
                    const left = selection.left + x * originWidth;
                    zones.push({
                        left,
                        top,
                        bottom: top + originHeight - 1,
                        right: left + originWidth - 1,
                    });
                }
            }
            return zones;
        }
        /**
         * Compute the complete zones where to paste the current clipboard
         */
        getPasteZones(target) {
            const cells = this.cells;
            if (!cells.length || !cells[0].length) {
                return target;
            }
            const pasteZones = [];
            const height = cells.length;
            const width = cells[0].length;
            const selection = target[target.length - 1];
            const col = selection.left;
            const row = selection.top;
            const repetitionCol = Math.max(1, Math.floor((selection.right + 1 - col) / width));
            const repetitionRow = Math.max(1, Math.floor((selection.bottom + 1 - row) / height));
            for (let x = 1; x <= repetitionCol; x++) {
                for (let y = 1; y <= repetitionRow; y++) {
                    pasteZones.push({
                        left: col,
                        top: row,
                        right: col - 1 + x * width,
                        bottom: row - 1 + y * height,
                    });
                }
            }
            return pasteZones;
        }
        /**
         * Update the selection with the newly pasted zone
         */
        selectPastedZone(width, height, isCutOperation, target) {
            const selection = target[0];
            const col = selection.left;
            const row = selection.top;
            if (height > 1 || width > 1 || isCutOperation) {
                const zones = this.pastedZones(target, width, height);
                const newZone = isCutOperation ? zones[0] : union(...zones);
                this.selection.selectZone({ cell: { col, row }, zone: newZone }, { scrollIntoView: false });
            }
        }
        /**
         * Clear the clipped zones: remove the cells and clear the formatting
         */
        clearClippedZones() {
            for (const row of this.cells) {
                for (const cell of row) {
                    if (cell?.cell) {
                        this.dispatch("CLEAR_CELL", cell.position);
                    }
                }
            }
            this.dispatch("CLEAR_FORMATTING", {
                sheetId: this.sheetId,
                target: this.zones,
            });
        }
        pasteZone(col, row, clipboardOptions) {
            const height = this.cells.length;
            const width = this.cells[0].length;
            // This condition is used to determine if we have to paste the CF or not.
            // We have to do it when the command handled is "PASTE", not "INSERT_CELL"
            // or "DELETE_CELL". So, the state should be the local state
            const shouldPasteCF = clipboardOptions?.pasteOption !== "onlyValue" && clipboardOptions?.shouldPasteCF;
            const shouldPasteDV = !clipboardOptions?.pasteOption;
            const sheetId = this.getters.getActiveSheetId();
            // first, add missing cols/rows if needed
            this.addMissingDimensions(width, height, col, row);
            // then, perform the actual paste operation
            for (let r = 0; r < height; r++) {
                const rowCells = this.cells[r];
                for (let c = 0; c < width; c++) {
                    const origin = rowCells[c];
                    if (!origin) {
                        continue;
                    }
                    const position = { col: col + c, row: row + r, sheetId: sheetId };
                    // TODO: refactor this part. the "Paste merge" action is also executed with
                    // MOVE_RANGES in pasteFromCut. Adding a condition on the operation type here
                    // is not appropriate
                    if (this.operation !== "CUT") {
                        this.pasteMergeIfExist(origin.position, position);
                    }
                    this.pasteCell(origin, position, this.operation, clipboardOptions);
                    if (shouldPasteCF) {
                        this.pasteCf(origin.position, position);
                    }
                    if (shouldPasteDV) {
                        this.pasteDataValidation(origin.position, position);
                    }
                }
            }
        }
        /**
         * Paste the cell at the given position to the target position
         */
        pasteCell(origin, target, operation, clipboardOption) {
            const { sheetId, col, row } = target;
            const targetCell = this.getters.getEvaluatedCell(target);
            if (clipboardOption?.pasteOption === "onlyValue") {
                const locale = this.getters.getLocale();
                const content = formatValue(origin.evaluatedCell.value, { locale });
                this.dispatch("UPDATE_CELL", { ...target, content });
                return;
            }
            const targetBorders = this.getters.getCellBorder(target);
            const originBorders = origin.border;
            const border = {
                top: targetBorders?.top || originBorders?.top,
                bottom: targetBorders?.bottom || originBorders?.bottom,
                left: targetBorders?.left || originBorders?.left,
                right: targetBorders?.right || originBorders?.right,
            };
            this.dispatch("SET_BORDER", { sheetId, col, row, border });
            if (clipboardOption?.pasteOption === "onlyFormat") {
                this.dispatch("UPDATE_CELL", {
                    ...target,
                    style: origin.cell?.style ?? null,
                    format: origin.cell?.format ?? origin.evaluatedCell.format ?? targetCell.format,
                });
                return;
            }
            const content = origin.cell && origin.cell.isFormula && operation === "COPY"
                ? this.getters.getTranslatedCellFormula(sheetId, col - origin.position.col, row - origin.position.row, origin.cell.compiledFormula)
                : origin.cell?.content;
            if (content !== "" || origin.cell?.format || origin.cell?.style) {
                this.dispatch("UPDATE_CELL", {
                    ...target,
                    content,
                    style: origin.cell?.style || null,
                    format: origin.cell?.format,
                });
            }
            else if (targetCell) {
                this.dispatch("CLEAR_CELL", target);
            }
        }
        /**
         * If the origin position given is the top left of a merge, merge the target
         * position.
         */
        pasteMergeIfExist(origin, target) {
            let { sheetId, col, row } = origin;
            const { col: mainCellColOrigin, row: mainCellRowOrigin } = this.getters.getMainCellPosition(origin);
            if (mainCellColOrigin === col && mainCellRowOrigin === row) {
                const merge = this.getters.getMerge(origin);
                if (!merge) {
                    return;
                }
                ({ sheetId, col, row } = target);
                this.dispatch("ADD_MERGE", {
                    sheetId,
                    force: true,
                    target: [
                        {
                            left: col,
                            top: row,
                            right: col + merge.right - merge.left,
                            bottom: row + merge.bottom - merge.top,
                        },
                    ],
                });
            }
        }
        /** Paste the filter tables that are in the state */
        pasteCopiedTables(target) {
            const sheetId = this.getters.getActiveSheetId();
            const selection = target[0];
            const cutZone = this.zones[0];
            const cutOffset = [
                selection.left - cutZone.left,
                selection.top - cutZone.top,
            ];
            for (const table of this.copiedTables) {
                const newTableZone = createAdaptedZone(table.zone, "both", "MOVE", cutOffset);
                this.dispatch("CREATE_FILTER_TABLE", { sheetId, target: [newTableZone] });
                for (const i of range(0, table.filtersValues.length)) {
                    this.dispatch("UPDATE_FILTER", {
                        sheetId,
                        col: newTableZone.left + i,
                        row: newTableZone.top,
                        hiddenValues: table.filtersValues[i],
                    });
                }
            }
        }
        getClipboardContent() {
            return {
                [ClipboardMIMEType.PlainText]: this.getPlainTextContent(),
                [ClipboardMIMEType.Html]: this.getHTMLContent(),
            };
        }
        getPlainTextContent() {
            return (this.cells
                .map((cells) => {
                return cells
                    .map((c) => this.getters.shouldShowFormulas() && c?.cell?.isFormula
                    ? c.cell?.content || ""
                    : c?.evaluatedCell?.formattedValue || "")
                    .join("\t");
            })
                .join("\n") || "\t");
        }
        getHTMLContent() {
            if (this.cells.length === 1 && this.cells[0].length === 1) {
                if (!this.cells[0][0]) {
                    return "";
                }
                return this.getters.getCellText(this.cells[0][0].position);
            }
            let htmlTable = '<table border="1" style="border-collapse:collapse">';
            for (const row of this.cells) {
                htmlTable += "<tr>";
                for (const cell of row) {
                    if (!cell) {
                        continue;
                    }
                    const cssStyle = cssPropertiesToCss(cellStyleToCss(this.getters.getCellComputedStyle(cell.position)));
                    const cellText = this.getters.getCellText(cell.position);
                    htmlTable += `<td style="${cssStyle}">` + xmlEscape(cellText) + "</td>";
                }
                htmlTable += "</tr>";
            }
            htmlTable += "</table>";
            return htmlTable;
        }
        isColRowDirtyingClipboard(position, dimension) {
            if (!this.zones) {
                return false;
            }
            for (let zone of this.zones) {
                if (dimension === "COL" && position <= zone.right) {
                    return true;
                }
                if (dimension === "ROW" && position <= zone.bottom) {
                    return true;
                }
            }
            return false;
        }
        drawClipboard(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            if (this.sheetId !== this.getters.getActiveSheetId() || !this.zones || !this.zones.length) {
                return;
            }
            ctx.setLineDash([8, 5]);
            ctx.strokeStyle = SELECTION_BORDER_COLOR;
            ctx.lineWidth = 3.3 * thinLineWidth;
            for (const zone of this.zones) {
                const { x, y, width, height } = this.getters.getVisibleRect(zone);
                if (width > 0 && height > 0) {
                    ctx.strokeRect(x, y, width, height);
                }
            }
        }
        pasteCf(origin, target) {
            const xc = toXC(target.col, target.row);
            for (let rule of this.getters.getConditionalFormats(origin.sheetId)) {
                for (let range of rule.ranges) {
                    if (isInside(origin.col, origin.row, this.getters.getRangeFromSheetXC(origin.sheetId, range).zone)) {
                        const cf = rule;
                        const toRemoveRange = [];
                        if (this.operation === "CUT") {
                            //remove from current rule
                            toRemoveRange.push(toXC(origin.col, origin.row));
                        }
                        if (origin.sheetId === target.sheetId) {
                            this.adaptCFRules(origin.sheetId, cf, [xc], toRemoveRange);
                        }
                        else {
                            this.adaptCFRules(origin.sheetId, cf, [], toRemoveRange);
                            const cfToCopyTo = this.getCFToCopyTo(target.sheetId, cf);
                            this.adaptCFRules(target.sheetId, cfToCopyTo, [xc], []);
                        }
                    }
                }
            }
        }
        /**
         * Add or remove cells to a given conditional formatting rule.
         */
        adaptCFRules(sheetId, cf, toAdd, toRemove) {
            const newRangesXC = this.getters.getAdaptedCfRanges(sheetId, cf, toAdd, toRemove);
            if (!newRangesXC) {
                return;
            }
            if (newRangesXC.length === 0) {
                this.dispatch("REMOVE_CONDITIONAL_FORMAT", { id: cf.id, sheetId });
                return;
            }
            this.dispatch("ADD_CONDITIONAL_FORMAT", {
                cf: {
                    id: cf.id,
                    rule: cf.rule,
                    stopIfTrue: cf.stopIfTrue,
                },
                ranges: newRangesXC.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
                sheetId,
            });
        }
        getCFToCopyTo(targetSheetId, originCF) {
            const cfInTarget = this.getters
                .getConditionalFormats(targetSheetId)
                .find((cf) => cf.stopIfTrue === originCF.stopIfTrue && deepEquals(cf.rule, originCF.rule));
            return cfInTarget ? cfInTarget : { ...originCF, id: this.uuidGenerator.uuidv4(), ranges: [] };
        }
        pasteDataValidation(origin, target) {
            const rule = this.getters.getValidationRuleForCell(origin);
            if (!rule) {
                return;
            }
            const xc = toXC(target.col, target.row);
            for (const range of rule.ranges) {
                if (isInside(origin.col, origin.row, range.zone)) {
                    const toRemoveRange = [];
                    if (this.operation === "CUT") {
                        toRemoveRange.push(toXC(origin.col, origin.row));
                    }
                    if (origin.sheetId === target.sheetId) {
                        this.adaptDataValidationRule(origin.sheetId, rule, [xc], toRemoveRange);
                    }
                    else {
                        this.adaptDataValidationRule(origin.sheetId, rule, [], toRemoveRange);
                        let copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule);
                        this.adaptDataValidationRule(target.sheetId, copyToRule, [xc], []);
                    }
                }
            }
        }
        getDataValidationRuleToCopyTo(targetSheetId, originRule) {
            const ruleInTargetSheet = this.getters
                .getDataValidationRules(targetSheetId)
                .find((rule) => deepEquals(originRule.criterion, rule.criterion) &&
                originRule.isBlocking === rule.isBlocking);
            return ruleInTargetSheet
                ? ruleInTargetSheet
                : { ...originRule, id: this.uuidGenerator.uuidv4(), ranges: [] };
        }
        /**
         * Add or remove XCs to a given data validation rule.
         */
        adaptDataValidationRule(sheetId, rule, toAdd, toRemove) {
            const dvRangesXcs = rule.ranges.map((range) => this.getters.getRangeString(range, sheetId));
            const newRangesXC = recomputeZones([...dvRangesXcs, ...toAdd], toRemove);
            if (newRangesXC.length === 0) {
                this.dispatch("REMOVE_DATA_VALIDATION_RULE", { sheetId, id: rule.id });
                return;
            }
            this.dispatch("ADD_DATA_VALIDATION_RULE", {
                rule,
                ranges: newRangesXC.map((xc) => this.getters.getRangeDataFromXc(sheetId, xc)),
                sheetId,
            });
        }
    }

    class DataCleanupPlugin extends UIPlugin {
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "REMOVE_DUPLICATES":
                    return this.checkValidations(cmd, this.chainValidations(this.checkSingleRangeSelected, this.checkNoMergeInZone, this.checkRangeContainsValues, this.checkColumnsIncludedInZone), this.chainValidations(this.checkNoColumnProvided, this.checkColumnsAreUnique));
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "REMOVE_DUPLICATES":
                    this.removeDuplicates(cmd.columns, cmd.hasHeader);
                    break;
                case "TRIM_WHITESPACE":
                    this.trimWhitespace();
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        removeDuplicates(columnsToAnalyze, hasHeader) {
            const sheetId = this.getters.getActiveSheetId();
            const zone = this.getters.getSelectedZone();
            if (hasHeader) {
                zone.top += 1;
            }
            const uniqueRowsIndexes = this.getUniqueRowsIndexes(sheetId, zone.top, zone.bottom, columnsToAnalyze);
            const numberOfUniqueRows = uniqueRowsIndexes.length;
            if (numberOfUniqueRows === zoneToDimension(zone).numberOfRows) {
                this.notifyRowsRemovedAndRemaining(0, numberOfUniqueRows);
                return;
            }
            const rowsToKeep = uniqueRowsIndexes.map((rowIndex) => ({
                left: zone.left,
                top: rowIndex,
                right: zone.right,
                bottom: rowIndex,
            }));
            const state = new ClipboardCellsState(rowsToKeep, "COPY", this.getters, this.dispatch, this.selection);
            for (const { col, row } of positions(zone)) {
                this.dispatch("CLEAR_CELL", { col, row, sheetId });
            }
            const zonePasted = {
                left: zone.left,
                top: zone.top,
                right: zone.left,
                bottom: zone.top,
            };
            state.paste([zonePasted]);
            const remainingZone = {
                left: zone.left,
                top: zone.top - (hasHeader ? 1 : 0),
                right: zone.right,
                bottom: zone.top + numberOfUniqueRows - 1,
            };
            this.selection.selectZone({
                cell: { col: remainingZone.left, row: remainingZone.top },
                zone: remainingZone,
            });
            const removedRows = zone.bottom - zone.top + 1 - numberOfUniqueRows;
            this.notifyRowsRemovedAndRemaining(removedRows, numberOfUniqueRows);
        }
        getUniqueRowsIndexes(sheetId, top, bottom, columns) {
            const uniqueRows = new Map();
            for (const row of range(top, bottom + 1)) {
                const cellsValuesInRow = columns.map((col) => {
                    return this.getters.getEvaluatedCell({
                        sheetId,
                        col,
                        row,
                    }).value;
                });
                const isRowUnique = !Object.values(uniqueRows).some((uniqueRow) => deepEquals(uniqueRow, cellsValuesInRow));
                if (isRowUnique) {
                    uniqueRows[row] = cellsValuesInRow;
                }
            }
            // transform key object in number
            return Object.keys(uniqueRows).map((key) => parseInt(key));
        }
        notifyRowsRemovedAndRemaining(removedRows, remainingRows) {
            this.ui.notifyUI({
                type: "info",
                text: _t("%s duplicate rows found and removed.\n%s unique rows remain.", removedRows.toString(), remainingRows.toString()),
                sticky: false,
            });
        }
        checkSingleRangeSelected() {
            const zones = this.getters.getSelectedZones();
            if (zones.length !== 1) {
                return "MoreThanOneRangeSelected" /* CommandResult.MoreThanOneRangeSelected */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkNoMergeInZone() {
            const sheetId = this.getters.getActiveSheetId();
            const zone = this.getters.getSelectedZone();
            const mergesInZone = this.getters.getMergesInZone(sheetId, zone);
            if (mergesInZone.length > 0) {
                return "WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkRangeContainsValues(cmd) {
            const sheetId = this.getters.getActiveSheetId();
            const zone = this.getters.getSelectedZone();
            if (cmd.hasHeader) {
                zone.top += 1;
            }
            const evaluatedCells = this.getters.getEvaluatedCellsInZone(sheetId, zone);
            if (evaluatedCells.every((evaluatedCel) => evaluatedCel.type === "empty")) {
                return "EmptyTarget" /* CommandResult.EmptyTarget */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkNoColumnProvided(cmd) {
            if (cmd.columns.length === 0) {
                return "NoColumnsProvided" /* CommandResult.NoColumnsProvided */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkColumnsIncludedInZone(cmd) {
            const zone = this.getters.getSelectedZone();
            const columnsToAnalyze = cmd.columns;
            if (columnsToAnalyze.some((colIndex) => colIndex < zone.left || colIndex > zone.right)) {
                return "ColumnsNotIncludedInZone" /* CommandResult.ColumnsNotIncludedInZone */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkColumnsAreUnique(cmd) {
            if (cmd.columns.length !== new Set(cmd.columns).size) {
                return "DuplicatesColumnsSelected" /* CommandResult.DuplicatesColumnsSelected */;
            }
            return "Success" /* CommandResult.Success */;
        }
        trimWhitespace() {
            const zones = this.getters.getSelectedZones();
            const sheetId = this.getters.getActiveSheetId();
            let count = 0;
            for (const { col, row } of zones.map(positions).flat()) {
                const cell = this.getters.getCell({ col, row, sheetId });
                if (!cell) {
                    continue;
                }
                const trimmedContent = trimContent(cell.content);
                if (trimmedContent !== cell.content) {
                    count += 1;
                    this.dispatch("UPDATE_CELL", {
                        sheetId,
                        col,
                        row,
                        content: trimmedContent,
                    });
                }
            }
            const text = count
                ? _t("Trimmed whitespace from %s cells.", count)
                : _t("No selected cells had whitespace trimmed.");
            this.ui.notifyUI({
                type: "info",
                text: text,
                sticky: false,
            });
        }
    }

    const BORDER_COLOR = "#8B008B";
    const BACKGROUND_COLOR = "#8B008B33";
    var Direction;
    (function (Direction) {
        Direction[Direction["previous"] = -1] = "previous";
        Direction[Direction["current"] = 0] = "current";
        Direction[Direction["next"] = 1] = "next";
    })(Direction || (Direction = {}));
    /**
     * Find and Replace Plugin
     *
     * This plugin is used in combination with the find_and_replace sidePanel
     * It is used to 'highlight' cells that match an input string according to
     * the given searchOptions. The second part of this plugin makes it possible
     * (again with the find_and_replace sidePanel), to replace the values that match
     * the search with a new value.
     */
    class FindAndReplacePlugin extends UIPlugin {
        static layers = [3 /* LAYERS.Search */];
        static getters = ["getSearchMatches", "getCurrentSelectedMatchIndex"];
        searchMatches = [];
        selectedMatchIndex = null;
        currentSearchRegex = null;
        searchOptions = {
            matchCase: false,
            exactMatch: false,
            searchFormulas: false,
        };
        toSearch = "";
        isSearchDirty = false;
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handle(cmd) {
            switch (cmd.type) {
                case "UPDATE_SEARCH":
                    this.updateSearch(cmd.toSearch, cmd.searchOptions);
                    break;
                case "CLEAR_SEARCH":
                    this.clearSearch();
                    break;
                case "SELECT_SEARCH_PREVIOUS_MATCH":
                    this.selectNextCell(Direction.previous);
                    break;
                case "SELECT_SEARCH_NEXT_MATCH":
                    this.selectNextCell(Direction.next);
                    break;
                case "REPLACE_SEARCH":
                    this.replace(cmd.replaceWith);
                    break;
                case "REPLACE_ALL_SEARCH":
                    this.replaceAll(cmd.replaceWith);
                    break;
                case "UNDO":
                case "REDO":
                case "REMOVE_FILTER_TABLE":
                case "UPDATE_FILTER":
                case "REMOVE_COLUMNS_ROWS":
                case "HIDE_COLUMNS_ROWS":
                case "UNHIDE_COLUMNS_ROWS":
                case "ADD_COLUMNS_ROWS":
                case "EVALUATE_CELLS":
                case "UPDATE_CELL":
                    this.isSearchDirty = true;
                    break;
                case "ACTIVATE_SHEET":
                    this.refreshSearch();
                    break;
            }
        }
        finalize() {
            if (this.isSearchDirty) {
                this.refreshSearch();
                this.isSearchDirty = false;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getSearchMatches() {
            return this.searchMatches;
        }
        getCurrentSelectedMatchIndex() {
            return this.selectedMatchIndex;
        }
        // ---------------------------------------------------------------------------
        // Search
        // ---------------------------------------------------------------------------
        /**
         * Will update the current searchOptions and accordingly update the regex.
         * It will then search for matches using the regex and store them.
         */
        updateSearch(toSearch, searchOptions) {
            this.searchOptions = searchOptions;
            if (toSearch !== this.toSearch) {
                this.selectedMatchIndex = null;
            }
            this.toSearch = toSearch;
            this.updateRegex();
            this.refreshSearch();
        }
        /**
         * refresh the matches according to the current search options
         */
        refreshSearch() {
            const matches = this.findMatches();
            this.searchMatches = matches;
            this.selectNextCell(Direction.current);
        }
        /**
         * Updates the regex based on the current searchOptions and
         * the value toSearch
         */
        updateRegex() {
            let searchValue = escapeRegExp(this.toSearch);
            const flags = !this.searchOptions.matchCase ? "i" : "";
            if (this.searchOptions.exactMatch) {
                searchValue = `^${searchValue}$`;
            }
            this.currentSearchRegex = RegExp(searchValue, flags);
        }
        /**
         * Find matches using the current regex
         */
        findMatches() {
            const sheetId = this.getters.getActiveSheetId();
            const cells = this.getters.getCells(sheetId);
            const matches = [];
            if (this.toSearch && this.currentSearchRegex) {
                for (const cell of Object.values(cells)) {
                    const { col, row } = this.getters.getCellPosition(cell.id);
                    const cellPosition = { sheetId, col, row };
                    const isColHidden = this.getters.isColHidden(sheetId, col);
                    const isRowHidden = this.getters.isRowHidden(sheetId, row);
                    if (isColHidden || isRowHidden) {
                        continue;
                    }
                    if (this.currentSearchRegex.test(this.getSearchableString(cellPosition))) {
                        const match = { col, row, selected: false };
                        matches.push(match);
                    }
                    for (const spreadPosition of this.getters.getSpreadPositionsOf(cellPosition)) {
                        if (this.currentSearchRegex.test(this.getSearchableString(spreadPosition))) {
                            const match = {
                                col: spreadPosition.col,
                                row: spreadPosition.row,
                                selected: false,
                            };
                            matches.push(match);
                        }
                    }
                }
            }
            return matches.sort(this.sortByRowThenColumn);
        }
        sortByRowThenColumn(a, b) {
            if (a.row === b.row) {
                return a.col - b.col;
            }
            return a.row > b.row ? 1 : -1;
        }
        /**
         * Changes the selected search cell. Given a direction it will
         * Change the selection to the previous, current or nextCell,
         * if it exists otherwise it will set the selectedMatchIndex to null.
         * It will also reset the index to 0 if the search has changed.
         * It is also used to keep coherence between the selected searchMatch
         * and selectedMatchIndex.
         */
        selectNextCell(indexChange) {
            const matches = this.searchMatches;
            if (!matches.length) {
                this.selectedMatchIndex = null;
                return;
            }
            let nextIndex;
            if (this.selectedMatchIndex === null) {
                nextIndex = 0;
            }
            else {
                nextIndex = this.selectedMatchIndex + indexChange;
            }
            //modulo of negative value to be able to cycle in both directions with previous and next
            nextIndex = ((nextIndex % matches.length) + matches.length) % matches.length;
            this.selectedMatchIndex = nextIndex;
            this.selection.selectCell(matches[nextIndex].col, matches[nextIndex].row);
            for (let index = 0; index < this.searchMatches.length; index++) {
                this.searchMatches[index].selected = index === this.selectedMatchIndex;
            }
        }
        clearSearch() {
            this.toSearch = "";
            this.searchMatches = [];
            this.selectedMatchIndex = null;
            this.currentSearchRegex = null;
            this.searchOptions = {
                matchCase: false,
                exactMatch: false,
                searchFormulas: false,
            };
        }
        // ---------------------------------------------------------------------------
        // Replace
        // ---------------------------------------------------------------------------
        replaceMatch(selectedMatch, replaceWith) {
            if (!this.currentSearchRegex) {
                return;
            }
            const sheetId = this.getters.getActiveSheetId();
            const position = { sheetId, ...selectedMatch };
            const cell = this.getters.getCell(position);
            if (!cell?.content) {
                return;
            }
            if (cell?.isFormula && !this.searchOptions.searchFormulas) {
                return;
            }
            const replaceRegex = new RegExp(this.currentSearchRegex.source, this.currentSearchRegex.flags + "g");
            const toReplace = this.getSearchableString(position);
            const content = toReplace.replace(replaceRegex, replaceWith);
            const canonicalContent = canonicalizeNumberContent(content, this.getters.getLocale());
            this.dispatch("UPDATE_CELL", { ...position, content: canonicalContent });
        }
        /**
         * Replace the value of the currently selected match
         */
        replace(replaceWith) {
            if (this.selectedMatchIndex === null) {
                return;
            }
            const selectedMatch = this.searchMatches[this.selectedMatchIndex];
            this.replaceMatch(selectedMatch, replaceWith);
            this.selectNextCell(Direction.next);
        }
        /**
         * Apply the replace function to all the matches one time.
         */
        replaceAll(replaceWith) {
            const matchCount = this.searchMatches.length;
            for (let i = 0; i < matchCount; i++) {
                this.replaceMatch(this.searchMatches[i], replaceWith);
            }
        }
        getSearchableString(position) {
            return this.getters.getCellText(position, this.searchOptions.searchFormulas);
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(renderingContext) {
            const { ctx } = renderingContext;
            const sheetId = this.getters.getActiveSheetId();
            for (const match of this.searchMatches) {
                const merge = this.getters.getMerge({ sheetId, col: match.col, row: match.row });
                const left = merge ? merge.left : match.col;
                const right = merge ? merge.right : match.col;
                const top = merge ? merge.top : match.row;
                const bottom = merge ? merge.bottom : match.row;
                const { x, y, width, height } = this.getters.getVisibleRect({ top, left, right, bottom });
                if (width > 0 && height > 0) {
                    ctx.fillStyle = BACKGROUND_COLOR;
                    ctx.fillRect(x, y, width, height);
                    if (match.selected) {
                        ctx.strokeStyle = BORDER_COLOR;
                        ctx.strokeRect(x, y, width, height);
                    }
                }
            }
        }
    }

    class FormatPlugin extends UIPlugin {
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handle(cmd) {
            switch (cmd.type) {
                case "SET_DECIMAL":
                    this.setDecimal(cmd.sheetId, cmd.target, cmd.step);
                    break;
            }
        }
        /**
         * This function allows to adjust the quantity of decimal places after a decimal
         * point on cells containing number value. It does this by changing the cells
         * format. Values aren't modified.
         *
         * The change of the decimal quantity is done one by one, the sign of the step
         * variable indicates whether we are increasing or decreasing.
         *
         * If several cells are in the zone, each cell's format will be individually
         * evaluated and updated with the number type.
         */
        setDecimal(sheetId, zones, step) {
            // Find the each cell with a number value and get the format
            for (const zone of zones) {
                for (const position of positions(zone)) {
                    const numberFormat = this.getCellNumberFormat({ sheetId, ...position });
                    if (numberFormat !== undefined) {
                        // Depending on the step sign, increase or decrease the decimal representation
                        // of the format
                        this.getters.getLocale();
                        const newFormat = changeDecimalPlaces(numberFormat, step);
                        // Apply the new format on the whole zone
                        this.dispatch("SET_FORMATTING", {
                            sheetId,
                            target: [positionToZone(position)],
                            format: newFormat,
                        });
                    }
                }
            }
        }
        /**
         * Take a range of cells and return the format of the first cell containing a
         * number value. Returns a default format if the cell hasn't format. Returns
         * undefined if no number value in the range.
         */
        getCellNumberFormat(position) {
            for (const pos of [position]) {
                const cell = this.getters.getEvaluatedCell(pos);
                if (cell.type === CellValueType.number &&
                    !(cell.format && isDateTimeFormat(cell.format)) // reject dates
                ) {
                    return cell.format || createDefaultFormat(cell.value);
                }
            }
            return undefined;
        }
    }

    class HeaderVisibilityUIPlugin extends UIPlugin {
        static getters = [
            "getNextVisibleCellPosition",
            "findVisibleHeader",
            "findLastVisibleColRowIndex",
            "findFirstVisibleColRowIndex",
            "isRowHidden",
            "isColHidden",
            "isHeaderHidden",
        ];
        isRowHidden(sheetId, index) {
            return (this.getters.isRowHiddenByUser(sheetId, index) || this.getters.isRowFiltered(sheetId, index));
        }
        isColHidden(sheetId, index) {
            return this.getters.isColHiddenByUser(sheetId, index);
        }
        isHeaderHidden(sheetId, dimension, index) {
            return dimension === "COL"
                ? this.isColHidden(sheetId, index)
                : this.isRowHidden(sheetId, index);
        }
        getNextVisibleCellPosition({ sheetId, col, row }) {
            return {
                sheetId,
                col: this.findVisibleHeader(sheetId, "COL", col, this.getters.getNumberCols(sheetId) - 1),
                row: this.findVisibleHeader(sheetId, "ROW", row, this.getters.getNumberRows(sheetId) - 1),
            };
        }
        /**
         * Find the first visible header in the range [`from` => `to`].
         *
         * Both `from` and `to` are inclusive.
         */
        findVisibleHeader(sheetId, dimension, from, to) {
            if (from <= to) {
                for (let i = from; i <= to; i++) {
                    if (this.getters.doesHeaderExist(sheetId, dimension, i) &&
                        !this.isHeaderHidden(sheetId, dimension, i)) {
                        return i;
                    }
                }
            }
            if (from > to) {
                for (let i = from; i >= to; i--) {
                    if (this.getters.doesHeaderExist(sheetId, dimension, i) &&
                        !this.isHeaderHidden(sheetId, dimension, i)) {
                        return i;
                    }
                }
            }
            return undefined;
        }
        findLastVisibleColRowIndex(sheetId, dimension, { last, first }) {
            const lastVisibleIndex = range(last, first, -1).find((index) => !this.isHeaderHidden(sheetId, dimension, index));
            return lastVisibleIndex || first;
        }
        findFirstVisibleColRowIndex(sheetId, dimension) {
            const numberOfHeaders = this.getters.getNumberHeaders(sheetId, dimension);
            for (let i = 0; i < numberOfHeaders; i++) {
                if (dimension === "COL" && !this.isColHidden(sheetId, i)) {
                    return i;
                }
                if (dimension === "ROW" && !this.isRowHidden(sheetId, i)) {
                    return i;
                }
            }
            return undefined;
        }
        exportForExcel(data) {
            for (const sheetData of data.sheets) {
                for (const [row, rowData] of Object.entries(sheetData.rows)) {
                    const isHidden = this.isRowHidden(sheetData.id, Number(row));
                    rowData.isHidden = isHidden;
                }
            }
        }
    }

    /**
     * HighlightPlugin
     */
    class HighlightPlugin extends UIPlugin {
        static layers = [1 /* LAYERS.Highlights */];
        static getters = ["getHighlights"];
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getHighlights() {
            return this.prepareHighlights(this.getters.getComposerHighlights().concat(this.getters.getSelectionInputHighlights()));
        }
        // ---------------------------------------------------------------------------
        // Other
        // ---------------------------------------------------------------------------
        prepareHighlights(highlights) {
            return highlights
                .filter((x) => x.zone.top >= 0 &&
                x.zone.left >= 0 &&
                x.zone.bottom < this.getters.getNumberRows(x.sheetId) &&
                x.zone.right < this.getters.getNumberCols(x.sheetId))
                .map((highlight) => {
                const { numberOfRows, numberOfCols } = zoneToDimension(highlight.zone);
                const zone = numberOfRows * numberOfCols === 1
                    ? this.getters.expandZone(highlight.sheetId, highlight.zone)
                    : highlight.zone;
                return {
                    ...highlight,
                    zone,
                };
            });
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(renderingContext) {
            // rendering selection highlights
            const { ctx, thinLineWidth } = renderingContext;
            const sheetId = this.getters.getActiveSheetId();
            const lineWidth = 3 * thinLineWidth;
            ctx.lineWidth = lineWidth;
            /**
             * We only need to draw the highlights of the current sheet.
             *
             * Note that there can be several times the same highlight in 'this.highlights'.
             * In order to avoid superposing the same color layer and modifying the final
             * opacity, we filter highlights to remove duplicates.
             */
            const highlights = this.getHighlights();
            for (let h of highlights.filter((highlight, index) => 
            // For every highlight in the sheet, deduplicated by zone
            highlights.findIndex((h) => isEqual(h.zone, highlight.zone) && h.sheetId === sheetId) ===
                index)) {
                const { x, y, width, height } = this.getters.getVisibleRect(h.zone);
                if (width > 0 && height > 0) {
                    ctx.strokeStyle = h.color;
                    ctx.strokeRect(x + lineWidth / 2, y + lineWidth / 2, width - lineWidth, height - lineWidth);
                    ctx.globalCompositeOperation = "source-over";
                    ctx.fillStyle = h.color + "20";
                    ctx.fillRect(x + lineWidth, y + lineWidth, width - 2 * lineWidth, height - 2 * lineWidth);
                }
            }
        }
    }

    class RendererPlugin extends UIPlugin {
        static layers = [0 /* LAYERS.Background */, 7 /* LAYERS.Headers */];
        static getters = ["getColDimensionsInViewport", "getRowDimensionsInViewport"];
        boxes = [];
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        /**
         * Returns the size, start and end coordinates of a column relative to the left
         * column of the current viewport
         */
        getColDimensionsInViewport(sheetId, col) {
            const left = Math.min(...this.getters.getSheetViewVisibleCols());
            const start = this.getters.getColRowOffsetInViewport("COL", left, col);
            const size = this.getters.getColSize(sheetId, col);
            const isColHidden = this.getters.isColHidden(sheetId, col);
            return {
                start,
                size: size,
                end: start + (isColHidden ? 0 : size),
            };
        }
        /**
         * Returns the size, start and end coordinates of a row relative to the top row
         * of the current viewport
         */
        getRowDimensionsInViewport(sheetId, row) {
            const top = Math.min(...this.getters.getSheetViewVisibleRows());
            const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
            const size = this.getters.getRowSize(sheetId, row);
            const isRowHidden = this.getters.isRowHidden(sheetId, row);
            return {
                start,
                size: size,
                end: start + (isRowHidden ? 0 : size),
            };
        }
        /**
         * Get the offset of a header (see getColRowOffsetInViewport), adjusted with the header
         * size (HEADER_HEIGHT and HEADER_WIDTH)
         */
        getHeaderOffset(dimension, start, index) {
            let size = this.getters.getColRowOffsetInViewport(dimension, start, index);
            if (!this.getters.isDashboard()) {
                size += dimension === "ROW" ? HEADER_HEIGHT : HEADER_WIDTH;
            }
            return size;
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(renderingContext, layer) {
            switch (layer) {
                case 0 /* LAYERS.Background */:
                    this.boxes = this.getGridBoxes();
                    this.drawBackground(renderingContext);
                    this.drawOverflowingCellBackground(renderingContext);
                    this.drawCellBackground(renderingContext);
                    this.drawBorders(renderingContext);
                    this.drawTexts(renderingContext);
                    this.drawIcon(renderingContext);
                    this.drawFrozenPanes(renderingContext);
                    break;
                case 7 /* LAYERS.Headers */:
                    if (!this.getters.isDashboard()) {
                        this.drawHeaders(renderingContext);
                        this.drawFrozenPanesHeaders(renderingContext);
                    }
                    break;
            }
        }
        drawBackground(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            const { width, height } = this.getters.getSheetViewDimensionWithHeaders();
            // white background
            ctx.fillStyle = "#ffffff";
            ctx.fillRect(0, 0, width + CANVAS_SHIFT, height + CANVAS_SHIFT);
            const areGridLinesVisible = !this.getters.isDashboard() &&
                this.getters.getGridLinesVisibility(this.getters.getActiveSheetId());
            const inset = areGridLinesVisible ? 0.1 * thinLineWidth : 0;
            if (areGridLinesVisible) {
                for (const box of this.boxes) {
                    ctx.strokeStyle = CELL_BORDER_COLOR;
                    ctx.lineWidth = thinLineWidth;
                    ctx.strokeRect(box.x + inset, box.y + inset, box.width - 2 * inset, box.height - 2 * inset);
                }
            }
        }
        drawCellBackground(renderingContext) {
            const { ctx } = renderingContext;
            for (const box of this.boxes) {
                let style = box.style;
                if (style.fillColor && style.fillColor !== "#ffffff") {
                    ctx.fillStyle = style.fillColor || "#ffffff";
                    ctx.fillRect(box.x, box.y, box.width, box.height);
                }
                if (box.isError) {
                    ctx.fillStyle = "red";
                    ctx.beginPath();
                    ctx.moveTo(box.x + box.width - 5, box.y);
                    ctx.lineTo(box.x + box.width, box.y);
                    ctx.lineTo(box.x + box.width, box.y + 5);
                    ctx.fill();
                }
            }
        }
        drawOverflowingCellBackground(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            for (const box of this.boxes) {
                if (box.content && box.isOverflow) {
                    const align = box.content.align || "left";
                    let x;
                    let width;
                    const y = box.y + thinLineWidth / 2;
                    const height = box.height - thinLineWidth;
                    const clipWidth = Math.min(box.clipRect?.width || Infinity, box.content.width);
                    if (align === "left") {
                        x = box.x + thinLineWidth / 2;
                        width = clipWidth - 2 * thinLineWidth;
                    }
                    else if (align === "right") {
                        x = box.x + box.width - thinLineWidth / 2;
                        width = -clipWidth + 2 * thinLineWidth;
                    }
                    else {
                        x =
                            (box.clipRect?.x || box.x + box.width / 2 - box.content.width / 2) + thinLineWidth / 2;
                        width = clipWidth - 2 * thinLineWidth;
                    }
                    ctx.fillStyle = "#ffffff";
                    ctx.fillRect(x, y, width, height);
                }
            }
        }
        drawBorders(renderingContext) {
            const { ctx } = renderingContext;
            for (let box of this.boxes) {
                const border = box.border;
                if (border) {
                    const { x, y, width, height } = box;
                    if (border.left) {
                        drawBorder(border.left, x, y, x, y + height);
                    }
                    if (border.top) {
                        drawBorder(border.top, x, y, x + width, y);
                    }
                    if (border.right) {
                        drawBorder(border.right, x + width, y, x + width, y + height);
                    }
                    if (border.bottom) {
                        drawBorder(border.bottom, x, y + height, x + width, y + height);
                    }
                }
            }
            /**
             * Following https://usefulangle.com/post/17/html5-canvas-drawing-1px-crisp-straight-lines,
             * we need to make sure that a "single" pixel line is drawn on a "half" pixel coordinate,
             * while a "double" pixel line is drawn on a "full" pixel coordinate. As, in the rendering
             * process, we always had 0.5 before rendering line (to make sure it is drawn on a "half"
             * pixel), we need to correct this behavior for the "medium" and the "dotted" styles, as
             * they are drawing a two pixels width line.
             * We also adapt here the coordinates of the line to make sure corner are correctly drawn,
             * avoiding a "round corners" effect. This is done by subtracting 1 pixel to the origin of
             * each line and adding 1 pixel to the end of each line (depending on the direction of the
             * line).
             */
            function drawBorder({ style, color }, x1, y1, x2, y2) {
                ctx.strokeStyle = color;
                switch (style) {
                    case "medium":
                        ctx.lineWidth = 2;
                        x1 += y1 === y2 ? -0.5 : 0.5;
                        x2 += y1 === y2 ? 1.5 : 0.5;
                        y1 += x1 === x2 ? -0.5 : 0.5;
                        y2 += x1 === x2 ? 1.5 : 0.5;
                        break;
                    case "thick":
                        ctx.lineWidth = 3;
                        if (y1 === y2) {
                            x1--;
                            x2++;
                        }
                        if (x1 === x2) {
                            y1--;
                            y2++;
                        }
                        break;
                    case "dashed":
                        ctx.lineWidth = 1;
                        ctx.setLineDash([1, 3]);
                        break;
                    case "dotted":
                        ctx.lineWidth = 1;
                        if (y1 === y2) {
                            x1 += 0.5;
                            x2 += 0.5;
                        }
                        if (x1 === x2) {
                            y1 += 0.5;
                            y2 += 0.5;
                        }
                        ctx.setLineDash([1, 1]);
                        break;
                    case "thin":
                    default:
                        ctx.lineWidth = 1;
                        break;
                }
                ctx.beginPath();
                ctx.moveTo(x1, y1);
                ctx.lineTo(x2, y2);
                ctx.stroke();
                ctx.lineWidth = 1;
                ctx.setLineDash([]);
            }
        }
        drawTexts(renderingContext) {
            const { ctx } = renderingContext;
            ctx.textBaseline = "top";
            let currentFont;
            for (let box of this.boxes) {
                if (box.content) {
                    const style = box.style || {};
                    const align = box.content.align || "left";
                    // compute font and textColor
                    const font = computeTextFont(style);
                    if (font !== currentFont) {
                        currentFont = font;
                        ctx.font = font;
                    }
                    ctx.fillStyle = style.textColor || "#000";
                    // compute horizontal align start point parameter
                    let x = box.x;
                    if (align === "left") {
                        x += MIN_CELL_TEXT_MARGIN + (box.image ? box.image.size + MIN_CF_ICON_MARGIN : 0);
                    }
                    else if (align === "right") {
                        x +=
                            box.width -
                                MIN_CELL_TEXT_MARGIN -
                                (box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0);
                    }
                    else {
                        x += box.width / 2;
                    }
                    // horizontal align text direction
                    ctx.textAlign = align;
                    // clip rect if needed
                    if (box.clipRect) {
                        ctx.save();
                        ctx.beginPath();
                        const { x, y, width, height } = box.clipRect;
                        ctx.rect(x, y, width, height);
                        ctx.clip();
                    }
                    // compute vertical align start point parameter:
                    const textLineHeight = computeTextFontSizeInPixels(style);
                    const numberOfLines = box.content.textLines.length;
                    let y = this.computeTextYCoordinate(box, textLineHeight, numberOfLines);
                    // use the horizontal and the vertical start points to:
                    // fill text / fill strikethrough / fill underline
                    for (let brokenLine of box.content.textLines) {
                        drawDecoratedText(ctx, brokenLine, { x: Math.round(x), y: Math.round(y) }, style.underline, style.strikethrough);
                        y += MIN_CELL_TEXT_MARGIN + textLineHeight;
                    }
                    if (box.clipRect) {
                        ctx.restore();
                    }
                }
            }
        }
        drawIcon(renderingContext) {
            const { ctx } = renderingContext;
            for (const box of this.boxes) {
                if (box.image) {
                    const icon = box.image.image;
                    if (box.image.clipIcon) {
                        ctx.save();
                        ctx.beginPath();
                        const { x, y, width, height } = box.image.clipIcon;
                        ctx.rect(x, y, width, height);
                        ctx.clip();
                    }
                    const iconSize = box.image.size;
                    const y = this.computeTextYCoordinate(box, iconSize);
                    ctx.drawImage(icon, box.x + MIN_CF_ICON_MARGIN, y, iconSize, iconSize);
                    if (box.image.clipIcon) {
                        ctx.restore();
                    }
                }
            }
        }
        /** Compute the vertical start point from which a text line should be draw.
         *
         * Note that in case the cell does not have enough spaces to display its text lines,
         * (wrapping cell case) then the vertical align should be at the top.
         * */
        computeTextYCoordinate(box, textLineHeight, numberOfLines = 1) {
            const y = box.y + 1;
            const textHeight = computeTextLinesHeight(textLineHeight, numberOfLines);
            const hasEnoughSpaces = box.height > textHeight + MIN_CELL_TEXT_MARGIN * 2;
            const verticalAlign = box.verticalAlign || DEFAULT_VERTICAL_ALIGN;
            if (hasEnoughSpaces) {
                if (verticalAlign === "middle") {
                    return y + (box.height - textHeight) / 2;
                }
                if (verticalAlign === "bottom") {
                    return y + box.height - textHeight - MIN_CELL_TEXT_MARGIN;
                }
            }
            return y + MIN_CELL_TEXT_MARGIN;
        }
        drawHeaders(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            const visibleCols = this.getters.getSheetViewVisibleCols();
            const left = visibleCols[0];
            const right = visibleCols[visibleCols.length - 1];
            const visibleRows = this.getters.getSheetViewVisibleRows();
            const top = visibleRows[0];
            const bottom = visibleRows[visibleRows.length - 1];
            const { width, height } = this.getters.getSheetViewDimensionWithHeaders();
            const selection = this.getters.getSelectedZones();
            const selectedCols = getZonesCols(selection);
            const selectedRows = getZonesRows(selection);
            const sheetId = this.getters.getActiveSheetId();
            const numberOfCols = this.getters.getNumberCols(sheetId);
            const numberOfRows = this.getters.getNumberRows(sheetId);
            const activeCols = this.getters.getActiveCols();
            const activeRows = this.getters.getActiveRows();
            ctx.font = `400 ${HEADER_FONT_SIZE}px ${DEFAULT_FONT}`;
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.lineWidth = thinLineWidth;
            ctx.strokeStyle = "#333";
            // Columns headers background
            for (let col = left; col <= right; col++) {
                const colZone = { left: col, right: col, top: 0, bottom: numberOfRows - 1 };
                const { x, width } = this.getters.getVisibleRect(colZone);
                const colHasFilter = this.getters.doesZonesContainFilter(sheetId, [colZone]);
                const isColActive = activeCols.has(col);
                const isColSelected = selectedCols.has(col);
                if (isColActive) {
                    ctx.fillStyle = colHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;
                }
                else if (isColSelected) {
                    ctx.fillStyle = colHasFilter
                        ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR
                        : BACKGROUND_HEADER_SELECTED_COLOR;
                }
                else {
                    ctx.fillStyle = colHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;
                }
                ctx.fillRect(x, 0, width, HEADER_HEIGHT);
            }
            // Rows headers background
            for (let row = top; row <= bottom; row++) {
                const rowZone = { top: row, bottom: row, left: 0, right: numberOfCols - 1 };
                const { y, height } = this.getters.getVisibleRect(rowZone);
                const rowHasFilter = this.getters.doesZonesContainFilter(sheetId, [rowZone]);
                const isRowActive = activeRows.has(row);
                const isRowSelected = selectedRows.has(row);
                if (isRowActive) {
                    ctx.fillStyle = rowHasFilter ? FILTERS_COLOR : BACKGROUND_HEADER_ACTIVE_COLOR;
                }
                else if (isRowSelected) {
                    ctx.fillStyle = rowHasFilter
                        ? BACKGROUND_HEADER_SELECTED_FILTER_COLOR
                        : BACKGROUND_HEADER_SELECTED_COLOR;
                }
                else {
                    ctx.fillStyle = rowHasFilter ? BACKGROUND_HEADER_FILTER_COLOR : BACKGROUND_HEADER_COLOR;
                }
                ctx.fillRect(0, y, HEADER_WIDTH, height);
            }
            // 2 main lines
            ctx.beginPath();
            ctx.moveTo(HEADER_WIDTH, 0);
            ctx.lineTo(HEADER_WIDTH, height);
            ctx.moveTo(0, HEADER_HEIGHT);
            ctx.lineTo(width, HEADER_HEIGHT);
            ctx.strokeStyle = HEADER_BORDER_COLOR;
            ctx.stroke();
            ctx.beginPath();
            // column text + separator
            for (const i of visibleCols) {
                const colSize = this.getters.getColSize(sheetId, i);
                const colName = numberToLetters(i);
                ctx.fillStyle = activeCols.has(i) ? "#fff" : TEXT_HEADER_COLOR;
                let colStart = this.getHeaderOffset("COL", left, i);
                ctx.fillText(colName, colStart + colSize / 2, HEADER_HEIGHT / 2);
                ctx.moveTo(colStart + colSize, 0);
                ctx.lineTo(colStart + colSize, HEADER_HEIGHT);
            }
            // row text + separator
            for (const i of visibleRows) {
                const rowSize = this.getters.getRowSize(sheetId, i);
                ctx.fillStyle = activeRows.has(i) ? "#fff" : TEXT_HEADER_COLOR;
                let rowStart = this.getHeaderOffset("ROW", top, i);
                ctx.fillText(String(i + 1), HEADER_WIDTH / 2, rowStart + rowSize / 2);
                ctx.moveTo(0, rowStart + rowSize);
                ctx.lineTo(HEADER_WIDTH, rowStart + rowSize);
            }
            ctx.stroke();
        }
        drawFrozenPanesHeaders(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();
            const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;
            const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;
            ctx.lineWidth = 6 * thinLineWidth;
            ctx.strokeStyle = FROZEN_PANE_HEADER_BORDER_COLOR;
            ctx.beginPath();
            if (offsetCorrectionX) {
                ctx.moveTo(widthCorrection + offsetCorrectionX, 0);
                ctx.lineTo(widthCorrection + offsetCorrectionX, heightCorrection);
            }
            if (offsetCorrectionY) {
                ctx.moveTo(0, heightCorrection + offsetCorrectionY);
                ctx.lineTo(widthCorrection, heightCorrection + offsetCorrectionY);
            }
            ctx.stroke();
        }
        drawFrozenPanes(renderingContext) {
            const { ctx, thinLineWidth } = renderingContext;
            const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();
            const visibleCols = this.getters.getSheetViewVisibleCols();
            const left = visibleCols[0];
            const right = visibleCols[visibleCols.length - 1];
            const visibleRows = this.getters.getSheetViewVisibleRows();
            const top = visibleRows[0];
            const bottom = visibleRows[visibleRows.length - 1];
            const viewport = { left, right, top, bottom };
            const rect = this.getters.getVisibleRect(viewport);
            const widthCorrection = this.getters.isDashboard() ? 0 : HEADER_WIDTH;
            const heightCorrection = this.getters.isDashboard() ? 0 : HEADER_HEIGHT;
            ctx.lineWidth = 6 * thinLineWidth;
            ctx.strokeStyle = FROZEN_PANE_BORDER_COLOR;
            ctx.beginPath();
            if (offsetCorrectionX) {
                ctx.moveTo(widthCorrection + offsetCorrectionX, heightCorrection);
                ctx.lineTo(widthCorrection + offsetCorrectionX, rect.height + heightCorrection);
            }
            if (offsetCorrectionY) {
                ctx.moveTo(widthCorrection, heightCorrection + offsetCorrectionY);
                ctx.lineTo(rect.width + widthCorrection, heightCorrection + offsetCorrectionY);
            }
            ctx.stroke();
        }
        findNextEmptyCol(base, max, row) {
            const sheetId = this.getters.getActiveSheetId();
            let col = base;
            while (col < max) {
                const position = { sheetId, col: col + 1, row };
                const nextCell = this.getters.getEvaluatedCell(position);
                const nextCellBorder = this.getters.getCellBorderWithFilterBorder(position);
                const cellHasIcon = this.getters.doesCellHaveGridIcon(position);
                const cellHasCheckbox = this.getters.isCellValidCheckbox(position);
                if (nextCell.type !== CellValueType.empty ||
                    this.getters.isInMerge(position) ||
                    nextCellBorder?.left ||
                    cellHasIcon ||
                    cellHasCheckbox) {
                    return col;
                }
                col++;
            }
            return col;
        }
        findPreviousEmptyCol(base, min, row) {
            const sheetId = this.getters.getActiveSheetId();
            let col = base;
            while (col > min) {
                const position = { sheetId, col: col - 1, row };
                const previousCell = this.getters.getEvaluatedCell(position);
                const previousCellBorder = this.getters.getCellBorderWithFilterBorder(position);
                const cellHasIcon = this.getters.doesCellHaveGridIcon(position);
                const cellHasCheckbox = this.getters.isCellValidCheckbox(position);
                if (previousCell.type !== CellValueType.empty ||
                    this.getters.isInMerge(position) ||
                    previousCellBorder?.right ||
                    cellHasIcon ||
                    cellHasCheckbox) {
                    return col;
                }
                col--;
            }
            return col;
        }
        computeCellAlignment(position, isOverflowing) {
            const cell = this.getters.getCell(position);
            if (cell?.isFormula && this.getters.shouldShowFormulas()) {
                return "left";
            }
            const { align } = this.getters.getCellStyle(position);
            const evaluatedCell = this.getters.getEvaluatedCell(position);
            if (isOverflowing && evaluatedCell.type === CellValueType.number) {
                return align !== "center" ? "left" : align;
            }
            return align || evaluatedCell.defaultAlign;
        }
        createZoneBox(sheetId, zone, viewport) {
            const { left, right } = viewport;
            const col = zone.left;
            const row = zone.top;
            const position = { sheetId, col, row };
            const cell = this.getters.getEvaluatedCell(position);
            const showFormula = this.getters.shouldShowFormulas();
            const { x, y, width, height } = this.getters.getVisibleRect(zone);
            const { verticalAlign } = this.getters.getCellStyle(position);
            const box = {
                x,
                y,
                width,
                height,
                border: this.getters.getCellBorderWithFilterBorder(position) || undefined,
                style: this.getters.getCellComputedStyle(position),
                verticalAlign,
                isError: (cell.type === CellValueType.error && cell.error.isVerbose) ||
                    this.getters.isDataValidationInvalid(position),
            };
            if (cell.type === CellValueType.empty || this.getters.isCellValidCheckbox(position)) {
                return box;
            }
            /** Icon CF */
            const cfIcon = this.getters.getConditionalIcon(position);
            const fontSizePX = computeTextFontSizeInPixels(box.style);
            const iconBoxWidth = cfIcon ? MIN_CF_ICON_MARGIN + fontSizePX : 0;
            if (cfIcon) {
                box.image = {
                    type: "icon",
                    size: fontSizePX,
                    clipIcon: { x: box.x, y: box.y, width: Math.min(iconBoxWidth, width), height },
                    image: ICONS[cfIcon].img,
                };
            }
            /** Filter Header or data validation icon */
            box.hasIcon = this.getters.doesCellHaveGridIcon(position);
            const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
            /** Content */
            const style = this.getters.getCellComputedStyle(position);
            const wrapping = style.wrapping || "overflow";
            const maxWidth = wrapping === "wrap" && !showFormula ? width - 2 * MIN_CELL_TEXT_MARGIN : undefined;
            const multiLineText = this.getters.getCellMultiLineText(position, maxWidth);
            const textWidth = Math.max(...multiLineText.map((line) => this.getters.getTextWidth(line, style) + MIN_CELL_TEXT_MARGIN));
            const contentWidth = iconBoxWidth + textWidth + headerIconWidth;
            const align = this.computeCellAlignment(position, contentWidth > width);
            box.content = {
                textLines: multiLineText,
                width: wrapping === "overflow" ? textWidth : width,
                align,
            };
            /** ClipRect */
            const isOverflowing = contentWidth > width || fontSizePX > height;
            if (cfIcon || box.hasIcon) {
                box.clipRect = {
                    x: box.x + iconBoxWidth,
                    y: box.y,
                    width: Math.max(0, width - iconBoxWidth - headerIconWidth),
                    height,
                };
            }
            else if (isOverflowing && wrapping === "overflow") {
                let nextColIndex, previousColIndex;
                const isCellInMerge = this.getters.isInMerge(position);
                if (isCellInMerge) {
                    // Always clip merges
                    nextColIndex = this.getters.getMerge(position).right;
                    previousColIndex = col;
                }
                else {
                    nextColIndex = this.findNextEmptyCol(col, right, row);
                    previousColIndex = this.findPreviousEmptyCol(col, left, row);
                    box.isOverflow = true;
                }
                switch (align) {
                    case "left": {
                        const emptyZoneOnTheLeft = positionToZone({ col: nextColIndex, row });
                        const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheLeft));
                        if (width < contentWidth || fontSizePX > height || multiLineText.length > 1) {
                            box.clipRect = { x, y, width, height };
                        }
                        break;
                    }
                    case "right": {
                        const emptyZoneOnTheRight = positionToZone({ col: previousColIndex, row });
                        const { x, y, width, height } = this.getters.getVisibleRect(union(zone, emptyZoneOnTheRight));
                        if (width < contentWidth || fontSizePX > height || multiLineText.length > 1) {
                            box.clipRect = { x, y, width, height };
                        }
                        break;
                    }
                    case "center": {
                        const emptyZone = {
                            ...zone,
                            left: previousColIndex,
                            right: nextColIndex,
                        };
                        const { x, y, height, width } = this.getters.getVisibleRect(emptyZone);
                        const halfContentWidth = contentWidth / 2;
                        const boxMiddle = box.x + box.width / 2;
                        if (x + width < boxMiddle + halfContentWidth ||
                            x > boxMiddle - halfContentWidth ||
                            fontSizePX > height ||
                            multiLineText.length > 1) {
                            const clipX = x > boxMiddle - halfContentWidth ? x : boxMiddle - halfContentWidth;
                            const clipWidth = x + width - clipX;
                            box.clipRect = { x: clipX, y, width: clipWidth, height };
                        }
                        break;
                    }
                }
            }
            else if (wrapping === "clip" || wrapping === "wrap" || multiLineText.length > 1) {
                box.clipRect = {
                    x: box.x,
                    y: box.y,
                    width,
                    height,
                };
            }
            return box;
        }
        getGridBoxes() {
            const boxes = [];
            const visibleCols = this.getters.getSheetViewVisibleCols();
            const left = visibleCols[0];
            const right = visibleCols[visibleCols.length - 1];
            const visibleRows = this.getters.getSheetViewVisibleRows();
            const top = visibleRows[0];
            const bottom = visibleRows[visibleRows.length - 1];
            const viewport = { left, right, top, bottom };
            const sheetId = this.getters.getActiveSheetId();
            for (const row of visibleRows) {
                for (const col of visibleCols) {
                    const position = { sheetId, col, row };
                    if (this.getters.isInMerge(position)) {
                        continue;
                    }
                    boxes.push(this.createZoneBox(sheetId, positionToZone(position), viewport));
                }
            }
            for (const merge of this.getters.getMerges(sheetId)) {
                if (this.getters.isMergeHidden(sheetId, merge)) {
                    continue;
                }
                if (overlap(merge, viewport)) {
                    const box = this.createZoneBox(sheetId, merge, viewport);
                    const borderBottomRight = this.getters.getCellBorder({
                        sheetId,
                        col: merge.right,
                        row: merge.bottom,
                    });
                    box.border = {
                        ...box.border,
                        bottom: borderBottomRight ? borderBottomRight.bottom : undefined,
                        right: borderBottomRight ? borderBottomRight.right : undefined,
                    };
                    box.isMerge = true;
                    boxes.push(box);
                }
            }
            return boxes;
        }
    }

    /**
     * Selection input Plugin
     *
     * The SelectionInput component input and output are both arrays of strings, but
     * it requires an intermediary internal state to work.
     * This plugin handles this internal state.
     */
    class SelectionInputPlugin extends UIPlugin {
        inputHasSingleRange;
        static layers = [1 /* LAYERS.Highlights */];
        static getters = [];
        ranges = [];
        focusedRangeIndex = null;
        inputSheetId;
        constructor(config, initialRanges, inputHasSingleRange) {
            if (inputHasSingleRange && initialRanges.length > 1) {
                throw new Error("Input with a single range cannot be instantiated with several range references.");
            }
            super(config);
            this.inputHasSingleRange = inputHasSingleRange;
            this.insertNewRange(0, initialRanges);
            this.inputSheetId = this.getters.getActiveSheetId();
            if (this.ranges.length === 0) {
                this.insertNewRange(this.ranges.length, [""]);
                this.focusLast();
            }
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handleEvent(event) {
            if (this.focusedRangeIndex === null) {
                return;
            }
            const inputSheetId = this.inputSheetId;
            const activeSheetId = this.getters.getActiveSheetId();
            const zone = event.options.unbounded
                ? this.getters.getUnboundedZone(activeSheetId, event.anchor.zone)
                : event.anchor.zone;
            const range = this.getters.getRangeFromZone(activeSheetId, zone);
            const willAddNewRange = event.mode === "newAnchor" &&
                !this.inputHasSingleRange &&
                this.ranges[this.focusedRangeIndex].xc.trim() !== "";
            if (willAddNewRange) {
                const xc = this.getters.getSelectionRangeString(range, inputSheetId);
                this.insertNewRange(this.ranges.length, [xc]);
                this.focusLast();
            }
            else {
                let parts = range.parts;
                const previousXc = this.ranges[this.focusedRangeIndex].xc.trim();
                if (previousXc) {
                    parts = this.getters.getRangeFromSheetXC(inputSheetId, previousXc).parts;
                }
                const newRange = range.clone({ parts });
                const xc = this.getters.getSelectionRangeString(newRange, inputSheetId);
                this.setRange(this.focusedRangeIndex, [xc]);
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "UNFOCUS_SELECTION_INPUT":
                    this.unfocus();
                    break;
                case "FOCUS_RANGE":
                    this.focus(this.getIndex(cmd.rangeId));
                    break;
                case "CHANGE_RANGE": {
                    const index = this.getIndex(cmd.rangeId);
                    if (index !== null && this.focusedRangeIndex !== index) {
                        this.focus(index);
                    }
                    if (index !== null) {
                        const valueWithoutLeadingComma = cmd.value.replace(/^,+/, "");
                        const values = valueWithoutLeadingComma.split(",").map((reference) => reference.trim());
                        this.setRange(index, values);
                    }
                    break;
                }
                case "ADD_EMPTY_RANGE":
                    this.insertNewRange(this.ranges.length, [""]);
                    this.focusLast();
                    break;
                case "ADD_RANGE":
                    this.insertNewRange(this.ranges.length, [cmd.value]);
                    this.focusLast();
                    break;
                case "REMOVE_RANGE":
                    const index = this.getIndex(cmd.rangeId);
                    if (index !== null) {
                        this.removeRange(index);
                    }
                    break;
                case "ACTIVATE_SHEET": {
                    if (cmd.sheetIdFrom !== cmd.sheetIdTo) {
                        const { col, row } = this.getters.getNextVisibleCellPosition({
                            sheetId: cmd.sheetIdTo,
                            col: 0,
                            row: 0,
                        });
                        const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));
                        this.selection.resetAnchor(this, { cell: { col, row }, zone });
                    }
                    break;
                }
                case "START_CHANGE_HIGHLIGHT":
                    const activeSheetId = this.getters.getActiveSheetId();
                    const newZone = this.getters.expandZone(activeSheetId, cmd.zone);
                    const focusIndex = this.ranges.findIndex((range) => {
                        const { xc, sheetName: sheet } = splitReference(range.xc);
                        const sheetName = sheet || this.getters.getSheetName(this.inputSheetId);
                        if (this.getters.getSheetName(activeSheetId) !== sheetName) {
                            return false;
                        }
                        const refRange = this.getters.getRangeFromSheetXC(activeSheetId, xc);
                        return isEqual(this.getters.expandZone(activeSheetId, refRange.zone), newZone);
                    });
                    if (focusIndex !== -1) {
                        this.focus(focusIndex);
                        const { left, top } = newZone;
                        this.selection.resetAnchor(this, { cell: { col: left, row: top }, zone: newZone });
                    }
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters || only callable by the parent
        // ---------------------------------------------------------------------------
        getSelectionInputValue() {
            return this.cleanInputs(this.ranges.map((range) => {
                return range.xc ? range.xc : "";
            }));
        }
        getSelectionInputHighlights() {
            return this.ranges.map((input) => this.inputToHighlights(input)).flat();
        }
        // ---------------------------------------------------------------------------
        // Other
        // ---------------------------------------------------------------------------
        /**
         * Focus a given range or remove the focus.
         */
        focus(index) {
            this.focusedRangeIndex = index;
        }
        focusLast() {
            this.focus(this.ranges.length - 1);
        }
        unfocus() {
            this.focusedRangeIndex = null;
        }
        setContent(index, xc) {
            this.ranges[index] = {
                ...this.ranges[index],
                xc,
            };
        }
        /**
         * Insert new inputs after the given index.
         */
        insertNewRange(index, values) {
            const currentMaxId = Math.max(0, ...this.ranges.map((range) => Number(range.id)));
            this.ranges.splice(index, 0, ...values.map((xc, i) => ({
                xc,
                id: currentMaxId + i + 1,
                color: colors$1[(currentMaxId + i) % colors$1.length],
            })));
        }
        /**
         * Set a new value in a given range input. If more than one value is provided,
         * new inputs will be added.
         */
        setRange(index, values) {
            const [, ...additionalValues] = values;
            this.setContent(index, values[0]);
            this.insertNewRange(index + 1, additionalValues);
            // focus the last newly added range
            if (additionalValues.length) {
                this.focus(index + additionalValues.length);
            }
        }
        removeRange(index) {
            this.ranges.splice(index, 1);
            if (this.focusedRangeIndex !== null) {
                this.focusLast();
            }
        }
        /**
         * Convert highlights input format to the command format.
         * The first xc in the input range will keep its color.
         * Invalid ranges and ranges from other sheets than the active sheets
         * are ignored.
         */
        inputToHighlights({ xc, color }) {
            const XCs = this.cleanInputs([xc])
                .filter((range) => this.getters.isRangeValid(range))
                .filter((reference) => this.shouldBeHighlighted(this.inputSheetId, reference));
            return XCs.map((xc) => {
                const { sheetName } = splitReference(xc);
                return {
                    zone: this.getters.getRangeFromSheetXC(this.inputSheetId, xc).zone,
                    sheetId: (sheetName && this.getters.getSheetIdByName(sheetName)) || this.inputSheetId,
                    color,
                };
            });
        }
        cleanInputs(ranges) {
            return ranges
                .map((xc) => xc.split(","))
                .flat()
                .map((xc) => xc.trim())
                .filter((xc) => xc !== "");
        }
        /**
         * Check if a cell or range reference should be highlighted.
         * It should be highlighted if it references the current active sheet.
         * Note that if no sheet name is given in the reference ("A1"), it refers to the
         * active sheet when the selection input was enabled which might be different from
         * the current active sheet.
         */
        shouldBeHighlighted(inputSheetId, reference) {
            const { sheetName } = splitReference(reference);
            const sheetId = this.getters.getSheetIdByName(sheetName);
            const activeSheetId = this.getters.getActiveSheet().id;
            const valid = this.getters.isRangeValid(reference);
            return (valid &&
                (sheetId === activeSheetId || (sheetId === undefined && activeSheetId === inputSheetId)));
        }
        /**
         * Return the index of a range given its id
         * or `null` if the range is not found.
         */
        getIndex(rangeId) {
            const index = this.ranges.findIndex((range) => range.id === rangeId);
            return index >= 0 ? index : null;
        }
    }

    /**
     * Selection input Plugin
     *
     * The SelectionInput component input and output are both arrays of strings, but
     * it requires an intermediary internal state to work.
     * This plugin handles this internal state.
     */
    class SelectionInputsManagerPlugin extends UIPlugin {
        config;
        static layers = [1 /* LAYERS.Highlights */];
        static getters = [
            "getSelectionInput",
            "getSelectionInputValue",
            "isRangeValid",
            "getSelectionInputHighlights",
        ];
        inputs = {};
        focusedInputId = null;
        get currentInput() {
            return this.focusedInputId ? this.inputs[this.focusedInputId] : null;
        }
        constructor(config) {
            super(config);
            this.config = config;
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "FOCUS_RANGE":
                case "CHANGE_RANGE":
                case "ADD_EMPTY_RANGE":
                case "REMOVE_RANGE":
                    if (!this.inputs[cmd.id]) {
                        return "InvalidInputId" /* CommandResult.InvalidInputId */;
                    }
            }
            switch (cmd.type) {
                case "FOCUS_RANGE":
                    const index = this.currentInput?.getIndex(cmd.rangeId);
                    if (this.focusedInputId === cmd.id && this.currentInput?.focusedRangeIndex === index) {
                        return "InputAlreadyFocused" /* CommandResult.InputAlreadyFocused */;
                    }
                    break;
                case "ADD_RANGE":
                case "ADD_EMPTY_RANGE":
                    const input = this.inputs[cmd.id];
                    if (input.inputHasSingleRange && input.ranges.length === 1) {
                        return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */;
                    }
                    break;
                case "REMOVE_RANGE":
                    if (this.inputs[cmd.id].ranges.length === 1) {
                        return "MinimumRangesReached" /* CommandResult.MinimumRangesReached */;
                    }
                    break;
                case "CHANGE_RANGE": {
                    const input = this.inputs[cmd.id];
                    if (input.inputHasSingleRange && cmd.value.split(",").length > 1) {
                        return "MaximumRangesReached" /* CommandResult.MaximumRangesReached */;
                    }
                    break;
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "ENABLE_NEW_SELECTION_INPUT":
                    this.initInput(cmd.id, cmd.initialRanges || [], cmd.hasSingleRange);
                    break;
                case "DISABLE_SELECTION_INPUT":
                    if (this.focusedInputId === cmd.id) {
                        this.unfocus();
                    }
                    delete this.inputs[cmd.id];
                    break;
                case "UNFOCUS_SELECTION_INPUT":
                    this.unfocus();
                    break;
                case "ADD_RANGE":
                case "ADD_EMPTY_RANGE":
                case "REMOVE_RANGE":
                    if (cmd.id !== this.focusedInputId) {
                        const input = this.inputs[cmd.id];
                        this.selection.capture(input, { cell: { col: 0, row: 0 }, zone: positionToZone({ col: 0, row: 0 }) }, {
                            handleEvent: input.handleEvent.bind(input),
                            release: () => (this.focusedInputId = null),
                        });
                        this.focusedInputId = cmd.id;
                    }
                    break;
                case "FOCUS_RANGE":
                case "CHANGE_RANGE":
                    if (cmd.id !== this.focusedInputId) {
                        const input = this.inputs[cmd.id];
                        const range = input.ranges.find((range) => range.id === cmd.rangeId);
                        if (this.isRangeValid(range?.xc || "A1")) {
                            const sheetId = this.getters.getActiveSheetId();
                            const zone = this.getters.getRangeFromSheetXC(sheetId, range?.xc || "A1").zone;
                            this.selection.capture(input, { cell: { col: zone.left, row: zone.top }, zone }, {
                                handleEvent: input.handleEvent.bind(input),
                                release: () => (this.focusedInputId = null),
                            });
                        }
                        this.focusedInputId = cmd.id;
                    }
                    break;
            }
            this.currentInput?.handle(cmd);
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        /**
         * Return a list of all valid XCs.
         * e.g. ["A1", "Sheet2!B3", "E12"]
         */
        getSelectionInput(id) {
            if (!this.inputs[id]) {
                return [];
            }
            return this.inputs[id].ranges.map((input, index) => Object.assign({}, input, {
                color: this.focusedInputId === id &&
                    this.inputs[id].focusedRangeIndex !== null &&
                    this.isRangeValid(input.xc)
                    ? input.color
                    : null,
                isFocused: this.focusedInputId === id && this.inputs[id].focusedRangeIndex === index,
            }));
        }
        isRangeValid(reference) {
            if (!reference) {
                return false;
            }
            const { xc, sheetName } = splitReference(reference);
            return (xc.match(rangeReference) !== null &&
                (!sheetName || this.getters.getSheetIdByName(sheetName) !== undefined));
        }
        getSelectionInputValue(id) {
            return this.inputs[id].getSelectionInputValue();
        }
        getSelectionInputHighlights() {
            if (!this.focusedInputId) {
                return [];
            }
            return this.inputs[this.focusedInputId].getSelectionInputHighlights();
        }
        // ---------------------------------------------------------------------------
        // Other
        // ---------------------------------------------------------------------------
        initInput(id, initialRanges, inputHasSingleRange = false) {
            if (this.inputs[id]) {
                this.unfocus();
            }
            this.inputs[id] = new SelectionInputPlugin(this.config, initialRanges, inputHasSingleRange);
            if (initialRanges.length === 0) {
                const input = this.inputs[id];
                const anchor = {
                    zone: positionToZone({ col: 0, row: 0 }),
                    cell: { col: 0, row: 0 },
                };
                this.selection.capture(input, anchor, {
                    handleEvent: input.handleEvent.bind(input),
                    release: () => (this.focusedInputId = null),
                });
                this.focusedInputId = id;
            }
        }
        unfocus() {
            this.selection.release(this.currentInput);
            this.focusedInputId = null;
        }
    }

    class SortPlugin extends UIPlugin {
        static getters = ["getContiguousZone"];
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "SORT_CELLS":
                    if (!isInside(cmd.col, cmd.row, cmd.zone)) {
                        throw new Error(_t("The anchor must be part of the provided zone"));
                    }
                    return this.checkValidations(cmd, this.checkMerge, this.checkMergeSizes);
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "SORT_CELLS":
                    this.sortZone(cmd.sheetId, cmd, cmd.zone, cmd.sortDirection, cmd.sortOptions || {});
                    break;
            }
        }
        checkMerge({ sheetId, zone }) {
            if (!this.getters.doesIntersectMerge(sheetId, zone)) {
                return "Success" /* CommandResult.Success */;
            }
            /*Test the presence of single cells*/
            const singleCells = positions(zone).some(({ col, row }) => !this.getters.isInMerge({ sheetId, col, row }));
            if (singleCells) {
                return "InvalidSortZone" /* CommandResult.InvalidSortZone */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkMergeSizes({ sheetId, zone }) {
            if (!this.getters.doesIntersectMerge(sheetId, zone)) {
                return "Success" /* CommandResult.Success */;
            }
            const merges = this.getters.getMerges(sheetId).filter((merge) => overlap(merge, zone));
            /*Test the presence of merges of different sizes*/
            const mergeDimension = zoneToDimension(merges[0]);
            let [widthFirst, heightFirst] = [mergeDimension.numberOfCols, mergeDimension.numberOfRows];
            if (!merges.every((merge) => {
                let [widthCurrent, heightCurrent] = [
                    merge.right - merge.left + 1,
                    merge.bottom - merge.top + 1,
                ];
                return widthCurrent === widthFirst && heightCurrent === heightFirst;
            })) {
                return "InvalidSortZone" /* CommandResult.InvalidSortZone */;
            }
            return "Success" /* CommandResult.Success */;
        }
        // getContiguousZone helpers
        /**
         * safe-version of expandZone to make sure we don't get out of the grid
         */
        expand(sheetId, z) {
            const { left, right, top, bottom } = this.getters.expandZone(sheetId, z);
            return {
                left: Math.max(0, left),
                right: Math.min(this.getters.getNumberCols(sheetId) - 1, right),
                top: Math.max(0, top),
                bottom: Math.min(this.getters.getNumberRows(sheetId) - 1, bottom),
            };
        }
        /**
         * verifies the presence of at least one non-empty cell in the given zone
         */
        checkExpandedValues(sheetId, z) {
            const expandedZone = this.expand(sheetId, z);
            let cell;
            if (this.getters.doesIntersectMerge(sheetId, expandedZone)) {
                const { left, right, top, bottom } = expandedZone;
                for (let c = left; c <= right; c++) {
                    for (let r = top; r <= bottom; r++) {
                        const { col, row } = this.getters.getMainCellPosition({ sheetId, col: c, row: r });
                        cell = this.getters.getEvaluatedCell({ sheetId, col, row });
                        if (cell.formattedValue) {
                            return true;
                        }
                    }
                }
            }
            else {
                for (let cell of this.getters.getEvaluatedCellsInZone(sheetId, expandedZone)) {
                    if (cell.formattedValue) {
                        return true;
                    }
                }
            }
            return false;
        }
        /**
         * This function will expand the provided zone in directions (top, bottom, left, right) for which there
         * are non-null cells on the external boundary of the zone in the given direction.
         *
         * Example:
         *          A     B     C     D     E
         *         ___   ___   ___   ___   ___
         *    1  |     |  D  |     |     |     |
         *         ___   ___   ___   ___   ___
         *    2  |  5  |     |  1  |  D  |     |
         *         ___   ___   ___   ___   ___
         *    3  |     |     |  A  |  X  |     |
         *         ___   ___   ___   ___   ___
         *    4  |     |     |     |     |     |
         *         ___   ___   ___   ___   ___
         *
         *  Let's consider a provided zone corresponding to (C2:D3) - (left:2, right: 3, top:1, bottom:2)
         *  - the top external boundary is (B1:E1)
         *    Since we have B1='D' != "", we expand to the top: => (C1:D3)
         *    The top boundary having reached the top of the grid, we cannot expand in that direction anymore
         *
         *  - the left boundary is (B1:B4)
         *    since we have B1 again, we expand to the left  => (B1:D3)
         *
         *  - the right and bottom boundaries are a dead end for now as (E1:E4) and (A4:E4) are empty.
         *
         *  - the left boundary is now (A1:A4)
         *    Since we have A2=5 != "", we can therefore expand to the left => (A1:D3)
         *
         *  This will be the final zone as left and top have reached the boundaries of the grid and
         *  the other boundaries (E1:E4) and (A4:E4) are empty.
         *
         * @param sheetId UID of concerned sheet
         * @param zone Zone
         *
         */
        getContiguousZone(sheetId, zone) {
            let { top, bottom, left, right } = zone;
            let canExpand;
            let stop = false;
            while (!stop) {
                stop = true;
                /** top row external boundary */
                if (top > 0) {
                    canExpand = this.checkExpandedValues(sheetId, {
                        left: left - 1,
                        right: right + 1,
                        top: top - 1,
                        bottom: top - 1,
                    });
                    if (canExpand) {
                        stop = false;
                        top--;
                    }
                }
                /** left column external boundary */
                if (left > 0) {
                    canExpand = this.checkExpandedValues(sheetId, {
                        left: left - 1,
                        right: left - 1,
                        top: top - 1,
                        bottom: bottom + 1,
                    });
                    if (canExpand) {
                        stop = false;
                        left--;
                    }
                }
                /** right column external boundary */
                if (right < this.getters.getNumberCols(sheetId) - 1) {
                    canExpand = this.checkExpandedValues(sheetId, {
                        left: right + 1,
                        right: right + 1,
                        top: top - 1,
                        bottom: bottom + 1,
                    });
                    if (canExpand) {
                        stop = false;
                        right++;
                    }
                }
                /** bottom row external boundary */
                if (bottom < this.getters.getNumberRows(sheetId) - 1) {
                    canExpand = this.checkExpandedValues(sheetId, {
                        left: left - 1,
                        right: right + 1,
                        top: bottom + 1,
                        bottom: bottom + 1,
                    });
                    if (canExpand) {
                        stop = false;
                        bottom++;
                    }
                }
            }
            return { left, right, top, bottom };
        }
        /**
         * This function evaluates if the top row of a provided zone can be considered as a `header`
         * by checking the following criteria:
         * * If the left-most column top row value (topLeft) is empty, we ignore it while evaluating the criteria.
         * 1 - Apart from the left-most column, every element of the top row must be non-empty, i.e. a cell should be present in the sheet.
         * 2 - There should be at least one column in which the type (CellValueType) of the rop row cell differs from the type of the cell below.
         *  For the second criteria, we ignore columns on which the cell below is empty.
         *
         */
        hasHeader(sheetId, items) {
            if (items[0].length === 1)
                return false;
            let cells = items.map((col) => col.map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }).type));
            // ignore left-most column when topLeft cell is empty
            const topLeft = cells[0][0];
            if (topLeft === CellValueType.empty) {
                cells = cells.slice(1);
            }
            if (cells.some((item) => item[0] === CellValueType.empty)) {
                return false;
            }
            else if (cells.some((item) => item[1] !== CellValueType.empty && item[0] !== item[1])) {
                return true;
            }
            else {
                return false;
            }
        }
        sortZone(sheetId, anchor, zone, sortDirection, options) {
            const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);
            let sortingCol = this.getters.getMainCellPosition({
                sheetId,
                col: anchor.col,
                row: anchor.row,
            }).col; // fetch anchor
            let sortZone = Object.assign({}, zone);
            // Update in case of merges in the zone
            let cellPositions = this.mainCells(sheetId, zone);
            if (!options.sortHeaders && this.hasHeader(sheetId, cellPositions)) {
                sortZone.top += stepY;
            }
            cellPositions = this.mainCells(sheetId, sortZone);
            const sortingCells = cellPositions[sortingCol - sortZone.left];
            const sortedIndexOfSortTypeCells = sortCells(sortingCells.map((position) => this.getters.getEvaluatedCell(position)), sortDirection, Boolean(options.emptyCellAsZero));
            const sortedIndex = sortedIndexOfSortTypeCells.map((x) => x.index);
            const [width, height] = [cellPositions.length, cellPositions[0].length];
            const updateCellCommands = [];
            for (let c = 0; c < width; c++) {
                for (let r = 0; r < height; r++) {
                    let { col, row, sheetId } = cellPositions[c][sortedIndex[r]];
                    const cell = this.getters.getCell({ sheetId, col, row });
                    let newCol = sortZone.left + c * stepX;
                    let newRow = sortZone.top + r * stepY;
                    let newCellValues = {
                        sheetId: sheetId,
                        col: newCol,
                        row: newRow,
                        content: "",
                    };
                    if (cell) {
                        let content = cell.content;
                        if (cell.isFormula) {
                            const position = this.getters.getCellPosition(cell.id);
                            // we only have a vertical offset
                            content = this.getters.getTranslatedCellFormula(sheetId, 0, newRow - position.row, cell.compiledFormula);
                        }
                        newCellValues.style = cell.style;
                        newCellValues.content = content;
                        newCellValues.format = cell.format;
                    }
                    updateCellCommands.push(newCellValues);
                }
            }
            updateCellCommands.forEach((cmdPayload) => this.dispatch("UPDATE_CELL", cmdPayload));
        }
        /**
         * Return the distances between main merge cells in the zone.
         * (1 if there are no merges).
         * Note: it is assumed all merges are the same in the zone.
         */
        mainCellsSteps(sheetId, zone) {
            const merge = this.getters.getMerge({ sheetId, col: zone.left, row: zone.top });
            const stepX = merge ? merge.right - merge.left + 1 : 1;
            const stepY = merge ? merge.bottom - merge.top + 1 : 1;
            return [stepX, stepY];
        }
        /**
         * Return a 2D array of cells in the zone (main merge cells if there are merges)
         */
        mainCells(sheetId, zone) {
            const [stepX, stepY] = this.mainCellsSteps(sheetId, zone);
            const cells = [];
            const cols = range(zone.left, zone.right + 1, stepX);
            const rows = range(zone.top, zone.bottom + 1, stepY);
            for (const col of cols) {
                const colCells = [];
                cells.push(colCells);
                for (const row of rows) {
                    colCells.push({ sheetId, col, row });
                }
            }
            return cells;
        }
    }

    class UIOptionsPlugin extends UIPlugin {
        static getters = ["shouldShowFormulas"];
        showFormulas = false;
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        handle(cmd) {
            switch (cmd.type) {
                case "SET_FORMULA_VISIBILITY":
                    this.showFormulas = cmd.show;
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        shouldShowFormulas() {
            return this.showFormulas;
        }
    }

    class SheetUIPlugin extends UIPlugin {
        static getters = [
            "doesCellHaveGridIcon",
            "getCellWidth",
            "getTextWidth",
            "getCellText",
            "getCellMultiLineText",
        ];
        ctx = document.createElement("canvas").getContext("2d");
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            return this.chainValidations(this.checkSheetExists, this.checkZonesAreInSheet)(cmd);
        }
        handle(cmd) {
            switch (cmd.type) {
                case "AUTORESIZE_COLUMNS":
                    for (let col of cmd.cols) {
                        const size = this.getColMaxWidth(cmd.sheetId, col);
                        if (size !== 0) {
                            this.dispatch("RESIZE_COLUMNS_ROWS", {
                                elements: [col],
                                dimension: "COL",
                                size,
                                sheetId: cmd.sheetId,
                            });
                        }
                    }
                    break;
                case "AUTORESIZE_ROWS":
                    for (let row of cmd.rows) {
                        this.dispatch("RESIZE_COLUMNS_ROWS", {
                            elements: [row],
                            dimension: "ROW",
                            size: null,
                            sheetId: cmd.sheetId,
                        });
                    }
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getCellWidth(position) {
            const style = this.getters.getCellComputedStyle(position);
            let contentWidth = 0;
            const content = this.getters.getEvaluatedCell(position).formattedValue;
            if (content) {
                const multiLineText = splitTextToWidth(this.ctx, content, style, undefined);
                contentWidth += Math.max(...multiLineText.map((line) => computeTextWidth(this.ctx, line, style)));
            }
            const icon = this.getters.getConditionalIcon(position);
            if (icon) {
                contentWidth += computeIconWidth(style);
            }
            if (this.getters.doesCellHaveGridIcon(position)) {
                contentWidth += ICON_EDGE_LENGTH + GRID_ICON_MARGIN;
            }
            if (contentWidth === 0) {
                return 0;
            }
            contentWidth += 2 * PADDING_AUTORESIZE_HORIZONTAL;
            if (style.wrapping === "wrap") {
                const colWidth = this.getters.getColSize(this.getters.getActiveSheetId(), position.col);
                return Math.min(colWidth, contentWidth);
            }
            return contentWidth;
        }
        getTextWidth(text, style) {
            return computeTextWidth(this.ctx, text, style);
        }
        getCellText(position, showFormula = false) {
            const cell = this.getters.getCell(position);
            if (showFormula && cell?.isFormula) {
                return localizeFormula(cell.content, this.getters.getLocale());
            }
            else {
                return this.getters.getEvaluatedCell(position).formattedValue;
            }
        }
        /**
         * Return the text of a cell, split in multiple lines if needed. The text will be split in multiple
         * line if it contains NEWLINE characters, or if it's longer than the given width.
         */
        getCellMultiLineText(position, width) {
            const style = this.getters.getCellStyle(position);
            const text = this.getters.getCellText(position, this.getters.shouldShowFormulas());
            return splitTextToWidth(this.ctx, text, style, width);
        }
        doesCellHaveGridIcon(position) {
            const isFilterHeader = this.getters.isFilterHeader(position);
            const hasListIcon = !this.getters.isReadonly() && this.getters.cellHasListDataValidationIcon(position);
            return isFilterHeader || hasListIcon;
        }
        // ---------------------------------------------------------------------------
        // Grid manipulation
        // ---------------------------------------------------------------------------
        getColMaxWidth(sheetId, index) {
            const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
            const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
            return Math.max(0, ...sizes);
        }
        /**
         * Check that any "sheetId" in the command matches an existing
         * sheet.
         */
        checkSheetExists(cmd) {
            if ("sheetId" in cmd && this.getters.tryGetSheet(cmd.sheetId) === undefined) {
                return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
            }
            return "Success" /* CommandResult.Success */;
        }
        /**
         * Check if zones in the command are well formed and
         * not outside the sheet.
         */
        checkZonesAreInSheet(cmd) {
            const sheetId = "sheetId" in cmd ? cmd.sheetId : this.getters.tryGetActiveSheetId();
            const zones = this.getters.getCommandZones(cmd);
            if (!sheetId && zones.length > 0) {
                return "NoActiveSheet" /* CommandResult.NoActiveSheet */;
            }
            if (sheetId && zones.length > 0) {
                return this.getters.checkZonesExistInSheet(sheetId, zones);
            }
            return "Success" /* CommandResult.Success */;
        }
    }

    const genericRepeatsTransforms = [
        repeatSheetDependantCommand,
        repeatTargetDependantCommand,
        repeatPositionDependantCommand,
    ];
    function repeatSheetDependantCommand(getters, command) {
        if (!("sheetId" in command))
            return command;
        return { ...deepCopy(command), sheetId: getters.getActiveSheetId() };
    }
    function repeatTargetDependantCommand(getters, command) {
        if (!("target" in command) || !Array.isArray(command.target))
            return command;
        return {
            ...deepCopy(command),
            target: getters.getSelectedZones(),
        };
    }
    function repeatZoneDependantCommand(getters, command) {
        if (!("zone" in command))
            return command;
        return {
            ...deepCopy(command),
            zone: getters.getSelectedZone(),
        };
    }
    function repeatPositionDependantCommand(getters, command) {
        if (!("row" in command) || !("col" in command))
            return command;
        const { col, row } = getters.getActivePosition();
        return { ...deepCopy(command), col, row };
    }

    const uuidGenerator = new UuidGenerator();
    function repeatCreateChartCommand(getters, cmd) {
        return {
            ...repeatSheetDependantCommand(getters, cmd),
            id: uuidGenerator.uuidv4(),
        };
    }
    function repeatCreateImageCommand(getters, cmd) {
        return {
            ...repeatSheetDependantCommand(getters, cmd),
            figureId: uuidGenerator.uuidv4(),
        };
    }
    function repeatCreateFigureCommand(getters, cmd) {
        const newCmd = repeatSheetDependantCommand(getters, cmd);
        newCmd.figure.id = uuidGenerator.uuidv4();
        return newCmd;
    }
    function repeatCreateSheetCommand(getters, cmd) {
        const newCmd = deepCopy(cmd);
        newCmd.sheetId = uuidGenerator.uuidv4();
        const sheetName = cmd.name || getters.getSheet(getters.getActiveSheetId()).name;
        // Extract the prefix of the sheet name (everything before the number at the end of the name)
        const namePrefix = sheetName.match(/(.+?)\d*$/)?.[1] || sheetName;
        newCmd.name = getters.getNextSheetName(namePrefix);
        return newCmd;
    }
    function repeatAddColumnsRowsCommand(getters, cmd) {
        const currentPosition = getters.getActivePosition();
        return {
            ...repeatSheetDependantCommand(getters, cmd),
            base: cmd.dimension === "COL" ? currentPosition.col : currentPosition.row,
        };
    }
    function repeatHeaderElementCommand(getters, cmd) {
        const currentSelection = getters.getSelectedZone();
        return {
            ...repeatSheetDependantCommand(getters, cmd),
            elements: cmd.dimension === "COL"
                ? range(currentSelection.left, currentSelection.right + 1)
                : range(currentSelection.top, currentSelection.bottom + 1),
        };
    }
    function repeatInsertOrDeleteCellCommand(getters, cmd) {
        const currentSelection = getters.getSelectedZone();
        return {
            ...deepCopy(cmd),
            zone: currentSelection,
        };
    }
    function repeatAutoResizeCommand(getters, cmd) {
        const newCmd = deepCopy(cmd);
        const currentSelection = getters.getSelectedZone();
        const { top, bottom, left, right } = currentSelection;
        if ("cols" in newCmd) {
            newCmd.cols = range(left, right + 1);
        }
        else if ("rows" in newCmd) {
            newCmd.rows = range(top, bottom + 1);
        }
        return newCmd;
    }
    function repeatSortCellsCommand(getters, cmd) {
        const currentSelection = getters.getSelectedZone();
        return {
            ...repeatSheetDependantCommand(getters, cmd),
            col: currentSelection.left,
            row: currentSelection.top,
            zone: currentSelection,
        };
    }
    function repeatPasteCommand(getters, cmd) {
        /**
         * Note : we have to store the state of the clipboard in the clipboard plugin, and introduce a
         * new command REPEAT_PASTE to be able to repeat the paste command.
         *
         * We cannot re-dispatch a paste, because the content of the clipboard may have changed in between.
         *
         * And we cannot adapt the sub-commands of the paste command, because they are dependant on the state of the sheet,
         * and may change based on the paste location. A simple example is that paste create new col/rows for the clipboard
         * content to fit the sheet. So there will be ADD_COL_ROW_COMMANDS in the sub-commands in the history, but repeating
         * paste might not need them. Or they could only needed for the repeated paste, not for the original.
         */
        return {
            type: "REPEAT_PASTE",
            pasteOption: deepCopy(cmd.pasteOption),
            target: getters.getSelectedZones(),
        };
    }
    function repeatGroupHeadersCommand(getters, cmd) {
        const currentSelection = getters.getSelectedZone();
        return {
            ...repeatSheetDependantCommand(getters, cmd),
            start: cmd.dimension === "COL" ? currentSelection.left : currentSelection.top,
            end: cmd.dimension === "COL" ? currentSelection.right : currentSelection.bottom,
        };
    }

    /**
     *  Registry containing all the command that can be repeated on redo, and function to transform them
     *  to the current state of the model.
     *
     * If the transform function is undefined, the command will be transformed using generic transformations.
     * (change the sheetId, the row, the col, the target, the ranges, to the current active sheet & selection)
     *
     */
    const repeatCommandTransformRegistry = new Registry();
    repeatCommandTransformRegistry.add("UPDATE_CELL", genericRepeat);
    repeatCommandTransformRegistry.add("CLEAR_CELL", genericRepeat);
    repeatCommandTransformRegistry.add("DELETE_CONTENT", genericRepeat);
    repeatCommandTransformRegistry.add("ADD_MERGE", genericRepeat);
    repeatCommandTransformRegistry.add("REMOVE_MERGE", genericRepeat);
    repeatCommandTransformRegistry.add("SET_FORMATTING", genericRepeat);
    repeatCommandTransformRegistry.add("CLEAR_FORMATTING", genericRepeat);
    repeatCommandTransformRegistry.add("SET_BORDER", genericRepeat);
    repeatCommandTransformRegistry.add("CREATE_FILTER_TABLE", genericRepeat);
    repeatCommandTransformRegistry.add("REMOVE_FILTER_TABLE", genericRepeat);
    repeatCommandTransformRegistry.add("HIDE_SHEET", genericRepeat);
    repeatCommandTransformRegistry.add("ADD_COLUMNS_ROWS", repeatAddColumnsRowsCommand);
    repeatCommandTransformRegistry.add("REMOVE_COLUMNS_ROWS", repeatHeaderElementCommand);
    repeatCommandTransformRegistry.add("HIDE_COLUMNS_ROWS", repeatHeaderElementCommand);
    repeatCommandTransformRegistry.add("RESIZE_COLUMNS_ROWS", repeatHeaderElementCommand);
    repeatCommandTransformRegistry.add("CREATE_SHEET", repeatCreateSheetCommand);
    repeatCommandTransformRegistry.add("CREATE_FIGURE", repeatCreateFigureCommand);
    repeatCommandTransformRegistry.add("CREATE_CHART", repeatCreateChartCommand);
    repeatCommandTransformRegistry.add("CREATE_IMAGE", repeatCreateImageCommand);
    repeatCommandTransformRegistry.add("GROUP_HEADERS", repeatGroupHeadersCommand);
    repeatCommandTransformRegistry.add("UNGROUP_HEADERS", repeatGroupHeadersCommand);
    repeatCommandTransformRegistry.add("UNGROUP_HEADERS", repeatGroupHeadersCommand);
    repeatCommandTransformRegistry.add("UNFOLD_HEADER_GROUPS_IN_ZONE", repeatZoneDependantCommand);
    repeatCommandTransformRegistry.add("FOLD_HEADER_GROUPS_IN_ZONE", repeatZoneDependantCommand);
    const repeatLocalCommandTransformRegistry = new Registry();
    repeatLocalCommandTransformRegistry.add("STOP_EDITION", repeatLocalCommandChildren);
    repeatLocalCommandTransformRegistry.add("PASTE", repeatPasteCommand);
    repeatLocalCommandTransformRegistry.add("INSERT_CELL", repeatInsertOrDeleteCellCommand);
    repeatLocalCommandTransformRegistry.add("DELETE_CELL", repeatInsertOrDeleteCellCommand);
    repeatLocalCommandTransformRegistry.add("AUTORESIZE_COLUMNS", repeatAutoResizeCommand);
    repeatLocalCommandTransformRegistry.add("AUTORESIZE_ROWS", repeatAutoResizeCommand);
    repeatLocalCommandTransformRegistry.add("SORT_CELLS", repeatSortCellsCommand);
    repeatLocalCommandTransformRegistry.add("SUM_SELECTION", genericRepeat);
    repeatLocalCommandTransformRegistry.add("SET_DECIMAL", genericRepeat);
    function genericRepeat(getters, command) {
        let transformedCommand = deepCopy(command);
        for (const repeatTransform of genericRepeatsTransforms) {
            transformedCommand = repeatTransform(getters, transformedCommand);
        }
        return transformedCommand;
    }
    function repeatCoreCommand(getters, command) {
        if (!command) {
            return undefined;
        }
        const isRepeatable = repeatCommandTransformRegistry.contains(command.type);
        if (!isRepeatable) {
            return undefined;
        }
        const transform = repeatCommandTransformRegistry.get(command.type);
        return transform(getters, command);
    }
    function repeatLocalCommand(getters, command, childCommands) {
        const isRepeatable = repeatLocalCommandTransformRegistry.contains(command.type);
        if (!isRepeatable) {
            return undefined;
        }
        const repeatTransform = repeatLocalCommandTransformRegistry.get(command.type);
        return repeatTransform(getters, command, childCommands);
    }
    function repeatLocalCommandChildren(getters, cmd, childCommands) {
        return childCommands
            .map((childCommand) => repeatCoreCommand(getters, childCommand))
            .filter(isDefined$1);
    }

    function canRepeatRevision(revision) {
        if (!revision || !revision.rootCommand || typeof revision.rootCommand !== "object") {
            return false;
        }
        if (isCoreCommand(revision.rootCommand)) {
            return repeatCommandTransformRegistry.contains(revision.rootCommand.type);
        }
        return repeatLocalCommandTransformRegistry.contains(revision.rootCommand.type);
    }
    function repeatRevision(revision, getters) {
        if (!revision.rootCommand || typeof revision.rootCommand !== "object") {
            return undefined;
        }
        if (isCoreCommand(revision.rootCommand)) {
            return repeatCoreCommand(getters, revision.rootCommand);
        }
        return repeatLocalCommand(getters, revision.rootCommand, revision.commands);
    }

    /**
     * Local History
     *
     * The local history is responsible of tracking the locally state updates
     * It maintains the local undo and redo stack to allow to undo/redo only local
     * changes
     */
    class HistoryPlugin extends UIPlugin {
        static getters = ["canUndo", "canRedo"];
        /**
         * Ids of the revisions which can be undone
         */
        undoStack = [];
        /**
         * Ids of the revisions which can be redone
         */
        redoStack = [];
        session;
        constructor(config) {
            super(config);
            this.session = config.session;
            this.session.on("new-local-state-update", this, this.onNewLocalStateUpdate);
            this.session.on("pending-revisions-dropped", this, ({ revisionIds }) => this.drop(revisionIds));
            this.session.on("snapshot", this, () => {
                this.undoStack = [];
                this.redoStack = [];
            });
        }
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "REQUEST_UNDO":
                    if (!this.canUndo()) {
                        return "EmptyUndoStack" /* CommandResult.EmptyUndoStack */;
                    }
                    break;
                case "REQUEST_REDO":
                    if (!this.canRedo()) {
                        return "EmptyRedoStack" /* CommandResult.EmptyRedoStack */;
                    }
                    break;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "REQUEST_UNDO":
                case "REQUEST_REDO":
                    // History changes (undo & redo) are *not* applied optimistically on the local state.
                    // We wait a global confirmation from the server. The goal is to avoid handling concurrent
                    // history changes on multiple clients which are very hard to manage correctly.
                    this.requestHistoryChange(cmd.type === "REQUEST_UNDO" ? "UNDO" : "REDO");
            }
        }
        finalize() { }
        requestHistoryChange(type) {
            const id = type === "UNDO" ? this.undoStack.pop() : this.redoStack.pop();
            if (!id) {
                const lastNonRedoRevision = this.getPossibleRevisionToRepeat();
                if (!lastNonRedoRevision) {
                    return;
                }
                const repeatedCommands = repeatRevision(lastNonRedoRevision, this.getters);
                if (!repeatedCommands) {
                    return;
                }
                if (!Array.isArray(repeatedCommands)) {
                    this.dispatch(repeatedCommands.type, repeatedCommands);
                    return;
                }
                for (const command of repeatedCommands) {
                    this.dispatch(command.type, command);
                }
                return;
            }
            if (type === "UNDO") {
                this.session.undo(id);
                this.redoStack.push(id);
            }
            else {
                this.session.redo(id);
                this.undoStack.push(id);
            }
        }
        canUndo() {
            return this.undoStack.length > 0;
        }
        canRedo() {
            if (this.redoStack.length > 0)
                return true;
            const lastNonRedoRevision = this.getPossibleRevisionToRepeat();
            return canRepeatRevision(lastNonRedoRevision);
        }
        drop(revisionIds) {
            this.undoStack = this.undoStack.filter((id) => !revisionIds.includes(id));
            this.redoStack = [];
        }
        onNewLocalStateUpdate({ id }) {
            this.undoStack.push(id);
            this.redoStack = [];
            if (this.undoStack.length > MAX_HISTORY_STEPS) {
                this.undoStack.shift();
            }
        }
        /**
         * Fetch the last revision which is not empty and not a repeated command
         *
         * Ignore repeated commands (REQUEST_REDO command as root command)
         * Ignore standard undo/redo revisions (that are empty)
         */
        getPossibleRevisionToRepeat() {
            return this.session.getLastLocalNonEmptyRevision();
        }
    }

    class SplitToColumnsPlugin extends UIPlugin {
        static getters = ["getAutomaticSeparator"];
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "SPLIT_TEXT_INTO_COLUMNS":
                    return this.chainValidations(this.batchValidations(this.checkSingleColSelected, this.checkNonEmptySelector), this.batchValidations(this.checkNotOverwritingContent, this.checkSeparatorInSelection))(cmd);
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "SPLIT_TEXT_INTO_COLUMNS":
                    this.splitIntoColumns(cmd);
                    break;
            }
        }
        getAutomaticSeparator() {
            const cells = this.getters.getSelectedCells();
            for (const cell of cells) {
                if (cell.value && cell.type === CellValueType.text) {
                    const separator = this.getAutoSeparatorForString(cell.value);
                    if (separator) {
                        return separator;
                    }
                }
            }
            return " ";
        }
        getAutoSeparatorForString(str) {
            const separators = [NEWLINE, ";", ",", " ", "."];
            for (const separator of separators) {
                if (str.includes(separator)) {
                    return separator;
                }
            }
            return;
        }
        splitIntoColumns({ separator, addNewColumns }) {
            const selection = this.getters.getSelectedZone();
            const sheetId = this.getters.getActiveSheetId();
            const splitted = this.getSplittedCols(selection, separator);
            if (addNewColumns) {
                this.addColsToAvoidCollisions(selection, splitted);
            }
            this.removeMergesInSplitZone(selection, splitted);
            this.addColumnsToNotOverflowSheet(selection, splitted);
            for (let i = 0; i < splitted.length; i++) {
                const row = selection.top + i;
                const splittedContent = splitted[i];
                const col = selection.left;
                const mainCell = this.getters.getCell({ sheetId, col, row });
                if (splittedContent.length === 1 && splittedContent[0] === mainCell?.content) {
                    continue;
                }
                for (const [index, content] of splittedContent.entries()) {
                    this.dispatch("UPDATE_CELL", {
                        sheetId,
                        col: col + index,
                        row,
                        content: canonicalizeNumberContent(content, this.getters.getLocale()),
                        format: "",
                        style: mainCell?.style || null,
                    });
                }
            }
        }
        getSplittedCols(selection, separator) {
            if (!separator) {
                throw new Error("Separator cannot be empty");
            }
            const sheetId = this.getters.getActiveSheetId();
            const splitted = [];
            for (const row of range(selection.top, selection.bottom + 1)) {
                const text = this.getters.getEvaluatedCell({
                    sheetId,
                    col: selection.left,
                    row,
                }).formattedValue;
                splitted.push(this.splitAndRemoveTrailingEmpty(text, separator));
            }
            return splitted;
        }
        splitAndRemoveTrailingEmpty(string, separator) {
            const splitted = string.split(separator);
            while (splitted.length > 1 && splitted[splitted.length - 1] === "") {
                splitted.pop();
            }
            return splitted;
        }
        willSplittedColsOverwriteContent(selection, splittedCols) {
            const sheetId = this.getters.getActiveSheetId();
            for (const row of range(selection.top, selection.bottom + 1)) {
                const splittedText = splittedCols[row - selection.top];
                for (let i = 1; i < splittedText.length; i++) {
                    const cell = this.getters.getCell({ sheetId, col: selection.left + i, row });
                    if (cell && cell.content) {
                        return true;
                    }
                }
            }
            return false;
        }
        removeMergesInSplitZone(selection, splittedCols) {
            const sheetId = this.getters.getActiveSheetId();
            const colsInSplitZone = Math.max(...splittedCols.map((s) => s.length));
            const splitZone = { ...selection, right: selection.left + colsInSplitZone - 1 };
            const merges = this.getters.getMergesInZone(sheetId, splitZone);
            this.dispatch("REMOVE_MERGE", { sheetId, target: merges });
        }
        addColsToAvoidCollisions(selection, splittedCols) {
            const sheetId = this.getters.getActiveSheetId();
            let colsToAdd = 0;
            for (const row of range(selection.top, selection.bottom + 1)) {
                const cellPosition = { sheetId, col: selection.left, row };
                const splittedText = splittedCols[row - selection.top];
                const colsToAddInRow = this.getColsToAddToAvoidCollision(cellPosition, splittedText);
                colsToAdd = Math.max(colsToAdd, colsToAddInRow);
            }
            if (colsToAdd) {
                this.dispatch("ADD_COLUMNS_ROWS", {
                    dimension: "COL",
                    base: selection.left,
                    sheetId,
                    quantity: colsToAdd,
                    position: "after",
                });
            }
        }
        getColsToAddToAvoidCollision(cellPosition, splittedText) {
            const maxColumnsToSpread = splittedText.length;
            for (let i = 1; i < maxColumnsToSpread; i++) {
                const col = cellPosition.col + i;
                const cell = this.getters.getCell({ ...cellPosition, col });
                if (cell && cell.content) {
                    return maxColumnsToSpread - i;
                }
            }
            return 0;
        }
        addColumnsToNotOverflowSheet(selection, splittedCols) {
            const sheetId = this.getters.getActiveSheetId();
            const maxColumnsToSpread = Math.max(...splittedCols.map((s) => s.length - 1));
            const maxColIndex = this.getters.getNumberCols(sheetId) - 1;
            if (selection.left + maxColumnsToSpread > maxColIndex) {
                this.dispatch("ADD_COLUMNS_ROWS", {
                    dimension: "COL",
                    base: maxColIndex,
                    sheetId,
                    quantity: selection.left + maxColumnsToSpread - maxColIndex,
                    position: "after",
                });
            }
        }
        checkSingleColSelected() {
            if (!this.getters.isSingleColSelected()) {
                return "MoreThanOneColumnSelected" /* CommandResult.MoreThanOneColumnSelected */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkNonEmptySelector(cmd) {
            if (cmd.separator === "") {
                return "EmptySplitSeparator" /* CommandResult.EmptySplitSeparator */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkNotOverwritingContent(cmd) {
            if (cmd.addNewColumns || cmd.force) {
                return "Success" /* CommandResult.Success */;
            }
            const selection = this.getters.getSelectedZones()[0];
            const splitted = this.getSplittedCols(selection, cmd.separator);
            if (this.willSplittedColsOverwriteContent(selection, splitted)) {
                return "SplitWillOverwriteContent" /* CommandResult.SplitWillOverwriteContent */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkSeparatorInSelection({ separator }) {
            const cells = this.getters.getSelectedCells();
            for (const cell of cells) {
                if (cell.formattedValue.includes(separator)) {
                    return "Success" /* CommandResult.Success */;
                }
            }
            return "NoSplitSeparatorInSelection" /* CommandResult.NoSplitSeparatorInSelection */;
        }
    }

    /** State of the clipboard when copying/cutting figures */
    class ClipboardFigureState {
        operation;
        getters;
        dispatch;
        sheetId;
        copiedFigure;
        copiedFigureContent;
        constructor(operation, getters, dispatch) {
            this.operation = operation;
            this.getters = getters;
            this.dispatch = dispatch;
            this.sheetId = getters.getActiveSheetId();
            const copiedFigureId = getters.getSelectedFigureId();
            if (!copiedFigureId) {
                throw new Error(`No figure selected`);
            }
            const figure = getters.getFigure(this.sheetId, copiedFigureId);
            if (!figure) {
                throw new Error(`No figure for the given id: ${copiedFigureId}`);
            }
            this.copiedFigure = { ...figure };
            switch (figure.tag) {
                case "chart":
                    this.copiedFigureContent = new ClipboardFigureChart(dispatch, getters, this.sheetId, copiedFigureId);
                    break;
                case "image":
                    this.copiedFigureContent = new ClipboardFigureImage(dispatch, getters, this.sheetId, copiedFigureId);
                    break;
                default:
                    throw new Error(`Unknow tag '${figure.tag}' for the given figure id: ${copiedFigureId}`);
            }
        }
        isCutAllowed(target) {
            return "Success" /* CommandResult.Success */;
        }
        isPasteAllowed(target, option) {
            if (target.length === 0) {
                return "EmptyTarget" /* CommandResult.EmptyTarget */;
            }
            if (option?.pasteOption !== undefined) {
                return "WrongFigurePasteOption" /* CommandResult.WrongFigurePasteOption */;
            }
            return "Success" /* CommandResult.Success */;
        }
        /**
         * Paste the clipboard content in the given target
         */
        paste(target) {
            const sheetId = this.getters.getActiveSheetId();
            const { width, height } = this.copiedFigure;
            const numCols = this.getters.getNumberCols(sheetId);
            const numRows = this.getters.getNumberRows(sheetId);
            const targetX = this.getters.getColDimensions(sheetId, target[0].left).start;
            const targetY = this.getters.getRowDimensions(sheetId, target[0].top).start;
            const maxX = this.getters.getColDimensions(sheetId, numCols - 1).end;
            const maxY = this.getters.getRowDimensions(sheetId, numRows - 1).end;
            const position = {
                x: maxX < width ? 0 : Math.min(targetX, maxX - width),
                y: maxY < height ? 0 : Math.min(targetY, maxY - height),
            };
            const newId = new UuidGenerator().uuidv4();
            this.copiedFigureContent.paste(sheetId, newId, position, { height, width });
            if (this.operation === "CUT") {
                this.dispatch("DELETE_FIGURE", {
                    sheetId: this.copiedFigureContent.sheetId,
                    id: this.copiedFigure.id,
                });
            }
            this.dispatch("SELECT_FIGURE", { id: newId });
        }
        getClipboardContent() {
            return { [ClipboardMIMEType.PlainText]: "\t" };
        }
        isColRowDirtyingClipboard(position, dimension) {
            return false;
        }
        drawClipboard(renderingContext) { }
    }
    class ClipboardFigureChart {
        dispatch;
        sheetId;
        copiedChart;
        constructor(dispatch, getters, sheetId, copiedFigureId) {
            this.dispatch = dispatch;
            this.sheetId = sheetId;
            const chart = getters.getChart(copiedFigureId);
            if (!chart) {
                throw new Error(`No chart for the given id: ${copiedFigureId}`);
            }
            this.copiedChart = chart.copyInSheetId(sheetId);
        }
        paste(sheetId, figureId, position, size) {
            const copy = this.copiedChart.copyInSheetId(sheetId);
            this.dispatch("CREATE_CHART", {
                id: figureId,
                sheetId,
                position,
                size,
                definition: copy.getDefinition(),
            });
        }
    }
    class ClipboardFigureImage {
        dispatch;
        sheetId;
        copiedImage;
        constructor(dispatch, getters, sheetId, copiedFigureId) {
            this.dispatch = dispatch;
            this.sheetId = sheetId;
            const image = getters.getImage(copiedFigureId);
            this.copiedImage = deepCopy(image);
        }
        paste(sheetId, figureId, position, size) {
            const copy = deepCopy(this.copiedImage);
            this.dispatch("CREATE_IMAGE", {
                figureId,
                sheetId,
                position,
                size,
                definition: copy,
            });
        }
    }

    /** Change a number string to its canonical form (en_US locale) */
    function canonicalizeNumberValue(content, locale) {
        return content.startsWith("=")
            ? canonicalizeFormula(content, locale)
            : canonicalizeNumberLiteral(content, locale);
    }
    /** Change a formula to its canonical form (en_US locale) */
    function canonicalizeFormula(formula, locale) {
        return _localizeFormula(formula, locale, DEFAULT_LOCALE);
    }
    function _localizeFormula(formula, fromLocale, toLocale) {
        if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
            fromLocale.decimalSeparator === toLocale.decimalSeparator) {
            return formula;
        }
        const tokens = tokenize(formula, fromLocale);
        let localizedFormula = "";
        for (const token of tokens) {
            if (token.type === "NUMBER") {
                localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
            }
            else if (token.type === "ARG_SEPARATOR") {
                localizedFormula += toLocale.formulaArgSeparator;
            }
            else {
                localizedFormula += token.value;
            }
        }
        return localizedFormula;
    }

    /** State of the clipboard when copying/cutting from the OS clipboard*/
    class ClipboardOsState extends ClipboardCellsAbstractState {
        values;
        constructor(content, getters, dispatch, selection) {
            super("COPY", getters, dispatch, selection);
            this.values = content
                .replace(/\r/g, "")
                .split("\n")
                .map((vals) => vals.split("\t"));
        }
        isPasteAllowed(target, clipboardOption) {
            const sheetId = this.getters.getActiveSheetId();
            const pasteZone = this.getPasteZone(target);
            if (this.getters.doesIntersectMerge(sheetId, pasteZone)) {
                return "WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */;
            }
            return "Success" /* CommandResult.Success */;
        }
        paste(target, clipboardOption) {
            if (clipboardOption?.pasteOption === "onlyFormat") {
                return;
            }
            const values = this.values;
            const pasteZone = this.getPasteZone(target);
            const { left: activeCol, top: activeRow } = pasteZone;
            const { numberOfCols, numberOfRows } = zoneToDimension(pasteZone);
            const sheetId = this.getters.getActiveSheetId();
            const locale = this.getters.getLocale();
            this.addMissingDimensions(numberOfCols, numberOfRows, activeCol, activeRow);
            for (let i = 0; i < values.length; i++) {
                for (let j = 0; j < values[i].length; j++) {
                    this.dispatch("UPDATE_CELL", {
                        row: activeRow + i,
                        col: activeCol + j,
                        content: canonicalizeNumberValue(values[i][j], locale),
                        sheetId,
                    });
                }
            }
            const zone = {
                left: activeCol,
                top: activeRow,
                right: activeCol + numberOfCols - 1,
                bottom: activeRow + numberOfRows - 1,
            };
            this.selection.selectZone({ cell: { col: activeCol, row: activeRow }, zone }, { scrollIntoView: false });
        }
        getClipboardContent() {
            return {
                [ClipboardMIMEType.PlainText]: this.values.map((values) => values.join("\t")).join("\n"),
            };
        }
        getPasteZone(target) {
            const height = this.values.length;
            const width = Math.max(...this.values.map((a) => a.length));
            const { left: activeCol, top: activeRow } = target[0];
            return {
                top: activeRow,
                left: activeCol,
                bottom: activeRow + height - 1,
                right: activeCol + width - 1,
            };
        }
    }

    /**
     * Clipboard Plugin
     *
     * This clipboard manages all cut/copy/paste interactions internal to the
     * application, and with the OS clipboard as well.
     */
    class ClipboardPlugin extends UIPlugin {
        static layers = [2 /* LAYERS.Clipboard */];
        static getters = [
            "getClipboardContent",
            "getClipboardTextContent",
            "isCutOperation",
            "isPaintingFormat",
        ];
        status = "invisible";
        state;
        lastPasteState;
        paintFormatStatus = "inactive";
        originSheetId;
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "CUT":
                    const zones = this.getters.getSelectedZones();
                    const state = this.getClipboardState(zones, cmd.type);
                    return state.isCutAllowed(zones);
                case "PASTE":
                    if (!this.state) {
                        return "EmptyClipboard" /* CommandResult.EmptyClipboard */;
                    }
                    const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
                    return this.state.isPasteAllowed(cmd.target, { pasteOption });
                case "PASTE_FROM_OS_CLIPBOARD": {
                    const state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection);
                    return state.isPasteAllowed(cmd.target, { pasteOption: cmd.pasteOption });
                }
                case "COPY_PASTE_CELLS_ABOVE": {
                    const zones = this.getters.getSelectedZones();
                    if (zones.length > 1 || (zones[0].top === 0 && zones[0].bottom === 0)) {
                        return "InvalidCopyPasteSelection" /* CommandResult.InvalidCopyPasteSelection */;
                    }
                    break;
                }
                case "COPY_PASTE_CELLS_ON_LEFT": {
                    const zones = this.getters.getSelectedZones();
                    if (zones.length > 1 || (zones[0].left === 0 && zones[0].right === 0)) {
                        return "InvalidCopyPasteSelection" /* CommandResult.InvalidCopyPasteSelection */;
                    }
                    break;
                }
                case "INSERT_CELL": {
                    const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);
                    const state = this.getClipboardStateForCopyCells(cut, "CUT");
                    return state.isPasteAllowed(paste);
                }
                case "DELETE_CELL": {
                    const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);
                    const state = this.getClipboardStateForCopyCells(cut, "CUT");
                    return state.isPasteAllowed(paste);
                }
                case "ACTIVATE_PAINT_FORMAT": {
                    if (this.paintFormatStatus !== "inactive") {
                        return "AlreadyInPaintingFormatMode" /* CommandResult.AlreadyInPaintingFormatMode */;
                    }
                }
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "COPY":
                case "CUT":
                    const zones = this.getters.getSelectedZones();
                    this.state = this.getClipboardState(zones, cmd.type);
                    this.status = "visible";
                    this.originSheetId = this.getters.getActiveSheetId();
                    break;
                case "PASTE":
                    if (!this.state) {
                        break;
                    }
                    const pasteOption = cmd.pasteOption || (this.paintFormatStatus !== "inactive" ? "onlyFormat" : undefined);
                    this.state.paste(cmd.target, { pasteOption, shouldPasteCF: true, selectTarget: true });
                    if (this.state.operation === "CUT") {
                        this.state = undefined;
                    }
                    this.lastPasteState = this.state;
                    if (this.paintFormatStatus === "oneOff") {
                        this.paintFormatStatus = "inactive";
                    }
                    this.status = "invisible";
                    break;
                case "COPY_PASTE_CELLS_ABOVE":
                    {
                        const zone = this.getters.getSelectedZone();
                        const multipleRowsInSelection = zone.top !== zone.bottom;
                        const copyTarget = {
                            ...zone,
                            bottom: multipleRowsInSelection ? zone.top : zone.top - 1,
                            top: multipleRowsInSelection ? zone.top : zone.top - 1,
                        };
                        const state = this.getClipboardStateForCopyCells([copyTarget], "COPY");
                        state.paste([zone], {
                            pasteOption: undefined,
                            shouldPasteCF: true,
                            selectTarget: true,
                        });
                    }
                    break;
                case "COPY_PASTE_CELLS_ON_LEFT":
                    {
                        const zone = this.getters.getSelectedZone();
                        const multipleColsInSelection = zone.left !== zone.right;
                        const copyTarget = {
                            ...zone,
                            right: multipleColsInSelection ? zone.left : zone.left - 1,
                            left: multipleColsInSelection ? zone.left : zone.left - 1,
                        };
                        const state = this.getClipboardStateForCopyCells([copyTarget], "COPY");
                        state.paste([zone], {
                            pasteOption: undefined,
                            shouldPasteCF: true,
                            selectTarget: true,
                        });
                    }
                    break;
                case "CLEAN_CLIPBOARD_HIGHLIGHT":
                    this.status = "invisible";
                    break;
                case "DELETE_CELL": {
                    const { cut, paste } = this.getDeleteCellsTargets(cmd.zone, cmd.shiftDimension);
                    if (!isZoneValid(cut[0])) {
                        for (const { col, row } of positions(cmd.zone)) {
                            this.dispatch("CLEAR_CELL", { col, row, sheetId: this.getters.getActiveSheetId() });
                        }
                        break;
                    }
                    const state = this.getClipboardStateForCopyCells(cut, "CUT");
                    state.paste(paste);
                    break;
                }
                case "INSERT_CELL": {
                    const { cut, paste } = this.getInsertCellsTargets(cmd.zone, cmd.shiftDimension);
                    const state = this.getClipboardStateForCopyCells(cut, "CUT");
                    state.paste(paste);
                    break;
                }
                case "ADD_COLUMNS_ROWS": {
                    this.status = "invisible";
                    // If we add a col/row inside or before the cut area, we invalidate the clipboard
                    if (this.state?.operation !== "CUT") {
                        return;
                    }
                    const isClipboardDirty = this.state.isColRowDirtyingClipboard(cmd.position === "before" ? cmd.base : cmd.base + 1, cmd.dimension);
                    if (isClipboardDirty) {
                        this.state = undefined;
                    }
                    break;
                }
                case "REMOVE_COLUMNS_ROWS": {
                    this.status = "invisible";
                    // If we remove a col/row inside or before the cut area, we invalidate the clipboard
                    if (this.state?.operation !== "CUT") {
                        return;
                    }
                    for (let el of cmd.elements) {
                        const isClipboardDirty = this.state.isColRowDirtyingClipboard(el, cmd.dimension);
                        if (isClipboardDirty) {
                            this.state = undefined;
                            break;
                        }
                    }
                    this.status = "invisible";
                    break;
                }
                case "PASTE_FROM_OS_CLIPBOARD":
                    this.state = new ClipboardOsState(cmd.text, this.getters, this.dispatch, this.selection);
                    this.state.paste(cmd.target, { pasteOption: cmd.pasteOption });
                    this.lastPasteState = this.state;
                    this.status = "invisible";
                    break;
                case "REPEAT_PASTE": {
                    this.lastPasteState?.paste(cmd.target, {
                        pasteOption: cmd.pasteOption,
                        shouldPasteCF: true,
                        selectTarget: true,
                    });
                    break;
                }
                case "ACTIVATE_PAINT_FORMAT": {
                    const zones = this.getters.getSelectedZones();
                    this.state = this.getClipboardStateForCopyCells(zones, "COPY");
                    this.status = "visible";
                    if (cmd.persistent) {
                        this.paintFormatStatus = "persistent";
                    }
                    else {
                        this.paintFormatStatus = "oneOff";
                    }
                    break;
                }
                case "DELETE_SHEET":
                    if (this.state?.operation !== "CUT") {
                        return;
                    }
                    if (this.originSheetId === cmd.sheetId) {
                        this.state = undefined;
                        this.status = "invisible";
                    }
                    break;
                case "CANCEL_PAINT_FORMAT": {
                    this.paintFormatStatus = "inactive";
                    this.status = "invisible";
                    break;
                }
                default:
                    if (isCoreCommand(cmd)) {
                        this.status = "invisible";
                    }
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        /**
         * Format the current clipboard to a string suitable for being pasted in other
         * programs.
         *
         * - add a tab character between each consecutive cells
         * - add a newline character between each line
         *
         * Note that it returns \t if the clipboard is empty. This is necessary for the
         * clipboard copy event to add it as data, otherwise an empty string is not
         * considered as a copy content.
         */
        getClipboardContent() {
            return this.state?.getClipboardContent() || { [ClipboardMIMEType.PlainText]: "\t" };
        }
        getClipboardTextContent() {
            return this.state?.getClipboardContent()[ClipboardMIMEType.PlainText] || "\t";
        }
        isCutOperation() {
            return this.state ? this.state.operation === "CUT" : false;
        }
        isPaintingFormat() {
            return this.paintFormatStatus !== "inactive";
        }
        // ---------------------------------------------------------------------------
        // Private methods
        // ---------------------------------------------------------------------------
        getDeleteCellsTargets(zone, dimension) {
            const sheetId = this.getters.getActiveSheetId();
            let cut;
            if (dimension === "COL") {
                cut = {
                    ...zone,
                    left: zone.right + 1,
                    right: this.getters.getNumberCols(sheetId) - 1,
                };
            }
            else {
                cut = {
                    ...zone,
                    top: zone.bottom + 1,
                    bottom: this.getters.getNumberRows(sheetId) - 1,
                };
            }
            return { cut: [cut], paste: [zone] };
        }
        getInsertCellsTargets(zone, dimension) {
            const sheetId = this.getters.getActiveSheetId();
            let cut;
            let paste;
            if (dimension === "COL") {
                cut = {
                    ...zone,
                    right: this.getters.getNumberCols(sheetId) - 1,
                };
                paste = {
                    ...zone,
                    left: zone.right + 1,
                    right: zone.right + 1,
                };
            }
            else {
                cut = {
                    ...zone,
                    bottom: this.getters.getNumberRows(sheetId) - 1,
                };
                paste = { ...zone, top: zone.bottom + 1, bottom: this.getters.getNumberRows(sheetId) - 1 };
            }
            return { cut: [cut], paste: [paste] };
        }
        getClipboardStateForCopyCells(zones, operation) {
            return new ClipboardCellsState(zones, operation, this.getters, this.dispatch, this.selection);
        }
        /**
         * Get the clipboard state from the given zones.
         */
        getClipboardState(zones, operation) {
            const selectedFigureId = this.getters.getSelectedFigureId();
            if (selectedFigureId) {
                return new ClipboardFigureState(operation, this.getters, this.dispatch);
            }
            return new ClipboardCellsState(zones, operation, this.getters, this.dispatch, this.selection);
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(renderingContext) {
            if (this.status !== "visible" || !this.state) {
                return;
            }
            this.state.drawClipboard(renderingContext);
        }
    }

    /**
     * Change the reference types inside the given token, if the token represent a range or a cell
     *
     * Eg. :
     *   A1 => $A$1 => A$1 => $A1 => A1
     *   A1:$B$1 => $A$1:B$1 => A$1:$B1 => $A1:B1 => A1:$B$1
     */
    function loopThroughReferenceType(token) {
        if (token.type !== "REFERENCE")
            return token;
        const { xc, sheetName } = splitReference(token.value);
        const [left, right] = xc.split(":");
        const sheetRef = sheetName ? `${getCanonicalSheetName(sheetName)}!` : "";
        const updatedLeft = getTokenNextReferenceType(left);
        const updatedRight = right ? `:${getTokenNextReferenceType(right)}` : "";
        return { ...token, value: sheetRef + updatedLeft + updatedRight };
    }
    /**
     * Get a new token with a changed type of reference from the given cell token symbol.
     * Undefined behavior if given a token other than a cell or if the Xc contains a sheet reference
     *
     * A1 => $A$1 => A$1 => $A1 => A1
     */
    function getTokenNextReferenceType(xc) {
        switch (getReferenceType(xc)) {
            case "none":
                xc = setXcToReferenceType(xc, "colrow");
                break;
            case "colrow":
                xc = setXcToReferenceType(xc, "row");
                break;
            case "row":
                xc = setXcToReferenceType(xc, "col");
                break;
            case "col":
                xc = setXcToReferenceType(xc, "none");
                break;
        }
        return xc;
    }
    /**
     * Returns the given XC with the given reference type.
     */
    function setXcToReferenceType(xc, referenceType) {
        xc = xc.replace(/\$/g, "");
        let indexOfNumber;
        switch (referenceType) {
            case "col":
                return "$" + xc;
            case "row":
                indexOfNumber = xc.search(/[0-9]/);
                return xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
            case "colrow":
                indexOfNumber = xc.search(/[0-9]/);
                if (indexOfNumber === -1 || indexOfNumber === 0) {
                    // no row number (eg. A) or no column (eg. 1)
                    return "$" + xc;
                }
                xc = xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
                return "$" + xc;
            case "none":
                return xc;
        }
    }
    /**
     * Return the type of reference used in the given XC of a cell.
     * Undefined behavior if the XC have a sheet reference
     */
    function getReferenceType(xcCell) {
        if (isColAndRowFixed(xcCell)) {
            return "colrow";
        }
        else if (isColFixed(xcCell)) {
            return "col";
        }
        else if (isRowFixed(xcCell)) {
            return "row";
        }
        return "none";
    }
    function isColFixed(xc) {
        return xc.startsWith("$");
    }
    function isRowFixed(xc) {
        return xc.includes("$", 1);
    }
    function isColAndRowFixed(xc) {
        return xc.startsWith("$") && xc.length > 1 && xc.slice(1).includes("$");
    }

    const CELL_DELETED_MESSAGE = _t("The cell you are trying to edit has been deleted.");
    class EditionPlugin extends UIPlugin {
        static getters = [
            "getEditionMode",
            "isSelectingForComposer",
            "showSelectionIndicator",
            "getCurrentContent",
            "getComposerSelection",
            "getCurrentTokens",
            "getTokenAtCursor",
            "getComposerHighlights",
            "getCurrentEditedCell",
            "getCycledReference",
            "getAutoCompleteDataValidationValues",
        ];
        col = 0;
        row = 0;
        mode = "inactive";
        sheetId = "";
        currentContent = "";
        currentTokens = [];
        selectionStart = 0;
        selectionEnd = 0;
        initialContent = "";
        colorIndexByRange = {};
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "CHANGE_COMPOSER_CURSOR_SELECTION":
                    return this.validateSelection(this.currentContent.length, cmd.start, cmd.end);
                case "SET_CURRENT_CONTENT":
                    if (cmd.selection) {
                        return this.validateSelection(cmd.content.length, cmd.selection.start, cmd.selection.end);
                    }
                    break;
                case "START_EDITION":
                    if (cmd.selection) {
                        const content = cmd.text || this.getComposerContent(this.getters.getActivePosition());
                        return this.validateSelection(content.length, cmd.selection.start, cmd.selection.end);
                    }
                    break;
                case "STOP_EDITION":
                    if (this.mode === "inactive") {
                        return "Success" /* CommandResult.Success */;
                    }
                    return this.checkDataValidation(cmd);
            }
            return "Success" /* CommandResult.Success */;
        }
        handleEvent(event) {
            const sheetId = this.getters.getActiveSheetId();
            let unboundedZone;
            if (event.options.unbounded) {
                unboundedZone = this.getters.getUnboundedZone(sheetId, event.anchor.zone);
            }
            else {
                unboundedZone = event.anchor.zone;
            }
            switch (event.mode) {
                case "newAnchor":
                    if (this.mode === "selecting") {
                        this.insertSelectedRange(unboundedZone);
                    }
                    break;
                default:
                    if (this.mode === "selecting") {
                        this.replaceSelectedRange(unboundedZone);
                    }
                    else {
                        this.updateComposerRange(event.previousAnchor.zone, unboundedZone);
                    }
                    break;
            }
        }
        handle(cmd) {
            switch (cmd.type) {
                case "CHANGE_COMPOSER_CURSOR_SELECTION":
                    this.selectionStart = cmd.start;
                    this.selectionEnd = cmd.end;
                    break;
                case "STOP_COMPOSER_RANGE_SELECTION":
                    if (this.isSelectingForComposer()) {
                        this.mode = "editing";
                    }
                    break;
                case "START_EDITION":
                    if (this.mode !== "inactive" && cmd.text) {
                        this.setContent(cmd.text, cmd.selection);
                    }
                    else {
                        this.startEdition(cmd.text, cmd.selection);
                    }
                    this.updateRangeColor();
                    break;
                case "STOP_EDITION":
                    this.stopEdition();
                    this.colorIndexByRange = {};
                    break;
                case "CANCEL_EDITION":
                    this.cancelEditionAndActivateSheet();
                    this.resetContent();
                    this.colorIndexByRange = {};
                    break;
                case "SET_CURRENT_CONTENT":
                    this.setContent(cmd.content, cmd.selection, true);
                    this.updateRangeColor();
                    break;
                case "REPLACE_COMPOSER_CURSOR_SELECTION":
                    this.replaceSelection(cmd.text);
                    break;
                case "SELECT_FIGURE":
                    this.cancelEditionAndActivateSheet();
                    this.resetContent();
                    break;
                case "ADD_COLUMNS_ROWS":
                    this.onAddElements(cmd);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                    if (cmd.dimension === "COL") {
                        this.onColumnsRemoved(cmd);
                    }
                    else {
                        this.onRowsRemoved(cmd);
                    }
                    break;
                case "START_CHANGE_HIGHLIGHT":
                    const { left, top } = cmd.zone;
                    // changing the highlight can conflit with the 'selecting' mode
                    if (this.isSelectingForComposer()) {
                        this.mode = "editing";
                    }
                    this.selection.resetAnchor(this, { cell: { col: left, row: top }, zone: cmd.zone });
                    break;
                case "ACTIVATE_SHEET":
                    if (!this.currentContent.startsWith("=")) {
                        this.cancelEdition();
                        this.resetContent();
                    }
                    if (cmd.sheetIdFrom !== cmd.sheetIdTo) {
                        const activePosition = this.getters.getActivePosition();
                        const { col, row } = this.getters.getNextVisibleCellPosition({
                            sheetId: cmd.sheetIdTo,
                            col: activePosition.col,
                            row: activePosition.row,
                        });
                        const zone = this.getters.expandZone(cmd.sheetIdTo, positionToZone({ col, row }));
                        this.selection.resetAnchor(this, { cell: { col, row }, zone });
                    }
                    break;
                case "DELETE_SHEET":
                case "UNDO":
                case "REDO":
                    const sheetIdExists = !!this.getters.tryGetSheet(this.sheetId);
                    if (!sheetIdExists && this.mode !== "inactive") {
                        this.sheetId = this.getters.getActiveSheetId();
                        this.cancelEditionAndActivateSheet();
                        this.resetContent();
                        this.ui.raiseBlockingErrorUI(CELL_DELETED_MESSAGE);
                    }
                    break;
                case "CYCLE_EDITION_REFERENCES":
                    this.cycleReferences();
                    break;
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        getEditionMode() {
            return this.mode;
        }
        getCurrentContent() {
            if (this.mode === "inactive") {
                return this.getComposerContent(this.getters.getActivePosition());
            }
            return this.currentContent;
        }
        getComposerSelection() {
            return {
                start: this.selectionStart,
                end: this.selectionEnd,
            };
        }
        getCurrentEditedCell() {
            return {
                sheetId: this.sheetId,
                col: this.col,
                row: this.row,
            };
        }
        isSelectingForComposer() {
            return this.mode === "selecting";
        }
        showSelectionIndicator() {
            return this.isSelectingForComposer() && this.canStartComposerRangeSelection();
        }
        getCurrentTokens() {
            return this.currentTokens;
        }
        /**
         * Return the (enriched) token just before the cursor.
         */
        getTokenAtCursor() {
            const start = Math.min(this.selectionStart, this.selectionEnd);
            const end = Math.max(this.selectionStart, this.selectionEnd);
            if (start === end && end === 0) {
                return undefined;
            }
            else {
                return this.currentTokens.find((t) => t.start <= start && t.end >= end);
            }
        }
        /**
         * Return the cycled reference if any (A1 -> $A$1 -> A$1 -> $A1 -> A1)
         */
        getCycledReference(selection, content) {
            const locale = this.getters.getLocale();
            const currentTokens = content.startsWith("=") ? composerTokenize(content, locale) : [];
            const tokens = currentTokens.filter((t) => (t.start <= selection.start && t.end >= selection.start) ||
                (t.start >= selection.start && t.start < selection.end));
            const refTokens = tokens.filter((token) => token.type === "REFERENCE");
            if (refTokens.length === 0) {
                return;
            }
            const updatedReferences = tokens
                .map(loopThroughReferenceType)
                .map((token) => token.value)
                .join("");
            const start = tokens[0].start;
            const end = tokens[tokens.length - 1].end;
            const newContent = content.slice(0, start) + updatedReferences + content.slice(end);
            const lengthDiff = newContent.length - content.length;
            const startOfTokens = refTokens[0].start;
            const endOfTokens = refTokens[refTokens.length - 1].end + lengthDiff;
            const newSelection = { start: startOfTokens, end: endOfTokens };
            if (refTokens.length === 1 && selection.start === selection.end) {
                newSelection.start = newSelection.end;
            }
            return { content: newContent, selection: newSelection };
        }
        getInitialComposerContent() {
            return this.initialContent;
        }
        // ---------------------------------------------------------------------------
        // Misc
        // ---------------------------------------------------------------------------
        cycleReferences() {
            const updated = this.getCycledReference(this.getComposerSelection(), this.currentContent);
            if (updated === undefined) {
                return;
            }
            this.dispatch("SET_CURRENT_CONTENT", {
                content: updated.content,
                selection: updated.selection,
            });
        }
        validateSelection(length, start, end) {
            return start >= 0 && start <= length && end >= 0 && end <= length
                ? "Success" /* CommandResult.Success */
                : "WrongComposerSelection" /* CommandResult.WrongComposerSelection */;
        }
        onColumnsRemoved(cmd) {
            if (cmd.elements.includes(this.col) && this.mode !== "inactive") {
                this.dispatch("CANCEL_EDITION");
                this.ui.raiseBlockingErrorUI(CELL_DELETED_MESSAGE);
                return;
            }
            const { top, left } = updateSelectionOnDeletion({ left: this.col, right: this.col, top: this.row, bottom: this.row }, "left", [...cmd.elements]);
            this.col = left;
            this.row = top;
        }
        onRowsRemoved(cmd) {
            if (cmd.elements.includes(this.row) && this.mode !== "inactive") {
                this.dispatch("CANCEL_EDITION");
                this.ui.raiseBlockingErrorUI(CELL_DELETED_MESSAGE);
                return;
            }
            const { top, left } = updateSelectionOnDeletion({ left: this.col, right: this.col, top: this.row, bottom: this.row }, "top", [...cmd.elements]);
            this.col = left;
            this.row = top;
        }
        onAddElements(cmd) {
            const { top, left } = updateSelectionOnInsertion({ left: this.col, right: this.col, top: this.row, bottom: this.row }, cmd.dimension === "COL" ? "left" : "top", cmd.base, cmd.position, cmd.quantity);
            this.col = left;
            this.row = top;
        }
        /**
         * Enable the selecting mode
         */
        startComposerRangeSelection() {
            if (this.sheetId === this.getters.getActiveSheetId()) {
                const zone = positionToZone({ col: this.col, row: this.row });
                this.selection.resetAnchor(this, { cell: { col: this.col, row: this.row }, zone });
            }
            this.mode = "selecting";
        }
        /**
         * start the edition of a cell
         * @param str the key that is used to start the edition if it is a "content" key like a letter or number
         * @param selection
         * @private
         */
        startEdition(str, selection) {
            const evaluatedCell = this.getters.getActiveCell();
            const locale = this.getters.getLocale();
            if (str && evaluatedCell.format?.includes("%") && isNumber(str, locale)) {
                selection = selection || { start: str.length, end: str.length };
                str = `${str}%`;
            }
            const { col, row, sheetId } = this.getters.getActivePosition();
            this.col = col;
            this.sheetId = sheetId;
            this.row = row;
            this.initialContent = this.getComposerContent({ sheetId, col, row });
            this.mode = "editing";
            this.setContent(str || this.initialContent, selection);
            this.colorIndexByRange = {};
            const zone = positionToZone({ col: this.col, row: this.row });
            this.selection.capture(this, { cell: { col: this.col, row: this.row }, zone }, {
                handleEvent: this.handleEvent.bind(this),
                release: () => {
                    this.stopEdition();
                },
            });
        }
        stopEdition() {
            if (this.mode !== "inactive") {
                this.cancelEditionAndActivateSheet();
                const col = this.col;
                const row = this.row;
                let content = this.getCurrentCanonicalContent();
                const didChange = this.initialContent !== content;
                if (!didChange) {
                    return;
                }
                if (content) {
                    const sheetId = this.getters.getActiveSheetId();
                    const cell = this.getters.getEvaluatedCell({ sheetId, col: this.col, row: this.row });
                    if (content.startsWith("=")) {
                        const left = this.currentTokens.filter((t) => t.type === "LEFT_PAREN").length;
                        const right = this.currentTokens.filter((t) => t.type === "RIGHT_PAREN").length;
                        const missing = left - right;
                        if (missing > 0) {
                            content += concat(new Array(missing).fill(")"));
                        }
                    }
                    else if (cell.link) {
                        content = markdownLink(content, cell.link.url);
                    }
                    this.dispatch("UPDATE_CELL", {
                        sheetId: this.sheetId,
                        col,
                        row,
                        content,
                    });
                }
                else {
                    this.dispatch("UPDATE_CELL", {
                        sheetId: this.sheetId,
                        content: "",
                        col,
                        row,
                    });
                }
                this.setContent("");
            }
        }
        getCurrentCanonicalContent() {
            return canonicalizeNumberContent(this.currentContent, this.getters.getLocale());
        }
        cancelEditionAndActivateSheet() {
            if (this.mode === "inactive") {
                return;
            }
            this.cancelEdition();
            const sheetId = this.getters.getActiveSheetId();
            if (sheetId !== this.sheetId) {
                this.dispatch("ACTIVATE_SHEET", {
                    sheetIdFrom: this.getters.getActiveSheetId(),
                    sheetIdTo: this.sheetId,
                });
            }
        }
        getComposerContent(position) {
            const locale = this.getters.getLocale();
            const cell = this.getters.getCell(position);
            if (cell?.isFormula) {
                return localizeFormula(cell.content, locale);
            }
            const { format, value, type, formattedValue } = this.getters.getEvaluatedCell(position);
            switch (type) {
                case CellValueType.text:
                case CellValueType.empty:
                    return value;
                case CellValueType.boolean:
                    return formattedValue;
                case CellValueType.error:
                    return cell?.content || "";
                case CellValueType.number:
                    if (format && isDateTimeFormat(format)) {
                        if (parseDateTime(formattedValue, locale) !== null) {
                            // formatted string can be parsed again
                            return formattedValue;
                        }
                        // display a simplified and parsable string otherwise
                        const timeFormat = Number.isInteger(value)
                            ? locale.dateFormat
                            : getDateTimeFormat(locale);
                        return formatValue(value, { locale, format: timeFormat });
                    }
                    return this.numberComposerContent(value, format, locale);
            }
        }
        numberComposerContent(value, format, locale) {
            if (format?.includes("%")) {
                return `${numberToString(value * 100, locale.decimalSeparator)}%`;
            }
            return numberToString(value, locale.decimalSeparator);
        }
        cancelEdition() {
            if (this.mode === "inactive") {
                return;
            }
            this.mode = "inactive";
            this.selection.release(this);
        }
        /**
         * Reset the current content to the active cell content
         */
        resetContent() {
            this.setContent(this.initialContent || "");
        }
        setContent(text, selection, raise) {
            const isNewCurrentContent = this.currentContent !== text;
            this.currentContent = text;
            if (selection) {
                this.selectionStart = selection.start;
                this.selectionEnd = selection.end;
            }
            else {
                this.selectionStart = this.selectionEnd = text.length;
            }
            if (isNewCurrentContent || this.mode !== "inactive") {
                const locale = this.getters.getLocale();
                this.currentTokens = text.startsWith("=") ? composerTokenize(text, locale) : [];
                if (this.currentTokens.length > 100) {
                    if (raise) {
                        this.ui.raiseBlockingErrorUI(_t("This formula has over 100 parts. It can't be processed properly, consider splitting it into multiple cells"));
                    }
                }
            }
            if (this.canStartComposerRangeSelection()) {
                this.startComposerRangeSelection();
            }
        }
        insertSelectedRange(zone) {
            // infer if range selected or selecting range from cursor position
            const start = Math.min(this.selectionStart, this.selectionEnd);
            const ref = this.getZoneReference(zone);
            if (this.canStartComposerRangeSelection()) {
                this.insertText(ref, start);
            }
            else {
                this.insertText("," + ref, start);
            }
        }
        /**
         * Replace the current reference selected by the new one.
         * */
        replaceSelectedRange(zone) {
            const ref = this.getZoneReference(zone);
            const currentToken = this.getTokenAtCursor();
            const start = currentToken?.type === "REFERENCE" ? currentToken.start : this.selectionStart;
            this.replaceText(ref, start, this.selectionEnd);
        }
        /**
         * Replace the reference of the old zone by the new one.
         */
        updateComposerRange(oldZone, newZone) {
            const activeSheetId = this.getters.getActiveSheetId();
            const tokentAtCursor = this.getTokenAtCursor();
            const tokens = tokentAtCursor ? [tokentAtCursor, ...this.currentTokens] : this.currentTokens;
            const previousRefToken = tokens
                .filter((token) => token.type === "REFERENCE")
                .find((token) => {
                const { xc, sheetName: sheet } = splitReference(token.value);
                const sheetName = sheet || this.getters.getSheetName(this.sheetId);
                if (this.getters.getSheetName(activeSheetId) !== sheetName) {
                    return false;
                }
                const refRange = this.getters.getRangeFromSheetXC(activeSheetId, xc);
                return isEqual(this.getters.expandZone(activeSheetId, refRange.zone), oldZone);
            });
            // this function assumes that the previous range is always found because
            // it's called when changing a highlight, which exists by definition
            if (!previousRefToken) {
                throw new Error("Previous range not found");
            }
            const previousRange = this.getters.getRangeFromSheetXC(activeSheetId, previousRefToken.value);
            this.selectionStart = previousRefToken.start;
            this.selectionEnd = this.selectionStart + previousRefToken.value.length;
            const newRange = this.getters.getRangeFromZone(activeSheetId, newZone);
            const newRef = this.getRangeReference(newRange, previousRange.parts);
            this.replaceSelection(newRef);
        }
        getZoneReference(zone) {
            const inputSheetId = this.getters.getCurrentEditedCell().sheetId;
            const sheetId = this.getters.getActiveSheetId();
            const range = this.getters.getRangeFromZone(sheetId, zone);
            return this.getters.getSelectionRangeString(range, inputSheetId);
        }
        getRangeReference(range, fixedParts) {
            let _fixedParts = [...fixedParts];
            const newRange = range.clone({ parts: _fixedParts });
            return this.getters.getSelectionRangeString(newRange, this.getters.getCurrentEditedCell().sheetId);
        }
        /**
         * Replace the current selection by a new text.
         * The cursor is then set at the end of the text.
         */
        replaceSelection(text) {
            const start = Math.min(this.selectionStart, this.selectionEnd);
            const end = Math.max(this.selectionStart, this.selectionEnd);
            this.replaceText(text, start, end);
        }
        replaceText(text, start, end) {
            this.currentContent =
                this.currentContent.slice(0, start) +
                    this.currentContent.slice(end, this.currentContent.length);
            this.insertText(text, start);
        }
        /**
         * Insert a text at the given position.
         * The cursor is then set at the end of the text.
         */
        insertText(text, start) {
            const content = this.currentContent.slice(0, start) + text + this.currentContent.slice(start);
            const end = start + text.length;
            this.dispatch("SET_CURRENT_CONTENT", {
                content,
                selection: { start: end, end },
            });
        }
        updateRangeColor() {
            if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
                return;
            }
            const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
            const XCs = this.getReferencedRanges().map((range) => this.getters.getRangeString(range, editionSheetId));
            const colorsToKeep = {};
            for (const xc of XCs) {
                if (this.colorIndexByRange[xc] !== undefined) {
                    colorsToKeep[xc] = this.colorIndexByRange[xc];
                }
            }
            const usedIndexes = new Set(Object.values(colorsToKeep));
            let currentIndex = 0;
            const nextIndex = () => {
                while (usedIndexes.has(currentIndex))
                    currentIndex++;
                usedIndexes.add(currentIndex);
                return currentIndex;
            };
            for (const xc of XCs) {
                const colorIndex = xc in colorsToKeep ? colorsToKeep[xc] : nextIndex();
                colorsToKeep[xc] = colorIndex;
            }
            this.colorIndexByRange = colorsToKeep;
        }
        /**
         * Highlight all ranges that can be found in the composer content.
         */
        getComposerHighlights() {
            if (!this.currentContent.startsWith("=") || this.mode === "inactive") {
                return [];
            }
            const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
            const rangeColor = (rangeString) => {
                const colorIndex = this.colorIndexByRange[rangeString];
                return colors$1[colorIndex % colors$1.length];
            };
            return this.getReferencedRanges().map((range) => {
                const rangeString = this.getters.getRangeString(range, editionSheetId);
                return {
                    zone: range.zone,
                    color: rangeColor(rangeString),
                    sheetId: range.sheetId,
                };
            });
        }
        /**
         * Return ranges currently referenced in the composer
         */
        getReferencedRanges() {
            const editionSheetId = this.getters.getCurrentEditedCell().sheetId;
            const referenceRanges = this.currentTokens
                .filter((token) => token.type === "REFERENCE")
                .map((token) => this.getters.getRangeFromSheetXC(editionSheetId, token.value));
            return referenceRanges.filter((range) => !range.invalidSheetName && !range.invalidXc);
        }
        getAutoCompleteDataValidationValues() {
            if (this.mode === "inactive") {
                return [];
            }
            const rule = this.getters.getValidationRuleForCell(this.getCurrentEditedCell());
            if (!rule ||
                (rule.criterion.type !== "isValueInList" && rule.criterion.type !== "isValueInRange")) {
                return [];
            }
            let values;
            if (rule.criterion.type === "isValueInList") {
                values = rule.criterion.values;
            }
            else {
                const range = this.getters.getRangeFromSheetXC(this.sheetId, rule.criterion.values[0]);
                values = this.getters
                    .getRangeValues(range)
                    .filter(isNotNull)
                    .map((value) => value.toString())
                    .filter((val) => val !== "");
            }
            const composerContent = this.getCurrentContent();
            if (composerContent && composerContent !== this.getInitialComposerContent()) {
                const filteredValues = fuzzyLookup(composerContent, values, (val) => val);
                values = filteredValues.length ? filteredValues : values;
            }
            return values;
        }
        /**
         * Function used to determine when composer selection can start.
         * Three conditions are necessary:
         * - the previous token is among ["ARG_SEPARATOR", "LEFT_PAREN", "OPERATOR"], and is not a postfix unary operator
         * - the next token is missing or is among ["ARG_SEPARATOR", "RIGHT_PAREN", "OPERATOR"]
         * - Previous and next tokens can be separated by spaces
         */
        canStartComposerRangeSelection() {
            if (this.currentContent.startsWith("=")) {
                const tokenAtCursor = this.getTokenAtCursor();
                if (!tokenAtCursor) {
                    return false;
                }
                const tokenIdex = this.currentTokens.map((token) => token.start).indexOf(tokenAtCursor.start);
                let count = tokenIdex;
                let currentToken = tokenAtCursor;
                // check previous token
                while (!["ARG_SEPARATOR", "LEFT_PAREN", "OPERATOR"].includes(currentToken.type) ||
                    POSTFIX_UNARY_OPERATORS.includes(currentToken.value)) {
                    if (currentToken.type !== "SPACE" || count < 1) {
                        return false;
                    }
                    count--;
                    currentToken = this.currentTokens[count];
                }
                count = tokenIdex + 1;
                currentToken = this.currentTokens[count];
                // check next token
                while (currentToken &&
                    !["ARG_SEPARATOR", "RIGHT_PAREN", "OPERATOR"].includes(currentToken.type)) {
                    if (currentToken.type !== "SPACE") {
                        return false;
                    }
                    count++;
                    currentToken = this.currentTokens[count];
                }
                return true;
            }
            return false;
        }
        checkDataValidation(cmd) {
            const cellPosition = { sheetId: this.sheetId, col: this.col, row: this.row };
            try {
                const content = this.getCurrentCanonicalContent();
                const cellValue = content.startsWith("=")
                    ? this.getters.evaluateFormula(this.sheetId, content)
                    : parseLiteral(content, this.getters.getLocale());
                if (isMatrix(cellValue)) {
                    return "Success" /* CommandResult.Success */;
                }
                const validationResult = this.getters.getValidationResultForCellValue(cellValue, cellPosition);
                if (!validationResult.isValid && validationResult.rule.isBlocking) {
                    return "BlockingValidationRule" /* CommandResult.BlockingValidationRule */;
                }
                return "Success" /* CommandResult.Success */;
            }
            catch (e) {
                // error at formula evaluation
                const rule = this.getters.getValidationRuleForCell(cellPosition);
                return rule?.isBlocking ? "BlockingValidationRule" /* CommandResult.BlockingValidationRule */ : "Success" /* CommandResult.Success */;
            }
        }
    }

    class FilterEvaluationPlugin extends UIPlugin {
        static getters = [
            "getCellBorderWithFilterBorder",
            "getFilterValues",
            "isRowFiltered",
            "isFilterActive",
        ];
        filterValues = {};
        hiddenRows = new Set();
        isEvaluationDirty = false;
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "UPDATE_FILTER":
                    if (!this.getters.getFilterId(cmd)) {
                        return "FilterNotFound" /* CommandResult.FilterNotFound */;
                    }
                    break;
            }
            return "Success" /* CommandResult.Success */;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "UNDO":
                case "REDO":
                case "UPDATE_CELL":
                case "EVALUATE_CELLS":
                case "ACTIVATE_SHEET":
                case "REMOVE_FILTER_TABLE":
                case "ADD_COLUMNS_ROWS":
                case "REMOVE_COLUMNS_ROWS":
                    this.isEvaluationDirty = true;
                    break;
                case "START":
                    for (const sheetId of this.getters.getSheetIds()) {
                        this.filterValues[sheetId] = {};
                        for (const filter of this.getters.getFilters(sheetId)) {
                            this.filterValues[sheetId][filter.id] = [];
                        }
                    }
                    break;
                case "CREATE_SHEET":
                    this.filterValues[cmd.sheetId] = {};
                    break;
                case "HIDE_COLUMNS_ROWS":
                case "UNHIDE_COLUMNS_ROWS":
                case "GROUP_HEADERS":
                case "UNGROUP_HEADERS":
                case "FOLD_HEADER_GROUP":
                case "UNFOLD_HEADER_GROUP":
                case "FOLD_ALL_HEADER_GROUPS":
                case "UNFOLD_ALL_HEADER_GROUPS":
                    this.updateHiddenRows();
                    break;
                case "UPDATE_FILTER":
                    this.updateFilter(cmd);
                    this.updateHiddenRows();
                    break;
                case "DUPLICATE_SHEET":
                    const filterValues = {};
                    for (const newFilter of this.getters.getFilters(cmd.sheetIdTo)) {
                        const zone = newFilter.zoneWithHeaders;
                        filterValues[newFilter.id] = this.getFilterValues({
                            sheetId: cmd.sheetId,
                            col: zone.left,
                            row: zone.top,
                        });
                    }
                    this.filterValues[cmd.sheetIdTo] = filterValues;
                    break;
                // If we don't handle DELETE_SHEET, on one hand we will have some residual data, on the other hand we keep the data
                // on DELETE_SHEET followed by undo
            }
        }
        finalize() {
            if (this.isEvaluationDirty) {
                this.updateHiddenRows();
                this.isEvaluationDirty = false;
            }
        }
        isRowFiltered(sheetId, row) {
            if (sheetId !== this.getters.getActiveSheetId()) {
                return false;
            }
            return this.hiddenRows.has(row);
        }
        getCellBorderWithFilterBorder(position) {
            const { sheetId, col, row } = position;
            let filterBorder = undefined;
            for (let filters of this.getters.getFilterTables(sheetId)) {
                const zone = filters.zone;
                if (isInside(col, row, zone)) {
                    // The borders should be at the edges of the visible zone of the filter
                    const visibleZone = this.intersectZoneWithViewport(sheetId, zone);
                    filterBorder = {
                        top: row === visibleZone.top ? DEFAULT_FILTER_BORDER_DESC : undefined,
                        bottom: row === visibleZone.bottom ? DEFAULT_FILTER_BORDER_DESC : undefined,
                        left: col === visibleZone.left ? DEFAULT_FILTER_BORDER_DESC : undefined,
                        right: col === visibleZone.right ? DEFAULT_FILTER_BORDER_DESC : undefined,
                    };
                }
            }
            const cellBorder = this.getters.getCellBorder(position);
            // Use removeFalsyAttributes to avoid overwriting filter borders with undefined values
            const border = { ...filterBorder, ...removeFalsyAttributes(cellBorder || {}) };
            return isObjectEmptyRecursive(border) ? null : border;
        }
        getFilterValues(position) {
            const id = this.getters.getFilterId(position);
            const sheetId = position.sheetId;
            if (!id || !this.filterValues[sheetId])
                return [];
            return this.filterValues[sheetId][id] || [];
        }
        isFilterActive(position) {
            const id = this.getters.getFilterId(position);
            const sheetId = position.sheetId;
            return Boolean(id && this.filterValues[sheetId]?.[id]?.length);
        }
        intersectZoneWithViewport(sheetId, zone) {
            return {
                left: this.getters.findVisibleHeader(sheetId, "COL", zone.left, zone.right),
                right: this.getters.findVisibleHeader(sheetId, "COL", zone.right, zone.left),
                top: this.getters.findVisibleHeader(sheetId, "ROW", zone.top, zone.bottom),
                bottom: this.getters.findVisibleHeader(sheetId, "ROW", zone.bottom, zone.top),
            };
        }
        updateFilter({ col, row, hiddenValues, sheetId }) {
            const id = this.getters.getFilterId({ sheetId, col, row });
            if (!id)
                return;
            if (!this.filterValues[sheetId])
                this.filterValues[sheetId] = {};
            this.filterValues[sheetId][id] = hiddenValues;
        }
        updateHiddenRows() {
            const sheetId = this.getters.getActiveSheetId();
            const filters = this.getters
                .getFilters(sheetId)
                .sort((filter1, filter2) => filter1.zoneWithHeaders.top - filter2.zoneWithHeaders.top);
            const hiddenRows = new Set();
            for (let filter of filters) {
                // Disable filters whose header are hidden
                if (hiddenRows.has(filter.zoneWithHeaders.top) ||
                    this.getters.isRowHiddenByUser(sheetId, filter.zoneWithHeaders.top)) {
                    continue;
                }
                const filteredValues = this.filterValues[sheetId]?.[filter.id]?.map(toLowerCase);
                if (!filteredValues || !filter.filteredZone)
                    continue;
                for (let row = filter.filteredZone.top; row <= filter.filteredZone.bottom; row++) {
                    const value = this.getCellValueAsString(sheetId, filter.col, row);
                    if (filteredValues.includes(value)) {
                        hiddenRows.add(row);
                    }
                }
            }
            this.hiddenRows = hiddenRows;
        }
        getCellValueAsString(sheetId, col, row) {
            const value = this.getters.getEvaluatedCell({ sheetId, col, row }).formattedValue;
            return value.toLowerCase();
        }
        exportForExcel(data) {
            for (const sheetData of data.sheets) {
                const sheetId = sheetData.id;
                for (const tableData of sheetData.filterTables) {
                    const tableZone = toZone(tableData.range);
                    const filters = [];
                    const headerNames = [];
                    for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {
                        const position = {
                            sheetId: sheetData.id,
                            col: tableZone.left + i,
                            row: tableZone.top,
                        };
                        const filteredValues = this.getFilterValues(position);
                        const filter = this.getters.getFilter(position);
                        if (!filter)
                            continue;
                        const valuesInFilterZone = filter.filteredZone
                            ? positions(filter.filteredZone).map((position) => this.getters.getEvaluatedCell({ sheetId, ...position }).formattedValue)
                            : [];
                        if (filteredValues.length) {
                            const xlsxDisplayedValues = valuesInFilterZone
                                .filter((val) => val)
                                .filter((val) => !filteredValues.includes(val));
                            filters.push({
                                colId: i,
                                displayedValues: [...new Set(xlsxDisplayedValues)],
                                displayBlanks: !filteredValues.includes("") && valuesInFilterZone.some((val) => !val),
                            });
                        }
                        // In xlsx, filter header should ALWAYS be a string and should be unique in the table
                        const headerPosition = { col: filter.col, row: filter.zoneWithHeaders.top, sheetId };
                        const headerString = this.getters.getEvaluatedCell(headerPosition).formattedValue;
                        const headerName = this.getUniqueColNameForExcel(i, headerString, headerNames);
                        headerNames.push(headerName);
                        sheetData.cells[toXC(headerPosition.col, headerPosition.row)] = {
                            ...sheetData.cells[toXC(headerPosition.col, headerPosition.row)],
                            content: headerName,
                            value: headerName,
                            isFormula: false,
                        };
                    }
                    tableData.filters = filters;
                }
            }
        }
        /**
         * Get an unique column name for the column at colIndex. If the column name is already in the array of used column names,
         * concatenate a number to the name until we find a new unique name (eg. "ColName" => "ColName1" => "ColName2" ...)
         */
        getUniqueColNameForExcel(colIndex, colName, usedColNames) {
            if (!colName) {
                colName = `Column${colIndex}`;
            }
            let currentColName = colName;
            let i = 2;
            while (usedColNames.includes(currentColName)) {
                currentColName = colName + String(i);
                i++;
            }
            return currentColName;
        }
    }

    const selectionStatisticFunctions = [
        {
            name: _t("Sum"),
            types: [CellValueType.number],
            compute: (values, locale) => SUM.compute.bind({ locale })([values]),
        },
        {
            name: _t("Avg"),
            types: [CellValueType.number],
            compute: (values, locale) => AVERAGE.compute.bind({ locale })([values]),
        },
        {
            name: _t("Min"),
            types: [CellValueType.number],
            compute: (values, locale) => MIN.compute.bind({ locale })([values]),
        },
        {
            name: _t("Max"),
            types: [CellValueType.number],
            compute: (values, locale) => MAX.compute.bind({ locale })([values]),
        },
        {
            name: _t("Count"),
            types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
            compute: (values, locale) => COUNTA.compute.bind({ locale })([values]),
        },
        {
            name: _t("Count Numbers"),
            types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
            compute: (values, locale) => COUNT.compute.bind({ locale })([values]),
        },
    ];
    /**
     * SelectionPlugin
     */
    class GridSelectionPlugin extends UIPlugin {
        static layers = [6 /* LAYERS.Selection */];
        static getters = [
            "getActiveSheet",
            "getActiveSheetId",
            "getActiveCell",
            "getActiveCols",
            "getActiveRows",
            "getCurrentStyle",
            "getSelectedZones",
            "getSelectedZone",
            "getSelectedCells",
            "getStatisticFnResults",
            "getAggregate",
            "getSelectedFigureId",
            "getSelection",
            "getActivePosition",
            "getSheetPosition",
            "isSelected",
            "isSingleColSelected",
            "getElementsFromSelection",
            "tryGetActiveSheetId",
            "isGridSelectionActive",
        ];
        gridSelection = {
            anchor: {
                cell: { col: 0, row: 0 },
                zone: { top: 0, left: 0, bottom: 0, right: 0 },
            },
            zones: [{ top: 0, left: 0, bottom: 0, right: 0 }],
        };
        selectedFigureId = null;
        sheetsData = {};
        moveClient;
        // This flag is used to avoid to historize the ACTIVE_SHEET command when it's
        // the main command.
        activeSheet = null;
        constructor(config) {
            super(config);
            this.moveClient = config.moveClient;
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "ACTIVATE_SHEET":
                    try {
                        this.getters.getSheet(cmd.sheetIdTo);
                        break;
                    }
                    catch (error) {
                        return "InvalidSheetId" /* CommandResult.InvalidSheetId */;
                    }
                case "MOVE_COLUMNS_ROWS":
                    return this.isMoveElementAllowed(cmd);
            }
            return "Success" /* CommandResult.Success */;
        }
        handleEvent(event) {
            const anchor = event.anchor;
            let zones = [];
            switch (event.mode) {
                case "overrideSelection":
                    zones = [anchor.zone];
                    break;
                case "updateAnchor":
                    zones = [...this.gridSelection.zones];
                    const index = zones.findIndex((z) => isEqual(z, event.previousAnchor.zone));
                    if (index >= 0) {
                        zones[index] = anchor.zone;
                    }
                    break;
                case "newAnchor":
                    zones = [...this.gridSelection.zones, anchor.zone];
                    break;
            }
            this.setSelectionMixin(event.anchor, zones);
            /** Any change to the selection has to be reflected in the selection processor. */
            this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));
            const { col, row } = this.gridSelection.anchor.cell;
            this.moveClient({
                sheetId: this.getters.getActiveSheetId(),
                col,
                row,
            });
            this.selectedFigureId = null;
        }
        handle(cmd) {
            switch (cmd.type) {
                case "START_EDITION":
                case "ACTIVATE_SHEET":
                    this.selectedFigureId = null;
                    break;
                case "DELETE_FIGURE":
                    if (this.selectedFigureId === cmd.id) {
                        this.selectedFigureId = null;
                    }
                    break;
                case "DELETE_SHEET":
                    if (this.selectedFigureId && this.getters.getFigure(cmd.sheetId, this.selectedFigureId)) {
                        this.selectedFigureId = null;
                    }
                    break;
            }
            switch (cmd.type) {
                case "START":
                    const firstSheetId = this.getters.getVisibleSheetIds()[0];
                    this.activateSheet(firstSheetId, firstSheetId);
                    const { col, row } = this.getters.getNextVisibleCellPosition({
                        sheetId: firstSheetId,
                        col: 0,
                        row: 0,
                    });
                    this.selectCell(col, row);
                    this.selection.registerAsDefault(this, this.gridSelection.anchor, {
                        handleEvent: this.handleEvent.bind(this),
                    });
                    this.moveClient({ sheetId: firstSheetId, col: 0, row: 0 });
                    break;
                case "ACTIVATE_SHEET": {
                    this.activateSheet(cmd.sheetIdFrom, cmd.sheetIdTo);
                    break;
                }
                case "REMOVE_COLUMNS_ROWS": {
                    const sheetId = this.getters.getActiveSheetId();
                    if (cmd.sheetId === sheetId) {
                        if (cmd.dimension === "COL") {
                            this.onColumnsRemoved(cmd);
                        }
                        else {
                            this.onRowsRemoved(cmd);
                        }
                        const { col, row } = this.gridSelection.anchor.cell;
                        this.moveClient({ sheetId, col, row });
                    }
                    break;
                }
                case "ADD_COLUMNS_ROWS": {
                    const sheetId = this.getters.getActiveSheetId();
                    if (cmd.sheetId === sheetId) {
                        this.onAddElements(cmd);
                        const { col, row } = this.gridSelection.anchor.cell;
                        this.moveClient({ sheetId, col, row });
                    }
                    break;
                }
                case "MOVE_COLUMNS_ROWS":
                    if (cmd.sheetId === this.getActiveSheetId()) {
                        this.onMoveElements(cmd);
                    }
                    break;
                case "SELECT_FIGURE":
                    this.selectedFigureId = cmd.id;
                    break;
                case "ACTIVATE_NEXT_SHEET":
                    this.activateNextSheet("right");
                    break;
                case "ACTIVATE_PREVIOUS_SHEET":
                    this.activateNextSheet("left");
                    break;
                case "HIDE_SHEET":
                    if (cmd.sheetId === this.getActiveSheetId()) {
                        this.dispatch("ACTIVATE_SHEET", {
                            sheetIdFrom: cmd.sheetId,
                            sheetIdTo: this.getters.getVisibleSheetIds()[0],
                        });
                    }
                    break;
                case "UNDO":
                case "REDO":
                case "DELETE_SHEET":
                    const deletedSheetIds = Object.keys(this.sheetsData).filter((sheetId) => !this.getters.tryGetSheet(sheetId));
                    for (const sheetId of deletedSheetIds) {
                        delete this.sheetsData[sheetId];
                    }
                    for (const sheetId in this.sheetsData) {
                        const gridSelection = this.clipSelection(sheetId, this.sheetsData[sheetId].gridSelection);
                        this.sheetsData[sheetId] = {
                            gridSelection: deepCopy(gridSelection),
                        };
                    }
                    if (!this.getters.tryGetSheet(this.getters.getActiveSheetId())) {
                        const currentSheetIds = this.getters.getVisibleSheetIds();
                        this.activeSheet = this.getters.getSheet(currentSheetIds[0]);
                        if (this.activeSheet.id in this.sheetsData) {
                            const { anchor } = this.clipSelection(this.activeSheet.id, this.sheetsData[this.activeSheet.id].gridSelection);
                            this.selectCell(anchor.cell.col, anchor.cell.row);
                        }
                        else {
                            this.selectCell(0, 0);
                        }
                        const { col, row } = this.gridSelection.anchor.cell;
                        this.moveClient({
                            sheetId: this.getters.getActiveSheetId(),
                            col,
                            row,
                        });
                    }
                    const sheetId = this.getters.getActiveSheetId();
                    this.gridSelection.zones = this.gridSelection.zones.map((z) => this.getters.expandZone(sheetId, z));
                    this.gridSelection.anchor.zone = this.getters.expandZone(sheetId, this.gridSelection.anchor.zone);
                    this.setSelectionMixin(this.gridSelection.anchor, this.gridSelection.zones);
                    break;
            }
            /** Any change to the selection has to be  reflected in the selection processor. */
            this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        isGridSelectionActive() {
            return this.selection.isListening(this);
        }
        getActiveSheet() {
            return this.activeSheet;
        }
        getActiveSheetId() {
            return this.activeSheet.id;
        }
        tryGetActiveSheetId() {
            return this.activeSheet?.id;
        }
        getActiveCell() {
            return this.getters.getEvaluatedCell(this.getActivePosition());
        }
        getActiveCols() {
            const activeCols = new Set();
            for (let zone of this.gridSelection.zones) {
                if (zone.top === 0 &&
                    zone.bottom === this.getters.getNumberRows(this.getters.getActiveSheetId()) - 1) {
                    for (let i = zone.left; i <= zone.right; i++) {
                        activeCols.add(i);
                    }
                }
            }
            return activeCols;
        }
        getActiveRows() {
            const activeRows = new Set();
            const sheetId = this.getters.getActiveSheetId();
            for (let zone of this.gridSelection.zones) {
                if (zone.left === 0 && zone.right === this.getters.getNumberCols(sheetId) - 1) {
                    for (let i = zone.top; i <= zone.bottom; i++) {
                        activeRows.add(i);
                    }
                }
            }
            return activeRows;
        }
        getCurrentStyle() {
            const zone = this.getters.getSelectedZone();
            const sheetId = this.getters.getActiveSheetId();
            return this.getters.getCellStyle({ sheetId, col: zone.left, row: zone.top });
        }
        getSelectedZones() {
            return deepCopy(this.gridSelection.zones);
        }
        getSelectedZone() {
            return deepCopy(this.gridSelection.anchor.zone);
        }
        getSelection() {
            return deepCopy(this.gridSelection);
        }
        getSelectedCells() {
            const sheetId = this.getters.getActiveSheetId();
            const cells = [];
            for (const zone of this.gridSelection.zones) {
                cells.push(...this.getters.getEvaluatedCellsInZone(sheetId, zone));
            }
            return cells;
        }
        getSelectedFigureId() {
            return this.selectedFigureId;
        }
        getActivePosition() {
            return this.getters.getMainCellPosition({
                sheetId: this.getActiveSheetId(),
                col: this.gridSelection.anchor.cell.col,
                row: this.gridSelection.anchor.cell.row,
            });
        }
        getSheetPosition(sheetId) {
            if (sheetId === this.getters.getActiveSheetId()) {
                return this.getActivePosition();
            }
            else {
                const sheetData = this.sheetsData[sheetId];
                return sheetData
                    ? {
                        sheetId,
                        col: sheetData.gridSelection.anchor.cell.col,
                        row: sheetData.gridSelection.anchor.cell.row,
                    }
                    : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });
            }
        }
        getStatisticFnResults() {
            const sheetId = this.getters.getActiveSheetId();
            const cells = new Set();
            for (const zone of this.gridSelection.zones) {
                for (const { col, row } of positions(zone)) {
                    if (this.getters.isRowHidden(sheetId, row) || this.getters.isColHidden(sheetId, col)) {
                        continue; // Skip hidden cells
                    }
                    const evaluatedCell = this.getters.getEvaluatedCell({ sheetId, col, row });
                    if (evaluatedCell.type !== CellValueType.empty) {
                        cells.add(evaluatedCell);
                    }
                }
            }
            let cellsTypes = new Set();
            let cellsValues = [];
            for (let cell of cells) {
                cellsTypes.add(cell.type);
                cellsValues.push(cell.value);
            }
            const locale = this.getters.getLocale();
            let statisticFnResults = {};
            for (let fn of selectionStatisticFunctions) {
                // We don't want to display statistical information when there is no interest:
                // We set the statistical result to undefined if the data handled by the selection
                // does not match the data handled by the function.
                // Ex: if there are only texts in the selection, we prefer that the SUM result
                // be displayed as undefined rather than 0.
                let fnResult = undefined;
                if (fn.types.some((t) => cellsTypes.has(t))) {
                    fnResult = fn.compute(cellsValues, locale);
                }
                statisticFnResults[fn.name] = fnResult;
            }
            return statisticFnResults;
        }
        getAggregate() {
            let aggregate = 0;
            let n = 0;
            const sheetId = this.getters.getActiveSheetId();
            const cellPositions = this.gridSelection.zones.map(positions).flat();
            for (const { col, row } of cellPositions) {
                const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
                if (cell.type === CellValueType.number) {
                    n++;
                    aggregate += cell.value;
                }
            }
            const locale = this.getters.getLocale();
            return n < 2 ? null : formatValue(aggregate, { locale });
        }
        isSelected(zone) {
            return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));
        }
        isSingleColSelected() {
            const selection = this.getters.getSelectedZones();
            if (selection.length !== 1 || selection[0].left !== selection[0].right) {
                return false;
            }
            return true;
        }
        /**
         * Returns a sorted array of indexes of all columns (respectively rows depending
         * on the dimension parameter) intersected by the currently selected zones.
         *
         * example:
         * assume selectedZones: [{left:0, right: 2, top :2, bottom: 4}, {left:5, right: 6, top :3, bottom: 5}]
         *
         * if dimension === "COL" => [0,1,2,5,6]
         * if dimension === "ROW" => [2,3,4,5]
         */
        getElementsFromSelection(dimension) {
            if (dimension === "COL" && this.getters.getActiveCols().size === 0) {
                return [];
            }
            if (dimension === "ROW" && this.getters.getActiveRows().size === 0) {
                return [];
            }
            const zones = this.getters.getSelectedZones();
            let elements = [];
            const start = dimension === "COL" ? "left" : "top";
            const end = dimension === "COL" ? "right" : "bottom";
            for (const zone of zones) {
                const zoneRows = Array.from({ length: zone[end] - zone[start] + 1 }, (_, i) => zone[start] + i);
                elements = elements.concat(zoneRows);
            }
            return [...new Set(elements)].sort();
        }
        // ---------------------------------------------------------------------------
        // Other
        // ---------------------------------------------------------------------------
        activateSheet(sheetIdFrom, sheetIdTo) {
            if (!this.getters.isSheetVisible(sheetIdTo)) {
                this.dispatch("SHOW_SHEET", { sheetId: sheetIdTo });
            }
            this.setActiveSheet(sheetIdTo);
            this.sheetsData[sheetIdFrom] = {
                gridSelection: deepCopy(this.gridSelection),
            };
            if (sheetIdTo in this.sheetsData) {
                Object.assign(this, this.sheetsData[sheetIdTo]);
                this.selection.resetDefaultAnchor(this, deepCopy(this.gridSelection.anchor));
            }
            else {
                const { col, row } = this.getters.getNextVisibleCellPosition({
                    sheetId: sheetIdTo,
                    col: 0,
                    row: 0,
                });
                this.selectCell(col, row);
            }
        }
        /**
         * Ensure selections are not outside sheet boundaries.
         * They are clipped to fit inside the sheet if needed.
         */
        setSelectionMixin(anchor, zones) {
            const { anchor: clippedAnchor, zones: clippedZones } = this.clipSelection(this.getters.getActiveSheetId(), { anchor, zones });
            this.gridSelection.anchor = clippedAnchor;
            this.gridSelection.zones = uniqueZones(clippedZones);
        }
        /**
         * Change the anchor of the selection active cell to an absolute col and row index.
         *
         * This is a non trivial task. We need to stop the editing process and update
         * properly the current selection.  Also, this method can optionally create a new
         * range in the selection.
         */
        selectCell(col, row) {
            const sheetId = this.getters.getActiveSheetId();
            const zone = this.getters.expandZone(sheetId, { left: col, right: col, top: row, bottom: row });
            this.setSelectionMixin({ zone, cell: { col, row } }, [zone]);
        }
        setActiveSheet(id) {
            const sheet = this.getters.getSheet(id);
            this.activeSheet = sheet;
        }
        activateNextSheet(direction) {
            const sheetIds = this.getters.getSheetIds();
            const oldSheetPosition = sheetIds.findIndex((id) => id === this.activeSheet.id);
            const delta = direction === "left" ? sheetIds.length - 1 : 1;
            const newPosition = (oldSheetPosition + delta) % sheetIds.length;
            this.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: this.getActiveSheetId(),
                sheetIdTo: sheetIds[newPosition],
            });
        }
        onColumnsRemoved(cmd) {
            const { cell, zone } = this.gridSelection.anchor;
            const selectedZone = updateSelectionOnDeletion(zone, "left", [...cmd.elements]);
            let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };
            anchorZone = updateSelectionOnDeletion(anchorZone, "left", [...cmd.elements]);
            const anchor = {
                cell: {
                    col: anchorZone.left,
                    row: anchorZone.top,
                },
                zone: selectedZone,
            };
            const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, "left", [...cmd.elements]));
            this.setSelectionMixin(anchor, selections);
        }
        onRowsRemoved(cmd) {
            const { cell, zone } = this.gridSelection.anchor;
            const selectedZone = updateSelectionOnDeletion(zone, "top", [...cmd.elements]);
            let anchorZone = { left: cell.col, right: cell.col, top: cell.row, bottom: cell.row };
            anchorZone = updateSelectionOnDeletion(anchorZone, "top", [...cmd.elements]);
            const anchor = {
                cell: {
                    col: anchorZone.left,
                    row: anchorZone.top,
                },
                zone: selectedZone,
            };
            const selections = this.gridSelection.zones.map((zone) => updateSelectionOnDeletion(zone, "top", [...cmd.elements]));
            this.setSelectionMixin(anchor, selections);
        }
        onAddElements(cmd) {
            const start = cmd.dimension === "COL" ? "left" : "top";
            const anchorZone = updateSelectionOnInsertion(this.gridSelection.anchor.zone, start, cmd.base, cmd.position, cmd.quantity);
            const selection = this.gridSelection.zones.map((zone) => updateSelectionOnInsertion(zone, start, cmd.base, cmd.position, cmd.quantity));
            const anchor = {
                cell: { col: anchorZone.left, row: anchorZone.top },
                zone: anchorZone,
            };
            this.setSelectionMixin(anchor, selection);
        }
        onMoveElements(cmd) {
            const thickness = cmd.elements.length;
            this.dispatch("ADD_COLUMNS_ROWS", {
                dimension: cmd.dimension,
                sheetId: cmd.sheetId,
                base: cmd.base,
                quantity: thickness,
                position: cmd.position,
            });
            const isCol = cmd.dimension === "COL";
            const start = cmd.elements[0];
            const end = cmd.elements[thickness - 1];
            const isBasedBefore = cmd.base < start;
            const deltaCol = isBasedBefore && isCol ? thickness : 0;
            const deltaRow = isBasedBefore && !isCol ? thickness : 0;
            const target = [
                {
                    left: isCol ? start + deltaCol : 0,
                    right: isCol ? end + deltaCol : this.getters.getNumberCols(cmd.sheetId) - 1,
                    top: !isCol ? start + deltaRow : 0,
                    bottom: !isCol ? end + deltaRow : this.getters.getNumberRows(cmd.sheetId) - 1,
                },
            ];
            const state = new ClipboardCellsState(target, "CUT", this.getters, this.dispatch, this.selection);
            const base = isBasedBefore ? cmd.base : cmd.base + 1;
            const pasteTarget = [
                {
                    left: isCol ? base : 0,
                    right: isCol ? base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
                    top: !isCol ? base : 0,
                    bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
                },
            ];
            state.paste(pasteTarget, { selectTarget: true });
            const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;
            let currentIndex = cmd.base;
            for (const element of toRemove) {
                const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);
                this.dispatch("RESIZE_COLUMNS_ROWS", {
                    dimension: cmd.dimension,
                    sheetId: cmd.sheetId,
                    size,
                    elements: [currentIndex],
                });
                currentIndex += 1;
            }
            this.dispatch("REMOVE_COLUMNS_ROWS", {
                dimension: cmd.dimension,
                sheetId: cmd.sheetId,
                elements: toRemove,
            });
        }
        isMoveElementAllowed(cmd) {
            const isCol = cmd.dimension === "COL";
            const start = cmd.elements[0];
            const end = cmd.elements[cmd.elements.length - 1];
            const id = cmd.sheetId;
            const doesElementsHaveCommonMerges = isCol
                ? this.getters.doesColumnsHaveCommonMerges
                : this.getters.doesRowsHaveCommonMerges;
            if (doesElementsHaveCommonMerges(id, start - 1, start) ||
                doesElementsHaveCommonMerges(id, end, end + 1) ||
                doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {
                return "WillRemoveExistingMerge" /* CommandResult.WillRemoveExistingMerge */;
            }
            const headers = [cmd.base, ...cmd.elements];
            const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);
            if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {
                return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
            }
            return "Success" /* CommandResult.Success */;
        }
        //-------------------------------------------
        // Helpers for extensions
        // ------------------------------------------
        /**
         * Clip the selection if it spans outside the sheet
         */
        clipSelection(sheetId, selection) {
            const cols = this.getters.getNumberCols(sheetId) - 1;
            const rows = this.getters.getNumberRows(sheetId) - 1;
            const zones = selection.zones.map((z) => {
                return {
                    left: clip(z.left, 0, cols),
                    right: clip(z.right, 0, cols),
                    top: clip(z.top, 0, rows),
                    bottom: clip(z.bottom, 0, rows),
                };
            });
            const anchorCol = clip(selection.anchor.cell.col, 0, cols);
            const anchorRow = clip(selection.anchor.cell.row, 0, rows);
            const anchorZone = {
                left: clip(selection.anchor.zone.left, 0, cols),
                right: clip(selection.anchor.zone.right, 0, cols),
                top: clip(selection.anchor.zone.top, 0, rows),
                bottom: clip(selection.anchor.zone.bottom, 0, rows),
            };
            return {
                zones,
                anchor: {
                    cell: { col: anchorCol, row: anchorRow },
                    zone: anchorZone,
                },
            };
        }
        // ---------------------------------------------------------------------------
        // Grid rendering
        // ---------------------------------------------------------------------------
        drawGrid(renderingContext) {
            if (this.getters.isDashboard()) {
                return;
            }
            const { ctx, thinLineWidth } = renderingContext;
            // selection
            const zones = this.getSelectedZones();
            ctx.fillStyle = "#f3f7fe";
            const onlyOneCell = zones.length === 1 && zones[0].left === zones[0].right && zones[0].top === zones[0].bottom;
            ctx.fillStyle = onlyOneCell ? "#f3f7fe" : "#e9f0ff";
            ctx.strokeStyle = SELECTION_BORDER_COLOR;
            ctx.lineWidth = 1.5 * thinLineWidth;
            for (const zone of zones) {
                const { x, y, width, height } = this.getters.getVisibleRect(zone);
                ctx.globalCompositeOperation = "multiply";
                ctx.fillRect(x, y, width, height);
                ctx.globalCompositeOperation = "source-over";
                ctx.strokeRect(x, y, width, height);
            }
            ctx.globalCompositeOperation = "source-over";
            // active zone
            const position = this.getActivePosition();
            ctx.strokeStyle = SELECTION_BORDER_COLOR;
            ctx.lineWidth = 3 * thinLineWidth;
            let zone;
            if (this.getters.isInMerge(position)) {
                zone = this.getters.getMerge(position);
            }
            else {
                zone = positionToZone(position);
            }
            const { x, y, width, height } = this.getters.getVisibleRect(zone);
            if (width > 0 && height > 0) {
                ctx.strokeRect(x, y, width, height);
            }
        }
    }

    class InternalViewport {
        getters;
        sheetId;
        boundaries;
        top;
        bottom;
        left;
        right;
        offsetX;
        offsetY;
        offsetScrollbarX;
        offsetScrollbarY;
        canScrollVertically;
        canScrollHorizontally;
        viewportWidth;
        viewportHeight;
        offsetCorrectionX;
        offsetCorrectionY;
        constructor(getters, sheetId, boundaries, sizeInGrid, options, offsets) {
            this.getters = getters;
            this.sheetId = sheetId;
            this.boundaries = boundaries;
            this.viewportWidth = sizeInGrid.width;
            this.viewportHeight = sizeInGrid.height;
            this.offsetScrollbarX = offsets.x;
            this.offsetScrollbarY = offsets.y;
            this.canScrollVertically = options.canScrollVertically;
            this.canScrollHorizontally = options.canScrollHorizontally;
            this.offsetCorrectionX = this.getters.getColDimensions(this.sheetId, this.boundaries.left).start;
            this.offsetCorrectionY = this.getters.getRowDimensions(this.sheetId, this.boundaries.top).start;
            this.adjustViewportOffsetX();
            this.adjustViewportOffsetY();
        }
        // PUBLIC
        /** Returns the maximum size (in Pixels) of the viewport relative to its allocated client size
         * When the viewport grid size is smaller than its client width (resp. height), it will return
         * the client width (resp. height).
         */
        getMaxSize() {
            const lastCol = this.getters.findLastVisibleColRowIndex(this.sheetId, "COL", {
                first: this.boundaries.left,
                last: this.boundaries.right,
            });
            const lastRow = this.getters.findLastVisibleColRowIndex(this.sheetId, "ROW", {
                first: this.boundaries.top,
                last: this.boundaries.bottom,
            });
            const { end: lastColEnd, size: lastColSize } = this.getters.getColDimensions(this.sheetId, lastCol);
            const { end: lastRowEnd, size: lastRowSize } = this.getters.getRowDimensions(this.sheetId, lastRow);
            const leftColIndex = this.searchHeaderIndex("COL", lastColEnd - this.viewportWidth, 0);
            const leftColSize = this.getters.getColSize(this.sheetId, leftColIndex);
            const leftRowIndex = this.searchHeaderIndex("ROW", lastRowEnd - this.viewportHeight, 0);
            const topRowSize = this.getters.getRowSize(this.sheetId, leftRowIndex);
            let width = lastColEnd - this.offsetCorrectionX;
            if (this.canScrollHorizontally) {
                width += Math.max(DEFAULT_CELL_WIDTH, // leave some minimal space to let the user know they scrolled all the way
                Math.min(leftColSize, this.viewportWidth - lastColSize) // Add pixels that allows the snapping at maximum horizontal scroll
                );
                width = Math.max(width, this.viewportWidth); // if the viewport grid size is smaller than its client width, return client width
            }
            let height = lastRowEnd - this.offsetCorrectionY;
            if (this.canScrollVertically) {
                height += Math.max(DEFAULT_CELL_HEIGHT + 5, // leave some space to let the user know they scrolled all the way
                Math.min(topRowSize, this.viewportHeight - lastRowSize) // Add pixels that allows the snapping at maximum vertical scroll
                );
                height = Math.max(height, this.viewportHeight); // if the viewport grid size is smaller than its client height, return client height
            }
            if (lastRowEnd + FOOTER_HEIGHT > height && !this.getters.isReadonly()) {
                height += FOOTER_HEIGHT;
            }
            return { width, height };
        }
        /**
         * Return the index of a column given an offset x, based on the pane left
         * visible cell.
         * It returns -1 if no column is found.
         */
        getColIndex(x) {
            if (x < this.offsetCorrectionX || x > this.offsetCorrectionX + this.viewportWidth) {
                return -1;
            }
            return this.searchHeaderIndex("COL", x - this.offsetCorrectionX, this.left);
        }
        /**
         * Return the index of a row given an offset y, based on the pane top
         * visible cell.
         * It returns -1 if no row is found.
         */
        getRowIndex(y) {
            if (y < this.offsetCorrectionY || y > this.offsetCorrectionY + this.viewportHeight) {
                return -1;
            }
            return this.searchHeaderIndex("ROW", y - this.offsetCorrectionY, this.top);
        }
        /**
         * This function will make sure that the provided cell position (or current selected position) is part of
         * the pane that is actually displayed on the client. We therefore adjust the offset of the pane
         * until it contains the cell completely.
         */
        adjustPosition(position) {
            const sheetId = this.sheetId;
            if (!position) {
                position = this.getters.getSheetPosition(sheetId);
            }
            const mainCellPosition = this.getters.getMainCellPosition({ sheetId, ...position });
            const { col, row } = this.getters.getNextVisibleCellPosition(mainCellPosition);
            if (isInside(col, this.boundaries.top, this.boundaries)) {
                this.adjustPositionX(col);
            }
            if (isInside(this.boundaries.left, row, this.boundaries)) {
                this.adjustPositionY(row);
            }
        }
        adjustPositionX(targetCol) {
            const sheetId = this.sheetId;
            const { end } = this.getters.getColDimensions(sheetId, targetCol);
            const maxCol = this.getters.getNumberCols(sheetId);
            if (this.offsetX + this.offsetCorrectionX + this.viewportWidth < end) {
                let finalTarget = targetCol;
                while (this.getters.isColHidden(sheetId, finalTarget) && targetCol < maxCol) {
                    finalTarget++;
                }
                const finalTargetEnd = this.getters.getColDimensions(sheetId, finalTarget).end;
                const startIndex = this.searchHeaderIndex("COL", finalTargetEnd - this.viewportWidth - this.offsetCorrectionX, this.boundaries.left);
                this.offsetX =
                    this.getters.getColDimensions(sheetId, startIndex).end - this.offsetCorrectionX;
                this.offsetScrollbarX = this.offsetX;
                this.adjustViewportZoneX();
            }
            else if (this.left > targetCol) {
                let finalTarget = targetCol;
                while (this.getters.isColHidden(sheetId, finalTarget) && targetCol > 0) {
                    finalTarget--;
                }
                this.offsetX =
                    this.getters.getColDimensions(sheetId, finalTarget).start - this.offsetCorrectionX;
                this.offsetScrollbarX = this.offsetX;
                this.adjustViewportZoneX();
            }
        }
        adjustPositionY(targetRow) {
            const sheetId = this.sheetId;
            const { end } = this.getters.getRowDimensions(sheetId, targetRow);
            const maxRow = this.getters.getNumberRows(sheetId);
            if (this.offsetY + this.viewportHeight + this.offsetCorrectionY < end) {
                let finalTarget = targetRow;
                while (this.getters.isRowHidden(sheetId, finalTarget) && targetRow < maxRow) {
                    finalTarget++;
                }
                const finalTargetEnd = this.getters.getRowDimensions(sheetId, finalTarget).end;
                const startIndex = this.searchHeaderIndex("ROW", finalTargetEnd - this.viewportHeight - this.offsetCorrectionY, this.boundaries.top);
                this.offsetY =
                    this.getters.getRowDimensions(sheetId, startIndex).end - this.offsetCorrectionY;
                this.offsetScrollbarY = this.offsetY;
                this.adjustViewportZoneY();
            }
            else if (this.top > targetRow) {
                let finalTarget = targetRow;
                while (this.getters.isRowHidden(sheetId, finalTarget) && targetRow > 0) {
                    finalTarget--;
                }
                this.offsetY =
                    this.getters.getRowDimensions(sheetId, finalTarget).start - this.offsetCorrectionY;
                this.offsetScrollbarY = this.offsetY;
                this.adjustViewportZoneY();
            }
        }
        setViewportOffset(offsetX, offsetY) {
            this.setViewportOffsetX(offsetX);
            this.setViewportOffsetY(offsetY);
        }
        adjustViewportZone() {
            this.adjustViewportZoneX();
            this.adjustViewportZoneY();
        }
        /**
         *
         * @param zone
         * @returns Computes the absolute coordinate of a given zone inside the viewport
         */
        getRect(zone) {
            const targetZone = intersection(zone, this.zone);
            if (targetZone) {
                const x = this.getters.getColRowOffset("COL", this.zone.left, targetZone.left) +
                    this.offsetCorrectionX;
                const y = this.getters.getColRowOffset("ROW", this.zone.top, targetZone.top) + this.offsetCorrectionY;
                const width = Math.min(this.getters.getColRowOffset("COL", targetZone.left, targetZone.right + 1), this.viewportWidth);
                const height = Math.min(this.getters.getColRowOffset("ROW", targetZone.top, targetZone.bottom + 1), this.viewportHeight);
                return {
                    x,
                    y,
                    width,
                    height,
                };
            }
            else {
                return undefined;
            }
        }
        isVisible(col, row) {
            const isInside = row <= this.bottom && row >= this.top && col >= this.left && col <= this.right;
            return (isInside &&
                !this.getters.isColHidden(this.sheetId, col) &&
                !this.getters.isRowHidden(this.sheetId, row));
        }
        // PRIVATE
        searchHeaderIndex(dimension, position, startIndex = 0) {
            const sheetId = this.sheetId;
            const headers = this.getters.getNumberHeaders(sheetId, dimension);
            // using a binary search:
            let start = startIndex;
            let end = headers;
            while (start <= end && start !== headers && end !== -1) {
                const mid = Math.floor((start + end) / 2);
                const offset = this.getters.getColRowOffset(dimension, startIndex, mid);
                const size = this.getters.getHeaderSize(sheetId, dimension, mid);
                if (position >= offset && position < offset + size) {
                    return mid;
                }
                else if (position >= offset + size) {
                    start = mid + 1;
                }
                else {
                    end = mid - 1;
                }
            }
            return -1;
        }
        get zone() {
            return { left: this.left, right: this.right, top: this.top, bottom: this.bottom };
        }
        setViewportOffsetX(offsetX) {
            if (!this.canScrollHorizontally) {
                return;
            }
            this.offsetScrollbarX = offsetX;
            this.adjustViewportZoneX();
        }
        setViewportOffsetY(offsetY) {
            if (!this.canScrollVertically) {
                return;
            }
            this.offsetScrollbarY = offsetY;
            this.adjustViewportZoneY();
        }
        /** Corrects the viewport's horizontal offset based on the current structure
         *  To make sure that at least on column is visible inside the viewport.
         */
        adjustViewportOffsetX() {
            if (this.canScrollHorizontally) {
                const { width: viewportWidth } = this.getMaxSize();
                if (this.viewportWidth + this.offsetScrollbarX > viewportWidth) {
                    this.offsetScrollbarX = Math.max(0, viewportWidth - this.viewportWidth);
                }
            }
            this.left = this.getColIndex(this.offsetScrollbarX);
            this.right = this.getColIndex(this.offsetScrollbarX + this.viewportWidth);
            if (this.right === -1) {
                this.right = this.boundaries.right;
            }
            this.adjustViewportZoneX();
        }
        /** Corrects the viewport's vertical offset based on the current structure
         *  To make sure that at least on row is visible inside the viewport.
         */
        adjustViewportOffsetY() {
            if (this.canScrollVertically) {
                const { height: paneHeight } = this.getMaxSize();
                if (this.viewportHeight + this.offsetScrollbarY > paneHeight) {
                    this.offsetScrollbarY = Math.max(0, paneHeight - this.viewportHeight);
                }
            }
            this.top = this.getRowIndex(this.offsetScrollbarY);
            this.bottom = this.getRowIndex(this.offsetScrollbarY + this.viewportHeight);
            if (this.bottom === -1) {
                this.bottom = this.boundaries.bottom;
            }
            this.adjustViewportZoneY();
        }
        /** Updates the pane zone and snapped offset based on its horizontal
         * offset (will find Left) and its width (will find Right) */
        adjustViewportZoneX() {
            const sheetId = this.sheetId;
            this.left = this.searchHeaderIndex("COL", this.offsetScrollbarX, this.boundaries.left);
            this.right = Math.min(this.boundaries.right, this.searchHeaderIndex("COL", this.viewportWidth, this.left));
            if (this.left === -1) {
                this.left = this.boundaries.left;
            }
            if (this.right === -1) {
                this.right = this.getters.getNumberCols(sheetId) - 1;
            }
            this.offsetX =
                this.getters.getColDimensions(sheetId, this.left).start -
                    this.getters.getColDimensions(sheetId, this.boundaries.left).start;
        }
        /** Updates the pane zone and snapped offset based on its vertical
         * offset (will find Top) and its width (will find Bottom) */
        adjustViewportZoneY() {
            const sheetId = this.sheetId;
            this.top = this.searchHeaderIndex("ROW", this.offsetScrollbarY, this.boundaries.top);
            this.bottom = Math.min(this.boundaries.bottom, this.searchHeaderIndex("ROW", this.viewportHeight, this.top));
            if (this.top === -1) {
                this.top = this.boundaries.top;
            }
            if (this.bottom === -1) {
                this.bottom = this.getters.getNumberRows(sheetId) - 1;
            }
            this.offsetY =
                this.getters.getRowDimensions(sheetId, this.top).start -
                    this.getters.getRowDimensions(sheetId, this.boundaries.top).start;
        }
    }

    /**
     *   EdgeScrollCases Schema
     *
     *  The dots/double dots represent a freeze (= a split of viewports)
     *  In this example, we froze vertically between columns D and E
     *  and horizontally between rows 4 and 5.
     *
     *  One can see that we scrolled horizontally from column E to G and
     *  vertically from row 5 to 7.
     *
     *     A  B  C  D   G  H  I  J  K  L  M  N  O  P  Q  R  S  T
     *     _______________________________________________________
     *  1 |           :                                           |
     *  2 |           :                                           |
     *  3 |           :        B   ↑                 6            |
     *  4 |           :        |   |                 |            |
     *     ····················+···+·················+············|
     *  7 |           :        |   |                 |            |
     *  8 |           :        ↓   2                 |            |
     *  9 |           :                              |            |
     * 10 |       A --+--→                           |            |
     * 11 |           :                              |            |
     * 12 |           :                              |            |
     * 13 |        ←--+-- 1                          |            |
     * 14 |           :                              |        3 --+--→
     * 15 |           :                              |            |
     * 16 |           :                              |            |
     * 17 |       5 --+-------------------------------------------+--→
     * 18 |           :                              |            |
     * 19 |           :                  4           |            |
     * 20 |           :                  |           |            |
     *     ______________________________+___________| ____________
     *                                   |           |
     *                                   ↓           ↓
     */
    /**
     * Viewport plugin.
     *
     * This plugin manages all things related to all viewport states.
     *
     */
    class SheetViewPlugin extends UIPlugin {
        static getters = [
            "getColIndex",
            "getRowIndex",
            "getActiveMainViewport",
            "getSheetViewDimension",
            "getSheetViewDimensionWithHeaders",
            "getMainViewportRect",
            "isVisibleInViewport",
            "getEdgeScrollCol",
            "getEdgeScrollRow",
            "getVisibleFigures",
            "getVisibleRect",
            "getColRowOffsetInViewport",
            "getMainViewportCoordinates",
            "getActiveSheetScrollInfo",
            "getActiveSheetDOMScrollInfo",
            "getSheetViewVisibleCols",
            "getSheetViewVisibleRows",
            "getFrozenSheetViewRatio",
            "isPositionVisible",
        ];
        viewports = {};
        /**
         * The viewport dimensions are usually set by one of the components
         * (i.e. when grid component is mounted) to properly reflect its state in the DOM.
         * In the absence of a component (standalone model), is it mandatory to set reasonable default values
         * to ensure the correct operation of this plugin.
         */
        sheetViewWidth = getDefaultSheetViewSize();
        sheetViewHeight = getDefaultSheetViewSize();
        gridOffsetX = 0;
        gridOffsetY = 0;
        sheetsWithDirtyViewports = new Set();
        shouldAdjustViewports = false;
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        allowDispatch(cmd) {
            switch (cmd.type) {
                case "SET_VIEWPORT_OFFSET":
                    return this.checkScrollingDirection(cmd);
                case "RESIZE_SHEETVIEW":
                    return this.chainValidations(this.checkValuesAreDifferent, this.checkPositiveDimension)(cmd);
                default:
                    return "Success" /* CommandResult.Success */;
            }
        }
        handleEvent(event) {
            if (event.options.scrollIntoView) {
                let { col, row } = findCellInNewZone(event.previousAnchor.zone, event.anchor.zone);
                if (event.mode === "updateAnchor") {
                    const oldZone = event.previousAnchor.zone;
                    const newZone = event.anchor.zone;
                    // altering a zone should not move the viewport in a dimension that wasn't changed
                    const { top, bottom, left, right } = this.getters.getActiveMainViewport();
                    if (oldZone.left === newZone.left && oldZone.right === newZone.right) {
                        col = left > col || col > right ? left : col;
                    }
                    if (oldZone.top === newZone.top && oldZone.bottom === newZone.bottom) {
                        row = top > row || row > bottom ? top : row;
                    }
                }
                const sheetId = this.getters.getActiveSheetId();
                col = Math.min(col, this.getters.getNumberCols(sheetId) - 1);
                row = Math.min(row, this.getters.getNumberRows(sheetId) - 1);
                if (!this.sheetsWithDirtyViewports.has(sheetId)) {
                    this.refreshViewport(this.getters.getActiveSheetId(), { col, row });
                }
            }
        }
        handle(cmd) {
            this.cleanViewports();
            // changing the evaluation can hide/show rows because of data filters
            if (invalidateEvaluationCommands.has(cmd.type)) {
                for (const sheetId of this.getters.getSheetIds()) {
                    this.sheetsWithDirtyViewports.add(sheetId);
                }
            }
            switch (cmd.type) {
                case "START":
                    this.selection.observe(this, {
                        handleEvent: this.handleEvent.bind(this),
                    });
                    this.resetViewports(this.getters.getActiveSheetId());
                    break;
                case "UNDO":
                case "REDO":
                    for (const sheetId of this.getters.getSheetIds()) {
                        this.sheetsWithDirtyViewports.add(sheetId);
                    }
                    this.shouldAdjustViewports = true;
                    break;
                case "RESIZE_SHEETVIEW":
                    this.resizeSheetView(cmd.height, cmd.width, cmd.gridOffsetX, cmd.gridOffsetY);
                    break;
                case "SET_VIEWPORT_OFFSET":
                    this.setSheetViewOffset(cmd.offsetX, cmd.offsetY);
                    break;
                case "SHIFT_VIEWPORT_DOWN":
                    const { top } = this.getActiveMainViewport();
                    const sheetId = this.getters.getActiveSheetId();
                    const shiftedOffsetY = this.clipOffsetY(this.getters.getRowDimensions(sheetId, top).start + this.sheetViewHeight);
                    this.shiftVertically(shiftedOffsetY);
                    break;
                case "SHIFT_VIEWPORT_UP": {
                    const { top } = this.getActiveMainViewport();
                    const sheetId = this.getters.getActiveSheetId();
                    const shiftedOffsetY = this.clipOffsetY(this.getters.getRowDimensions(sheetId, top).end - this.sheetViewHeight);
                    this.shiftVertically(shiftedOffsetY);
                    break;
                }
                case "REMOVE_FILTER_TABLE":
                case "UPDATE_FILTER":
                    this.sheetsWithDirtyViewports.add(cmd.sheetId);
                    break;
                case "REMOVE_COLUMNS_ROWS":
                case "RESIZE_COLUMNS_ROWS":
                case "HIDE_COLUMNS_ROWS":
                case "ADD_COLUMNS_ROWS":
                case "UNHIDE_COLUMNS_ROWS":
                case "UNGROUP_HEADERS":
                case "GROUP_HEADERS":
                case "FOLD_HEADER_GROUP":
                case "UNFOLD_HEADER_GROUP":
                case "FOLD_HEADER_GROUPS_IN_ZONE":
                case "UNFOLD_HEADER_GROUPS_IN_ZONE":
                case "UNFOLD_ALL_HEADER_GROUPS":
                case "FOLD_ALL_HEADER_GROUPS": {
                    const sheetId = "sheetId" in cmd ? cmd.sheetId : this.getters.getActiveSheetId();
                    this.sheetsWithDirtyViewports.add(sheetId);
                    break;
                }
                case "UPDATE_CELL":
                    // update cell content or format can change hidden rows because of data filters
                    if ("content" in cmd || "format" in cmd || cmd.style?.fontSize !== undefined) {
                        for (const sheetId of this.getters.getSheetIds()) {
                            this.sheetsWithDirtyViewports.add(sheetId);
                        }
                    }
                    break;
                case "ACTIVATE_SHEET":
                    this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);
                    break;
                case "UNFREEZE_ROWS":
                case "UNFREEZE_COLUMNS":
                case "FREEZE_COLUMNS":
                case "FREEZE_ROWS":
                case "UNFREEZE_COLUMNS_ROWS":
                    this.resetViewports(this.getters.getActiveSheetId());
                    break;
                case "DELETE_SHEET":
                    this.sheetsWithDirtyViewports.delete(cmd.sheetId);
                    break;
                case "START_EDITION":
                    const { col, row } = this.getters.getActivePosition();
                    this.refreshViewport(this.getters.getActiveSheetId(), { col, row });
                    break;
            }
        }
        finalize() {
            for (const sheetId of this.sheetsWithDirtyViewports) {
                this.resetViewports(sheetId);
                if (this.shouldAdjustViewports) {
                    const position = this.getters.getSheetPosition(sheetId);
                    const viewports = this.getSubViewports(sheetId);
                    Object.values(viewports).forEach((viewport) => {
                        viewport.adjustPosition(position);
                    });
                }
            }
            this.sheetsWithDirtyViewports = new Set();
            this.shouldAdjustViewports = false;
            this.setViewports();
        }
        setViewports() {
            const sheetIds = this.getters.getSheetIds();
            for (const sheetId of sheetIds) {
                if (!this.viewports[sheetId]?.bottomRight) {
                    this.resetViewports(sheetId);
                }
            }
        }
        // ---------------------------------------------------------------------------
        // Getters
        // ---------------------------------------------------------------------------
        /**
         * Return the index of a column given an offset x, based on the viewport left
         * visible cell.
         * It returns -1 if no column is found.
         */
        getColIndex(x) {
            const sheetId = this.getters.getActiveSheetId();
            return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getColIndex(x)));
        }
        /**
         * Return the index of a row given an offset y, based on the viewport top
         * visible cell.
         * It returns -1 if no row is found.
         */
        getRowIndex(y) {
            const sheetId = this.getters.getActiveSheetId();
            return Math.max(...this.getSubViewports(sheetId).map((viewport) => viewport.getRowIndex(y)));
        }
        getSheetViewDimensionWithHeaders() {
            return {
                width: this.sheetViewWidth + this.gridOffsetX,
                height: this.sheetViewHeight + this.gridOffsetY,
            };
        }
        getSheetViewDimension() {
            return {
                width: this.sheetViewWidth,
                height: this.sheetViewHeight,
            };
        }
        /** type as pane, not viewport but basically pane extends viewport */
        getActiveMainViewport() {
            const sheetId = this.getters.getActiveSheetId();
            return this.getMainViewport(sheetId);
        }
        /**
         * Return the scroll info of the active sheet, ie. the offset between the viewport left/top side and
         * the grid left/top side, snapped to the columns/rows.
         */
        getActiveSheetScrollInfo() {
            const sheetId = this.getters.getActiveSheetId();
            const viewport = this.getMainInternalViewport(sheetId);
            return {
                scrollX: viewport.offsetX,
                scrollY: viewport.offsetY,
            };
        }
        /**
         * Return the DOM scroll info of the active sheet, ie. the offset between the viewport left/top side and
         * the grid left/top side, corresponding to the scroll of the scrollbars and not snapped to the grid.
         */
        getActiveSheetDOMScrollInfo() {
            const sheetId = this.getters.getActiveSheetId();
            const viewport = this.getMainInternalViewport(sheetId);
            return {
                scrollX: viewport.offsetScrollbarX,
                scrollY: viewport.offsetScrollbarY,
            };
        }
        getSheetViewVisibleCols() {
            const sheetId = this.getters.getActiveSheetId();
            const viewports = this.getSubViewports(sheetId);
            //TODO ake another commit to eimprove this
            return [...new Set(viewports.map((v) => range(v.left, v.right + 1)).flat())].filter((col) => !this.getters.isHeaderHidden(sheetId, "COL", col));
        }
        getSheetViewVisibleRows() {
            const sheetId = this.getters.getActiveSheetId();
            const viewports = this.getSubViewports(sheetId);
            return [...new Set(viewports.map((v) => range(v.top, v.bottom + 1)).flat())].filter((row) => !this.getters.isHeaderHidden(sheetId, "ROW", row));
        }
        /**
         * Return the main viewport maximum size relative to the client size.
         */
        getMainViewportRect() {
            const sheetId = this.getters.getActiveSheetId();
            const viewport = this.getMainInternalViewport(sheetId);
            const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
            let { width, height } = viewport.getMaxSize();
            const x = this.getters.getColDimensions(sheetId, xSplit).start;
            const y = this.getters.getRowDimensions(sheetId, ySplit).start;
            return { x, y, width, height };
        }
        getMaximumSheetOffset() {
            const sheetId = this.getters.getActiveSheetId();
            const { width, height } = this.getMainViewportRect();
            const viewport = this.getMainInternalViewport(sheetId);
            return {
                maxOffsetX: Math.max(0, width - viewport.viewportWidth + 1),
                maxOffsetY: Math.max(0, height - viewport.viewportHeight + 1),
            };
        }
        getColRowOffsetInViewport(dimension, referenceIndex, index) {
            const sheetId = this.getters.getActiveSheetId();
            const visibleCols = this.getters.getSheetViewVisibleCols();
            const visibleRows = this.getters.getSheetViewVisibleRows();
            if (index < referenceIndex) {
                return -this.getColRowOffsetInViewport(dimension, index, referenceIndex);
            }
            let offset = 0;
            const visibleIndexes = dimension === "COL" ? visibleCols : visibleRows;
            for (let i = referenceIndex; i < index; i++) {
                if (!visibleIndexes.includes(i)) {
                    continue;
                }
                offset += this.getters.getHeaderSize(sheetId, dimension, i);
            }
            return offset;
        }
        /**
         * Check if a given position is visible in the viewport.
         */
        isVisibleInViewport({ sheetId, col, row }) {
            return this.getSubViewports(sheetId).some((pane) => pane.isVisible(col, row));
        }
        // => return s the new offset
        getEdgeScrollCol(x, previousX, startingX) {
            let canEdgeScroll = false;
            let direction = 0;
            let delay = 0;
            /** 4 cases : See EdgeScrollCases Schema at the top
             * 1. previous in XRight > XLeft
             * 3. previous in XRight > outside
             * 5. previous in Left > outside
             * A. previous in Left > right
             * with X a position taken in the bottomRIght (aka scrollable) viewport
             */
            const { xSplit } = this.getters.getPaneDivisions(this.getters.getActiveSheetId());
            const { width } = this.getSheetViewDimension();
            const { x: offsetCorrectionX } = this.getMainViewportCoordinates();
            const currentOffsetX = this.getActiveSheetScrollInfo().scrollX;
            if (x > width) {
                // 3 & 5
                canEdgeScroll = true;
                delay = scrollDelay(x - width);
                direction = 1;
            }
            else if (x < offsetCorrectionX && startingX >= offsetCorrectionX && currentOffsetX > 0) {
                // 1
                canEdgeScroll = true;
                delay = scrollDelay(offsetCorrectionX - x);
                direction = -1;
            }
            else if (xSplit && previousX < offsetCorrectionX && x > offsetCorrectionX) {
                // A
                canEdgeScroll = true;
                delay = scrollDelay(x);
                direction = "reset";
            }
            return { canEdgeScroll, direction, delay };
        }
        getEdgeScrollRow(y, previousY, tartingY) {
            let canEdgeScroll = false;
            let direction = 0;
            let delay = 0;
            /** 4 cases : See EdgeScrollCases Schema at the top
             * 2. previous in XBottom > XTop
             * 4. previous in XRight > outside
             * 6. previous in Left > outside
             * B. previous in Left > right
             * with X a position taken in the bottomRIght (aka scrollable) viewport
             */
            const { ySplit } = this.getters.getPaneDivisions(this.getters.getActiveSheetId());
            const { height } = this.getSheetViewDimension();
            const { y: offsetCorrectionY } = this.getMainViewportCoordinates();
            const currentOffsetY = this.getActiveSheetScrollInfo().scrollY;
            if (y > height) {
                // 4 & 6
                canEdgeScroll = true;
                delay = scrollDelay(y - height);
                direction = 1;
            }
            else if (y < offsetCorrectionY && tartingY >= offsetCorrectionY && currentOffsetY > 0) {
                // 2
                canEdgeScroll = true;
                delay = scrollDelay(offsetCorrectionY - y);
                direction = -1;
            }
            else if (ySplit && previousY < offsetCorrectionY && y > offsetCorrectionY) {
                // B
                canEdgeScroll = true;
                delay = scrollDelay(y);
                direction = "reset";
            }
            return { canEdgeScroll, direction, delay };
        }
        /**
         * Computes the coordinates and size to draw the zone on the canvas
         */
        getVisibleRect(zone) {
            const sheetId = this.getters.getActiveSheetId();
            const viewportRects = this.getSubViewports(sheetId)
                .map((viewport) => viewport.getRect(zone))
                .filter(isDefined$1);
            if (viewportRects.length === 0) {
                return { x: 0, y: 0, width: 0, height: 0 };
            }
            const x = Math.min(...viewportRects.map((rect) => rect.x));
            const y = Math.min(...viewportRects.map((rect) => rect.y));
            const width = Math.max(...viewportRects.map((rect) => rect.x + rect.width)) - x;
            const height = Math.max(...viewportRects.map((rect) => rect.y + rect.height)) - y;
            return {
                x: x + this.gridOffsetX,
                y: y + this.gridOffsetY,
                width,
                height,
            };
        }
        /**
         * Returns the position of the MainViewport relatively to the start of the grid (without headers)
         * It corresponds to the summed dimensions of the visible cols/rows (in x/y respectively)
         * situated before the pane divisions.
         */
        getMainViewportCoordinates() {
            const sheetId = this.getters.getActiveSheetId();
            const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
            const x = this.getters.getColDimensions(sheetId, xSplit).start;
            const y = this.getters.getRowDimensions(sheetId, ySplit).start;
            return { x, y };
        }
        // ---------------------------------------------------------------------------
        // Private
        // ---------------------------------------------------------------------------
        ensureMainViewportExist(sheetId) {
            if (!this.viewports[sheetId]) {
                this.resetViewports(sheetId);
            }
        }
        getSubViewports(sheetId) {
            this.ensureMainViewportExist(sheetId);
            return Object.values(this.viewports[sheetId]).filter(isDefined$1);
        }
        checkPositiveDimension(cmd) {
            if (cmd.width < 0 || cmd.height < 0) {
                return "InvalidViewportSize" /* CommandResult.InvalidViewportSize */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkValuesAreDifferent(cmd) {
            const { height, width } = this.getSheetViewDimension();
            if (cmd.gridOffsetX === this.gridOffsetX &&
                cmd.gridOffsetY === this.gridOffsetY &&
                cmd.width === width &&
                cmd.height === height) {
                return "ValuesNotChanged" /* CommandResult.ValuesNotChanged */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkScrollingDirection({ offsetX, offsetY, }) {
            const pane = this.getMainInternalViewport(this.getters.getActiveSheetId());
            if ((!pane.canScrollHorizontally && offsetX > 0) ||
                (!pane.canScrollVertically && offsetY > 0)) {
                return "InvalidScrollingDirection" /* CommandResult.InvalidScrollingDirection */;
            }
            return "Success" /* CommandResult.Success */;
        }
        getMainViewport(sheetId) {
            const viewport = this.getMainInternalViewport(sheetId);
            return {
                top: viewport.top,
                left: viewport.left,
                bottom: viewport.bottom,
                right: viewport.right,
            };
        }
        getMainInternalViewport(sheetId) {
            this.ensureMainViewportExist(sheetId);
            return this.viewports[sheetId].bottomRight;
        }
        /** gets rid of deprecated sheetIds */
        cleanViewports() {
            const sheetIds = this.getters.getSheetIds();
            for (let sheetId of Object.keys(this.viewports)) {
                if (!sheetIds.includes(sheetId)) {
                    delete this.viewports[sheetId];
                }
            }
        }
        resizeSheetView(height, width, gridOffsetX = 0, gridOffsetY = 0) {
            this.sheetViewHeight = height;
            this.sheetViewWidth = width;
            this.gridOffsetX = gridOffsetX;
            this.gridOffsetY = gridOffsetY;
            this.recomputeViewports();
        }
        recomputeViewports() {
            for (let sheetId of Object.keys(this.viewports)) {
                this.resetViewports(sheetId);
            }
        }
        setSheetViewOffset(offsetX, offsetY) {
            const sheetId = this.getters.getActiveSheetId();
            const { maxOffsetX, maxOffsetY } = this.getMaximumSheetOffset();
            Object.values(this.getSubViewports(sheetId)).forEach((viewport) => viewport.setViewportOffset(clip(offsetX, 0, maxOffsetX), clip(offsetY, 0, maxOffsetY)));
        }
        /**
         * Clip the vertical offset within the allowed range.
         * Not above the sheet, nor below the sheet.
         */
        clipOffsetY(offsetY) {
            const { height } = this.getMainViewportRect();
            const maxOffset = height - this.sheetViewHeight;
            offsetY = Math.min(offsetY, maxOffset);
            offsetY = Math.max(offsetY, 0);
            return offsetY;
        }
        getViewportOffset(sheetId) {
            return {
                x: this.viewports[sheetId]?.bottomRight.offsetScrollbarX || 0,
                y: this.viewports[sheetId]?.bottomRight.offsetScrollbarY || 0,
            };
        }
        resetViewports(sheetId) {
            if (!this.getters.tryGetSheet(sheetId)) {
                return;
            }
            const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
            const nCols = this.getters.getNumberCols(sheetId);
            const nRows = this.getters.getNumberRows(sheetId);
            const colOffset = this.getters.getColRowOffset("COL", 0, xSplit, sheetId);
            const rowOffset = this.getters.getColRowOffset("ROW", 0, ySplit, sheetId);
            const { xRatio, yRatio } = this.getFrozenSheetViewRatio(sheetId);
            const canScrollHorizontally = xRatio < 1.0;
            const canScrollVertically = yRatio < 1.0;
            const previousOffset = this.getViewportOffset(sheetId);
            const sheetViewports = {
                topLeft: (ySplit &&
                    xSplit &&
                    new InternalViewport(this.getters, sheetId, { left: 0, right: xSplit - 1, top: 0, bottom: ySplit - 1 }, { width: colOffset, height: rowOffset }, { canScrollHorizontally: false, canScrollVertically: false }, { x: 0, y: 0 })) ||
                    undefined,
                topRight: (ySplit &&
                    new InternalViewport(this.getters, sheetId, { left: xSplit, right: nCols - 1, top: 0, bottom: ySplit - 1 }, { width: this.sheetViewWidth - colOffset, height: rowOffset }, { canScrollHorizontally, canScrollVertically: false }, { x: canScrollHorizontally ? previousOffset.x : 0, y: 0 })) ||
                    undefined,
                bottomLeft: (xSplit &&
                    new InternalViewport(this.getters, sheetId, { left: 0, right: xSplit - 1, top: ySplit, bottom: nRows - 1 }, { width: colOffset, height: this.sheetViewHeight - rowOffset }, { canScrollHorizontally: false, canScrollVertically }, { x: 0, y: canScrollVertically ? previousOffset.y : 0 })) ||
                    undefined,
                bottomRight: new InternalViewport(this.getters, sheetId, { left: xSplit, right: nCols - 1, top: ySplit, bottom: nRows - 1 }, {
                    width: this.sheetViewWidth - colOffset,
                    height: this.sheetViewHeight - rowOffset,
                }, { canScrollHorizontally, canScrollVertically }, {
                    x: canScrollHorizontally ? previousOffset.x : 0,
                    y: canScrollVertically ? previousOffset.y : 0,
                }),
            };
            this.viewports[sheetId] = sheetViewports;
        }
        /**
         * Adjust the viewport such that the anchor position is visible
         */
        refreshViewport(sheetId, anchorPosition) {
            Object.values(this.getSubViewports(sheetId)).forEach((viewport) => {
                viewport.adjustViewportZone();
                viewport.adjustPosition(anchorPosition);
            });
        }
        /**
         * Shift the viewport vertically and move the selection anchor
         * such that it remains at the same place relative to the
         * viewport top.
         */
        shiftVertically(offset) {
            const { top } = this.getActiveMainViewport();
            const { scrollX } = this.getActiveSheetScrollInfo();
            this.setSheetViewOffset(scrollX, offset);
            const { anchor } = this.getters.getSelection();
            const deltaRow = this.getActiveMainViewport().top - top;
            this.selection.selectCell(anchor.cell.col, anchor.cell.row + deltaRow);
        }
        getVisibleFigures() {
            const sheetId = this.getters.getActiveSheetId();
            const result = [];
            const figures = this.getters.getFigures(sheetId);
            const { scrollX, scrollY } = this.getActiveSheetScrollInfo();
            const { x: offsetCorrectionX, y: offsetCorrectionY } = this.getters.getMainViewportCoordinates();
            const { width, height } = this.getters.getSheetViewDimensionWithHeaders();
            for (const figure of figures) {
                if (figure.x >= offsetCorrectionX &&
                    (figure.x + figure.width <= offsetCorrectionX + scrollX ||
                        figure.x >= width + scrollX + offsetCorrectionX)) {
                    continue;
                }
                if (figure.y >= offsetCorrectionY &&
                    (figure.y + figure.height <= offsetCorrectionY + scrollY ||
                        figure.y >= height + scrollY + offsetCorrectionY)) {
                    continue;
                }
                result.push(figure);
            }
            return result;
        }
        isPositionVisible(position) {
            const { scrollX, scrollY } = this.getters.getActiveSheetScrollInfo();
            const { x: mainViewportX, y: mainViewportY } = this.getters.getMainViewportCoordinates();
            const { width, height } = this.getters.getSheetViewDimension();
            if (position.x >= mainViewportX &&
                (position.x < mainViewportX + scrollX || position.x > width + scrollX + mainViewportX)) {
                return false;
            }
            if (position.y >= mainViewportY &&
                (position.y < mainViewportY + scrollY || position.y > height + scrollY + mainViewportY)) {
                return false;
            }
            return true;
        }
        getFrozenSheetViewRatio(sheetId) {
            const { xSplit, ySplit } = this.getters.getPaneDivisions(sheetId);
            const offsetCorrectionX = this.getters.getColDimensions(sheetId, xSplit).start;
            const offsetCorrectionY = this.getters.getRowDimensions(sheetId, ySplit).start;
            const width = this.sheetViewWidth + this.gridOffsetX;
            const height = this.sheetViewHeight + this.gridOffsetY;
            return { xRatio: offsetCorrectionX / width, yRatio: offsetCorrectionY / height };
        }
    }

    class HeaderPositionsUIPlugin extends UIPlugin {
        static getters = ["getColDimensions", "getRowDimensions", "getColRowOffset"];
        headerPositions = {};
        isDirty = true;
        handle(cmd) {
            if (invalidateEvaluationCommands.has(cmd.type)) {
                this.headerPositions = {};
                this.isDirty = true;
            }
            switch (cmd.type) {
                case "START":
                    for (const sheetId of this.getters.getSheetIds()) {
                        this.headerPositions[sheetId] = this.computeHeaderPositionsOfSheet(sheetId);
                    }
                    break;
                case "UPDATE_CELL":
                    if ("content" in cmd || "format" in cmd) {
                        this.headerPositions = {};
                        this.isDirty = true;
                    }
                    else {
                        this.headerPositions[cmd.sheetId] = this.computeHeaderPositionsOfSheet(cmd.sheetId);
                    }
                    break;
                case "UPDATE_FILTER":
                case "REMOVE_FILTER_TABLE":
                    this.headerPositions = {};
                    this.isDirty = true;
                    break;
                case "REMOVE_COLUMNS_ROWS":
                case "RESIZE_COLUMNS_ROWS":
                case "HIDE_COLUMNS_ROWS":
                case "ADD_COLUMNS_ROWS":
                case "UNHIDE_COLUMNS_ROWS":
                case "FOLD_HEADER_GROUP":
                case "UNFOLD_HEADER_GROUP":
                case "FOLD_HEADER_GROUPS_IN_ZONE":
                case "UNFOLD_HEADER_GROUPS_IN_ZONE":
                case "UNFOLD_ALL_HEADER_GROUPS":
                case "FOLD_ALL_HEADER_GROUPS":
                case "UNGROUP_HEADERS":
                case "GROUP_HEADERS":
                case "CREATE_SHEET":
                    this.headerPositions[cmd.sheetId] = this.computeHeaderPositionsOfSheet(cmd.sheetId);
                    break;
                case "DUPLICATE_SHEET":
                    this.headerPositions[cmd.sheetIdTo] = deepCopy(this.headerPositions[cmd.sheetId]);
                    break;
            }
        }
        finalize() {
            if (this.isDirty) {
                for (const sheetId of this.getters.getSheetIds()) {
                    this.headerPositions[sheetId] = this.computeHeaderPositionsOfSheet(sheetId);
                }
                this.isDirty = false;
            }
        }
        /**
         * Returns the size, start and end coordinates of a column on an unfolded sheet
         */
        getColDimensions(sheetId, col) {
            const start = this.headerPositions[sheetId]["COL"][col];
            const size = this.getters.getColSize(sheetId, col);
            const isColHidden = this.getters.isColHidden(sheetId, col);
            return {
                start,
                size,
                end: start + (isColHidden ? 0 : size),
            };
        }
        /**
         * Returns the size, start and end coordinates of a row an unfolded sheet
         */
        getRowDimensions(sheetId, row) {
            const start = this.headerPositions[sheetId]["ROW"][row];
            const size = this.getters.getRowSize(sheetId, row);
            const isRowHidden = this.getters.isRowHidden(sheetId, row);
            return {
                start,
                size: size,
                end: start + (isRowHidden ? 0 : size),
            };
        }
        /**
         * Returns the offset of a header (determined by the dimension) at the given index
         * based on the referenceIndex given. If start === 0, this method will return
         * the start attribute of the header.
         *
         * i.e. The size from A to B is the distance between A.start and B.end
         */
        getColRowOffset(dimension, referenceIndex, index, sheetId = this.getters.getActiveSheetId()) {
            const referencePosition = this.headerPositions[sheetId][dimension][referenceIndex];
            const position = this.headerPositions[sheetId][dimension][index];
            return position - referencePosition;
        }
        computeHeaderPositionsOfSheet(sheetId) {
            return {
                COL: this.computePositions(sheetId, "COL"),
                ROW: this.computePositions(sheetId, "ROW"),
            };
        }
        computePositions(sheetId, dimension) {
            const positions = {};
            let offset = 0;
            // loop on number of headers +1 so the position of (last header + 1) is the end of the sheet
            for (let i = 0; i < this.getters.getNumberHeaders(sheetId, dimension) + 1; i++) {
                positions[i] = offset;
                if (this.getters.isHeaderHidden(sheetId, dimension, i)) {
                    continue;
                }
                offset += this.getters.getHeaderSize(sheetId, dimension, i);
            }
            return positions;
        }
    }

    const corePluginRegistry = new Registry()
        .add("settings", SettingsPlugin)
        .add("sheet", SheetPlugin)
        .add("header grouping", HeaderGroupingPlugin)
        .add("header visibility", HeaderVisibilityPlugin)
        .add("filters", FiltersPlugin)
        .add("cell", CellPlugin)
        .add("merge", MergePlugin)
        .add("headerSize", HeaderSizePlugin)
        .add("borders", BordersPlugin)
        .add("conditional formatting", ConditionalFormatPlugin)
        .add("figures", FigurePlugin)
        .add("chart", ChartPlugin)
        .add("image", ImagePlugin)
        .add("dataValidation", DataValidationPlugin);
    // Plugins which handle a specific feature, without handling any core commands
    const featurePluginRegistry = new Registry()
        .add("ui_sheet", SheetUIPlugin)
        .add("ui_options", UIOptionsPlugin)
        .add("selectionInputManager", SelectionInputsManagerPlugin)
        .add("highlight", HighlightPlugin)
        .add("grid renderer", RendererPlugin)
        .add("autofill", AutofillPlugin)
        .add("find_and_replace", FindAndReplacePlugin)
        .add("sort", SortPlugin)
        .add("automatic_sum", AutomaticSumPlugin)
        .add("format", FormatPlugin)
        .add("split_to_columns", SplitToColumnsPlugin)
        .add("cell_popovers", CellPopoverPlugin)
        .add("collaborative", CollaborativePlugin)
        .add("history", HistoryPlugin)
        .add("data_cleanup", DataCleanupPlugin);
    // Plugins which have a state, but which should not be shared in collaborative
    const statefulUIPluginRegistry = new Registry()
        .add("selection", GridSelectionPlugin)
        .add("evaluation_filter", FilterEvaluationPlugin)
        .add("header_visibility_ui", HeaderVisibilityUIPlugin)
        .add("header_positions", HeaderPositionsUIPlugin)
        .add("viewport", SheetViewPlugin)
        .add("clipboard", ClipboardPlugin)
        .add("edition", EditionPlugin);
    // Plugins which have a derived state from core data
    const coreViewsPluginRegistry = new Registry()
        .add("evaluation", EvaluationPlugin)
        .add("evaluation_chart", EvaluationChartPlugin)
        .add("evaluation_cf", EvaluationConditionalFormatPlugin)
        .add("row_size", HeaderSizeUIPlugin)
        .add("custom_colors", CustomColorsPlugin)
        .add("data_validation_ui", EvaluationDataValidationPlugin);

    const clickableCellRegistry = new Registry();
    clickableCellRegistry.add("link", {
        condition: (position, env) => !!env.model.getters.getEvaluatedCell(position).link,
        execute: (position, env) => openLink(env.model.getters.getEvaluatedCell(position).link, env),
        sequence: 5,
    });

    class ImageProvider {
        fileStore;
        constructor(fileStore) {
            this.fileStore = fileStore;
        }
        async requestImage() {
            const file = await this.getImageFromUser();
            const path = await this.fileStore.upload(file);
            const size = await this.getImageOriginalSize(path);
            return { path, size, mimetype: file.type };
        }
        getImageFromUser() {
            return new Promise((resolve, reject) => {
                const input = document.createElement("input");
                input.setAttribute("type", "file");
                input.setAttribute("accept", "image/*");
                input.addEventListener("change", async () => {
                    if (input.files === null || input.files.length != 1) {
                        reject();
                    }
                    else {
                        resolve(input.files[0]);
                    }
                });
                input.click();
            });
        }
        getImageOriginalSize(path) {
            return new Promise((resolve, reject) => {
                const image = new Image();
                image.src = path;
                image.addEventListener("load", () => {
                    const size = { width: image.width, height: image.height };
                    resolve(size);
                });
                image.addEventListener("error", reject);
            });
        }
    }

    class FocusableElement {
        focusableElement = undefined;
        setFocusableElement(element) {
            this.focusableElement = element;
        }
        focus() {
            this.focusableElement?.focus();
        }
    }

    const RIPPLE_KEY_FRAMES = [
        { transform: "scale(0)" },
        { transform: "scale(0.8)", offset: 0.33 },
        { opacity: "0", transform: "scale(1)", offset: 1 },
    ];
    css /* scss */ `
  .o-ripple {
    z-index: 1;
  }
`;
    class RippleEffect extends owl.Component {
        static template = "o-spreadsheet-RippleEffect";
        rippleRef = owl.useRef("ripple");
        setup() {
            let animation = undefined;
            owl.onMounted(() => {
                const rippleEl = this.rippleRef.el;
                if (!rippleEl || !rippleEl.animate)
                    return;
                animation = rippleEl.animate(RIPPLE_KEY_FRAMES, {
                    duration: this.props.duration,
                    easing: "ease-out",
                });
                animation.addEventListener("finish", this.props.onAnimationEnd);
            });
            owl.onWillUnmount(() => {
                animation?.removeEventListener("finish", this.props.onAnimationEnd);
            });
        }
        get rippleStyle() {
            const { x, y, width, height } = this.props;
            const offsetX = this.props.offsetX || 0;
            const offsetY = this.props.offsetY || 0;
            return cssPropertiesToCss({
                transform: "scale(0)",
                left: x,
                top: y,
                "margin-left": `${-width / 2 + offsetX}px`,
                "margin-top": `${-height / 2 + offsetY}px`,
                width: `${width}px`,
                height: `${height}px`,
                background: this.props.color,
                "border-radius": "100%",
                opacity: `${this.props.opacity}`,
            });
        }
    }
    RippleEffect.props = {
        x: String,
        y: String,
        color: String,
        opacity: Number,
        duration: Number,
        width: Number,
        height: Number,
        offsetY: Number,
        offsetX: Number,
        allowOverflow: Boolean,
        onAnimationEnd: Function,
        style: String,
    };
    class Ripple extends owl.Component {
        static template = "o-spreadsheet-Ripple";
        static components = { RippleEffect };
        static defaultProps = {
            color: "#aaaaaa",
            opacity: 0.4,
            duration: 800,
            enabled: true,
            onAnimationEnd: () => { },
            class: "",
        };
        childContainer = owl.useRef("childContainer");
        state = owl.useState({ ripples: [] });
        currentId = 1;
        onClick(ev) {
            if (!this.props.enabled)
                return;
            const containerEl = this.childContainer.el;
            if (!containerEl)
                return;
            const rect = this.getRippleChildRectInfo();
            const { x, y, width, height } = rect;
            const maxDim = Math.max(width, height);
            const rippleRect = {
                x: ev.clientX - x,
                y: ev.clientY - y,
                width: this.props.width || maxDim * 2.85,
                height: this.props.height || maxDim * 2.85,
            };
            this.state.ripples.push({ rippleRect, id: this.currentId++ });
        }
        getRippleStyle() {
            const containerEl = this.childContainer.el;
            if (!containerEl || containerEl.childElementCount !== 1 || !containerEl.firstElementChild) {
                return "";
            }
            const rect = this.getRippleChildRectInfo();
            return cssPropertiesToCss({
                top: rect.marginTop + "px",
                left: rect.marginLeft + "px",
                width: rect.width + "px",
                height: rect.height + "px",
            });
        }
        getRippleChildRectInfo() {
            const el = this.childContainer.el;
            if (!el)
                throw new Error("No child container element found");
            if (el.childElementCount !== 1 || !el.firstElementChild) {
                const boundingRect = getBoundingRectAsPOJO(el);
                return { ...boundingRect, marginLeft: 0, marginTop: 0 };
            }
            const childEl = el.firstElementChild;
            const margins = getElementMargins(childEl);
            const boundingRect = getBoundingRectAsPOJO(childEl);
            return {
                ...boundingRect,
                marginLeft: margins.left,
                marginTop: margins.top,
            };
        }
        removeRipple(id) {
            const index = this.state.ripples.findIndex((r) => r.id === id);
            if (index === -1)
                return;
            this.state.ripples.splice(index, 1);
        }
        getRippleEffectProps(id) {
            const rect = this.state.ripples.find((r) => r.id === id)?.rippleRect;
            if (!rect)
                throw new Error("Cannot find a ripple with the id " + id);
            return {
                color: this.props.color,
                opacity: this.props.opacity,
                duration: this.props.duration,
                x: this.props.ignoreClickPosition ? "50%" : rect.x + "px",
                y: this.props.ignoreClickPosition ? "50%" : rect.y + "px",
                width: rect.width,
                height: rect.height,
                offsetX: this.props.offsetX || 0,
                offsetY: this.props.offsetY || 0,
                allowOverflow: this.props.allowOverflow || false,
                style: this.getRippleStyle(),
                onAnimationEnd: () => this.removeRipple(id),
            };
        }
    }
    Ripple.props = {
        color: { type: String, optional: true },
        opacity: { type: Number, optional: true },
        duration: { type: Number, optional: true },
        ignoreClickPosition: { type: Boolean, optional: true },
        width: { type: Number, optional: true },
        height: { type: Number, optional: true },
        offsetY: { type: Number, optional: true },
        offsetX: { type: Number, optional: true },
        allowOverflow: { type: Boolean, optional: true },
        enabled: { type: Boolean, optional: true },
        onAnimationEnd: { type: Function, optional: true },
        slots: Object,
        class: { type: String, optional: true },
    };

    function interactiveRenameSheet(env, sheetId, name, errorCallback) {
        const result = env.model.dispatch("RENAME_SHEET", { sheetId, name });
        if (result.reasons.includes("MissingSheetName" /* CommandResult.MissingSheetName */)) {
            env.raiseError(_t("The sheet name cannot be empty."), errorCallback);
        }
        else if (result.reasons.includes("DuplicatedSheetName" /* CommandResult.DuplicatedSheetName */)) {
            env.raiseError(_t("A sheet with the name %s already exists. Please select another name.", name), errorCallback);
        }
        else if (result.reasons.includes("ForbiddenCharactersInSheetName" /* CommandResult.ForbiddenCharactersInSheetName */)) {
            env.raiseError(_t("Some used characters are not allowed in a sheet name (Forbidden characters are %s).", FORBIDDEN_SHEET_CHARS.join(" ")), errorCallback);
        }
    }

    css /* scss */ `
  .o-sheet {
    padding: 0 15px;
    padding-right: 10px;
    height: ${BOTTOMBAR_HEIGHT}px;
    border-left: 1px solid #c1c1c1;
    border-right: 1px solid #c1c1c1;
    margin-left: -1px;
    cursor: pointer;
    &:hover {
      background-color: rgba(0, 0, 0, 0.08);
    }

    &.active {
      color: #484;
      background-color: #ffffff;
      box-shadow: 0 1px 3px 1px rgba(60, 64, 67, 0.15);
    }

    .o-sheet-icon {
      z-index: 1;

      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }

    .o-sheet-name {
      outline: none;
      padding: 2px 4px;

      &.o-sheet-name-editable {
        border-radius: 2px;
        border: 2px solid mediumblue;
        /* negative margins so nothing moves when the border is added */
        margin-left: -2px;
        margin-right: -2px;
      }
    }
  }
`;
    class BottomBarSheet extends owl.Component {
        static template = "o-spreadsheet-BottomBarSheet";
        static components = { Ripple };
        static defaultProps = {
            onMouseDown: () => { },
            style: "",
        };
        state = owl.useState({ isEditing: false });
        sheetDivRef = owl.useRef("sheetDiv");
        sheetNameRef = owl.useRef("sheetNameSpan");
        editionState = "initializing";
        setup() {
            owl.onMounted(() => {
                if (this.isSheetActive) {
                    this.scrollToSheet();
                }
            });
            owl.onPatched(() => {
                if (this.sheetNameRef.el && this.state.isEditing && this.editionState === "initializing") {
                    this.editionState = "editing";
                    this.focusInputAndSelectContent();
                }
            });
        }
        focusInputAndSelectContent() {
            if (!this.state.isEditing || !this.sheetNameRef.el)
                return;
            this.sheetNameRef.el.focus();
            const selection = window.getSelection();
            if (selection && this.sheetNameRef.el.firstChild) {
                selection.setBaseAndExtent(this.sheetNameRef.el.firstChild, 0, this.sheetNameRef.el.firstChild, this.sheetNameRef.el.textContent?.length || 0);
            }
        }
        scrollToSheet() {
            this.sheetDivRef.el?.scrollIntoView?.();
        }
        onFocusOut() {
            if (this.state.isEditing && this.editionState !== "initializing") {
                this.stopEdition();
            }
        }
        onMouseDown(ev) {
            this.activateSheet();
            this.props.onMouseDown(ev);
        }
        activateSheet() {
            this.env.model.dispatch("ACTIVATE_SHEET", {
                sheetIdFrom: this.env.model.getters.getActiveSheetId(),
                sheetIdTo: this.props.sheetId,
            });
            this.scrollToSheet();
        }
        onDblClick() {
            this.startEdition();
        }
        onKeyDown(ev) {
            if (!this.state.isEditing)
                return;
            if (ev.key === "Enter") {
                ev.preventDefault();
                this.stopEdition();
            }
            if (ev.key === "Escape") {
                this.cancelEdition();
            }
        }
        onClickSheetName(ev) {
            if (this.state.isEditing)
                ev.stopPropagation();
        }
        startEdition() {
            this.state.isEditing = true;
            this.editionState = "initializing";
        }
        stopEdition() {
            if (!this.state.isEditing)
                return;
            this.state.isEditing = false;
            this.editionState = "initializing";
            this.sheetNameRef.el?.blur();
            const inputValue = this.getInputContent() || "";
            interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
        }
        cancelEdition() {
            this.state.isEditing = false;
            this.editionState = "initializing";
            this.sheetNameRef.el?.blur();
            this.setInputContent(this.sheetName);
        }
        onIconClick(ev) {
            if (!this.isSheetActive) {
                this.activateSheet();
            }
            this.props.openContextMenu(this.contextMenuRegistry, ev);
        }
        onContextMenu(ev) {
            if (!this.isSheetActive) {
                this.activateSheet();
            }
            this.props.openContextMenu(this.contextMenuRegistry, ev);
        }
        getInputContent() {
            return this.sheetNameRef.el?.textContent;
        }
        setInputContent(content) {
            if (this.sheetNameRef.el)
                this.sheetNameRef.el.textContent = content;
        }
        get contextMenuRegistry() {
            return getSheetMenuRegistry({
                renameSheetCallback: () => {
                    this.scrollToSheet();
                    this.startEdition();
                },
            });
        }
        get isSheetActive() {
            return this.env.model.getters.getActiveSheetId() === this.props.sheetId;
        }
        get sheetName() {
            return this.env.model.getters.getSheetName(this.props.sheetId);
        }
    }
    BottomBarSheet.props = {
        sheetId: String,
        openContextMenu: Function,
        style: { type: String, optional: true },
        onMouseDown: { type: Function, optional: true },
    };

    // -----------------------------------------------------------------------------
    // SpreadSheet
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-selection-statistic {
    margin-right: 20px;
    padding: 4px 4px 4px 8px;
    color: #333;
    cursor: pointer;
    &:hover {
      background-color: rgba(0, 0, 0, 0.08) !important;
    }
  }
`;
    class BottomBarStatistic extends owl.Component {
        static template = "o-spreadsheet-BottomBarStatisic";
        static components = { Ripple };
        selectedStatisticFn = "";
        statisticFnResults = {};
        setup() {
            this.statisticFnResults = this.env.model.getters.getStatisticFnResults();
            owl.onWillUpdateProps(() => {
                const newStatisticFnResults = this.env.model.getters.getStatisticFnResults();
                if (!deepEquals(newStatisticFnResults, this.statisticFnResults)) {
                    this.props.closeContextMenu();
                }
                this.statisticFnResults = newStatisticFnResults;
            });
        }
        getSelectedStatistic() {
            // don't display button if no function has a result
            if (Object.values(this.statisticFnResults).every((result) => result === undefined)) {
                return undefined;
            }
            if (this.selectedStatisticFn === "") {
                this.selectedStatisticFn = Object.keys(this.statisticFnResults)[0];
            }
            return this.getComposedFnName(this.selectedStatisticFn, this.statisticFnResults[this.selectedStatisticFn]);
        }
        listSelectionStatistics(ev) {
            const registry = new MenuItemRegistry();
            let i = 0;
            for (let [fnName, fnValue] of Object.entries(this.statisticFnResults)) {
                registry.add(fnName, {
                    name: this.getComposedFnName(fnName, fnValue),
                    sequence: i,
                    isReadonlyAllowed: true,
                    execute: () => {
                        this.selectedStatisticFn = fnName;
                    },
                });
                i++;
            }
            const target = ev.currentTarget;
            const { top, left, width } = target.getBoundingClientRect();
            this.props.openContextMenu(left + width, top, registry);
        }
        getComposedFnName(fnName, fnValue) {
            const locale = this.env.model.getters.getLocale();
            return fnName + ": " + (fnValue !== undefined ? formatValue(fnValue, { locale }) : "__");
        }
    }
    BottomBarStatistic.props = {
        openContextMenu: Function,
        closeContextMenu: Function,
    };

    // -----------------------------------------------------------------------------
    // SpreadSheet
    // -----------------------------------------------------------------------------
    const MENU_MAX_HEIGHT = 250;
    css /* scss */ `
  .o-spreadsheet-bottom-bar {
    color: ${TEXT_HEADER_COLOR};
    background-color: ${BACKGROUND_GRAY_COLOR};
    padding-left: ${HEADER_WIDTH}px;
    font-size: 15px;
    border-top: 1px solid lightgrey;

    .o-sheet-item {
      cursor: pointer;
      &:hover {
        background-color: rgba(0, 0, 0, 0.08);
      }
    }

    .o-all-sheets {
      max-width: 70%;
      .o-bottom-bar-fade-out {
        background-image: linear-gradient(-90deg, #cfcfcf, transparent 1%);
      }

      .o-bottom-bar-fade-in {
        background-image: linear-gradient(90deg, #cfcfcf, transparent 1%);
      }
    }

    .o-bottom-bar-arrows {
      .o-bottom-bar-arrow {
        cursor: pointer;
        &:hover:not([class*="o-disabled"]) {
          .o-icon {
            opacity: 0.9;
          }
        }

        .o-icon {
          height: 18px;
          width: 18px;
        }
      }
    }
  }
`;
    class BottomBar extends owl.Component {
        static template = "o-spreadsheet-BottomBar";
        static components = { Menu, Ripple, BottomBarSheet, BottomBarStatistic };
        bottomBarRef = owl.useRef("bottomBar");
        sheetListRef = owl.useRef("sheetList");
        dragAndDrop = useDragAndDropListItems();
        targetScroll = undefined;
        state = owl.useState({
            isSheetListScrollableLeft: false,
            isSheetListScrollableRight: false,
        });
        menuMaxHeight = MENU_MAX_HEIGHT;
        menuState = owl.useState({
            isOpen: false,
            menuId: undefined,
            position: null,
            menuItems: [],
        });
        sheetList = this.getVisibleSheets();
        setup() {
            owl.onWillUpdateProps(() => {
                this.updateScrollState();
                const visibleSheets = this.getVisibleSheets();
                // Cancel sheet dragging when there is a change in the sheets
                if (!deepEquals(this.sheetList, visibleSheets)) {
                    this.dragAndDrop.cancel();
                }
                this.sheetList = visibleSheets;
            });
        }
        clickAddSheet(ev) {
            const activeSheetId = this.env.model.getters.getActiveSheetId();
            const position = this.env.model.getters.getSheetIds().findIndex((sheetId) => sheetId === activeSheetId) + 1;
            const sheetId = this.env.model.uuidGenerator.uuidv4();
            const name = this.env.model.getters.getNextSheetName(_t("Sheet"));
            this.env.model.dispatch("CREATE_SHEET", { sheetId, position, name });
            this.env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: sheetId });
        }
        getVisibleSheets() {
            return this.env.model.getters.getVisibleSheetIds().map((sheetId) => {
                const sheet = this.env.model.getters.getSheet(sheetId);
                return { id: sheet.id, name: sheet.name };
            });
        }
        clickListSheets(ev) {
            const registry = new MenuItemRegistry();
            const from = this.env.model.getters.getActiveSheetId();
            let i = 0;
            for (const sheetId of this.env.model.getters.getSheetIds()) {
                const sheet = this.env.model.getters.getSheet(sheetId);
                registry.add(sheetId, {
                    name: sheet.name,
                    sequence: i,
                    isReadonlyAllowed: true,
                    textColor: sheet.isVisible ? undefined : "grey",
                    execute: (env) => {
                        env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: from, sheetIdTo: sheetId });
                    },
                });
                i++;
            }
            const target = ev.currentTarget;
            const { left } = target.getBoundingClientRect();
            const top = this.bottomBarRef.el.getBoundingClientRect().top;
            this.openContextMenu(left, top, "listSheets", registry);
        }
        openContextMenu(x, y, menuId, registry) {
            this.menuState.isOpen = true;
            this.menuState.menuId = menuId;
            this.menuState.menuItems = registry.getMenuItems();
            this.menuState.position = { x, y };
        }
        onSheetContextMenu(sheetId, registry, ev) {
            const target = ev.currentTarget;
            const { top, left } = target.getBoundingClientRect();
            if (ev.closedMenuId === sheetId) {
                this.closeMenu();
                return;
            }
            this.openContextMenu(left, top, sheetId, registry);
        }
        closeMenu() {
            this.menuState.isOpen = false;
            this.menuState.menuId = undefined;
            this.menuState.menuItems = [];
            this.menuState.position = null;
        }
        closeContextMenuWithId(menuId) {
            if (this.menuState.menuId === menuId) {
                this.closeMenu();
            }
        }
        onWheel(ev) {
            this.targetScroll = undefined;
            const target = ev.currentTarget;
            target.scrollLeft += ev.deltaY * 0.5;
        }
        onScroll() {
            this.updateScrollState();
            if (this.targetScroll === this.sheetListCurrentScroll) {
                this.targetScroll = undefined;
            }
        }
        onArrowLeft(ev) {
            if (!this.state.isSheetListScrollableLeft)
                return;
            if (!this.targetScroll)
                this.targetScroll = this.sheetListCurrentScroll;
            const newScroll = this.targetScroll - this.sheetListWidth;
            this.scrollSheetListTo(Math.max(0, newScroll));
        }
        onArrowRight(ev) {
            if (!this.state.isSheetListScrollableRight)
                return;
            if (!this.targetScroll)
                this.targetScroll = this.sheetListCurrentScroll;
            const newScroll = this.targetScroll + this.sheetListWidth;
            this.scrollSheetListTo(Math.min(this.sheetListMaxScroll, newScroll));
        }
        updateScrollState() {
            this.state.isSheetListScrollableLeft = this.sheetListCurrentScroll > 0;
            this.state.isSheetListScrollableRight = this.sheetListCurrentScroll < this.sheetListMaxScroll;
        }
        scrollSheetListTo(scroll) {
            if (!this.sheetListRef.el)
                return;
            this.targetScroll = scroll;
            this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
        }
        onSheetMouseDown(sheetId, event) {
            if (event.button !== 0)
                return;
            this.closeMenu();
            const visibleSheets = this.getVisibleSheets();
            const sheetRects = this.getSheetItemRects();
            const sheets = visibleSheets.map((sheet, index) => ({
                id: sheet.id,
                size: sheetRects[index].width,
                position: sheetRects[index].x,
            }));
            this.dragAndDrop.start("horizontal", {
                draggedItemId: sheetId,
                initialMousePosition: event.clientX,
                items: sheets,
                containerEl: this.sheetListRef.el,
                onDragEnd: (sheetId, finalIndex) => this.onDragEnd(sheetId, finalIndex),
            });
        }
        onDragEnd(sheetId, finalIndex) {
            const originalIndex = this.getVisibleSheets().findIndex((sheet) => sheet.id === sheetId);
            const delta = finalIndex - originalIndex;
            if (sheetId && delta !== 0) {
                this.env.model.dispatch("MOVE_SHEET", {
                    sheetId: sheetId,
                    delta: delta,
                });
            }
        }
        getSheetStyle(sheetId) {
            return this.dragAndDrop.itemsStyle[sheetId] || "";
        }
        getSheetItemRects() {
            return Array.from(this.bottomBarRef.el.querySelectorAll(`.o-sheet`))
                .map((sheetEl) => sheetEl.getBoundingClientRect())
                .map((rect) => ({
                x: rect.x,
                width: rect.width - 1,
                y: rect.y,
                height: rect.height,
            }));
        }
        get sheetListCurrentScroll() {
            if (!this.sheetListRef.el)
                return 0;
            return this.sheetListRef.el.scrollLeft;
        }
        get sheetListWidth() {
            if (!this.sheetListRef.el)
                return 0;
            return this.sheetListRef.el.clientWidth;
        }
        get sheetListMaxScroll() {
            if (!this.sheetListRef.el)
                return 0;
            return this.sheetListRef.el.scrollWidth - this.sheetListRef.el.clientWidth;
        }
    }
    BottomBar.props = {
        onClick: Function,
    };

    css /* scss */ `
  .o-dashboard-clickable-cell {
    position: absolute;
    cursor: pointer;
  }
`;
    let tKey = 1;
    class SpreadsheetDashboard extends owl.Component {
        static template = "o-spreadsheet-SpreadsheetDashboard";
        static components = {
            GridOverlay,
            GridPopover,
            Popover,
            VerticalScrollBar,
            HorizontalScrollBar,
            FilterIconsOverlay,
        };
        onMouseWheel;
        canvasPosition;
        hoveredCell;
        setup() {
            const gridRef = owl.useRef("grid");
            this.canvasPosition = useAbsoluteBoundingRect(gridRef);
            this.hoveredCell = owl.useState({ col: undefined, row: undefined });
            owl.useChildSubEnv({ getPopoverContainerRect: () => this.getGridRect() });
            useGridDrawing("canvas", this.env.model, () => this.env.model.getters.getSheetViewDimension());
            this.onMouseWheel = useWheelHandler((deltaX, deltaY) => {
                this.moveCanvas(deltaX, deltaY);
                this.hoveredCell.col = undefined;
                this.hoveredCell.row = undefined;
            });
        }
        onCellHovered({ col, row }) {
            this.hoveredCell.col = col;
            this.hoveredCell.row = row;
        }
        get gridContainer() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const { right } = this.env.model.getters.getSheetZone(sheetId);
            const { end } = this.env.model.getters.getColDimensions(sheetId, right);
            return cssPropertiesToCss({ "max-width": `${end}px` });
        }
        get gridOverlayDimensions() {
            return cssPropertiesToCss({
                height: "100%",
                width: "100%",
            });
        }
        getCellClickableStyle(coordinates) {
            return cssPropertiesToCss({
                top: `${coordinates.y}px`,
                left: `${coordinates.x}px`,
                width: `${coordinates.width}px`,
                height: `${coordinates.height}px`,
            });
        }
        /**
         * Get all the boxes for the cell in the sheet view that are clickable.
         * This function is used to render an overlay over each clickable cell in
         * order to display a pointer cursor.
         *
         */
        getClickableCells() {
            const cells = [];
            const sheetId = this.env.model.getters.getActiveSheetId();
            for (const col of this.env.model.getters.getSheetViewVisibleCols()) {
                for (const row of this.env.model.getters.getSheetViewVisibleRows()) {
                    const position = { sheetId, col, row };
                    const action = this.getClickableAction(position);
                    if (!action) {
                        continue;
                    }
                    let zone;
                    if (this.env.model.getters.isInMerge(position)) {
                        zone = this.env.model.getters.getMerge(position);
                    }
                    else {
                        zone = positionToZone({ col, row });
                    }
                    const rect = this.env.model.getters.getVisibleRect(zone);
                    cells.push({
                        coordinates: rect,
                        position: { col, row },
                        action,
                        // we can't rely on position only because a row or a column could
                        // be inserted at any time.
                        tKey: `${tKey}-${col}-${row}`,
                    });
                }
            }
            tKey++;
            return cells;
        }
        getClickableAction(position) {
            for (const items of clickableCellRegistry.getAll().sort((a, b) => a.sequence - b.sequence)) {
                if (items.condition(position, this.env)) {
                    return items.execute;
                }
            }
            return false;
        }
        selectClickableCell(clickableCell) {
            const { position, action } = clickableCell;
            action({ ...position, sheetId: this.env.model.getters.getActiveSheetId() }, this.env);
        }
        onClosePopover() {
            this.env.model.dispatch("CLOSE_CELL_POPOVER");
        }
        onGridResized({ height, width }) {
            this.env.model.dispatch("RESIZE_SHEETVIEW", {
                width: width,
                height: height,
                gridOffsetX: 0,
                gridOffsetY: 0,
            });
        }
        moveCanvas(deltaX, deltaY) {
            const { scrollX, scrollY } = this.env.model.getters.getActiveSheetDOMScrollInfo();
            this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
                offsetX: scrollX + deltaX,
                offsetY: scrollY + deltaY,
            });
        }
        getGridRect() {
            return { ...this.canvasPosition, ...this.env.model.getters.getSheetViewDimensionWithHeaders() };
        }
    }
    SpreadsheetDashboard.props = {};

    css /* scss */ `
  .o-header-group {
    .o-header-group-header {
      z-index: ${ComponentsImportance.HeaderGroupingButton};
      .o-group-fold-button {
        cursor: pointer;
        width: 13px;
        height: 13px;
        border: 1px solid ${HEADER_GROUPING_BORDER_COLOR};
        .o-icon {
          width: 7px;
          height: 7px;
        }

        &:hover {
          border-color: #777;
        }
      }
    }
    .o-group-border {
      box-sizing: border-box;
    }
  }
`;
    class AbstractHeaderGroup extends owl.Component {
        static template = "o-spreadsheet-HeaderGroup";
        toggleGroup() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const { start, end } = this.props.group;
            interactiveToggleGroup(this.env, sheetId, this.dimension, start, end);
        }
        get groupBoxStyle() {
            const groupBox = this.groupBox;
            return cssPropertiesToCss({
                top: `${groupBox.groupRect.y}px`,
                left: `${groupBox.groupRect.x}px`,
                width: `${groupBox.groupRect.width}px`,
                height: `${groupBox.groupRect.height}px`,
            });
        }
        get groupButtonStyle() {
            return cssPropertiesToCss({
                "background-color": this.isGroupFolded ? "#333" : "#fff",
                color: this.isGroupFolded ? "#fff" : "#333",
            });
        }
        get groupButtonIcon() {
            return this.isGroupFolded ? "o-spreadsheet-Icon.PLUS" : "o-spreadsheet-Icon.MINUS";
        }
        get isGroupFolded() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const group = this.props.group;
            return this.env.model.getters.isGroupFolded(sheetId, this.dimension, group.start, group.end);
        }
        onContextMenu(ev) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const position = { x: ev.clientX, y: ev.clientY };
            const group = this.props.group;
            const menuItems = getHeaderGroupContextMenu(sheetId, this.dimension, group.start, group.end);
            this.props.openContextMenu(position, menuItems);
        }
    }
    AbstractHeaderGroup.props = {
        group: Object,
        layerOffset: Number,
        openContextMenu: Function,
    };
    class RowGroup extends AbstractHeaderGroup {
        dimension = "ROW";
        get groupBorderStyle() {
            const groupBox = this.groupBox;
            if (this.groupBox.groupRect.height === 0) {
                return "";
            }
            return cssPropertiesToCss({
                top: `${groupBox.headerRect.height / 2}px`,
                left: `calc(50% - 1px)`,
                width: `30%`,
                height: `calc(100% - ${groupBox.headerRect.height / 2}px)`,
                "border-left": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
                "border-bottom": groupBox.isEndHidden ? "" : `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
            });
        }
        get groupHeaderStyle() {
            return cssPropertiesToCss({
                width: `100%`,
                height: `${this.groupBox.headerRect.height}px`,
            });
        }
        get groupBox() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const { start: startRow, end: endRow } = this.props.group;
            const startCoordinates = this.env.model.getters.getRowDimensions(sheetId, startRow).start;
            const endCoordinates = this.env.model.getters.getRowDimensions(sheetId, endRow).end;
            let groupHeaderY = 0;
            let groupHeaderHeight = HEADER_HEIGHT;
            if (startRow !== 0) {
                const headerRowDims = this.env.model.getters.getRowDimensions(sheetId, startRow - 1);
                groupHeaderY = HEADER_HEIGHT + headerRowDims.start;
                groupHeaderHeight = headerRowDims.end - headerRowDims.start;
            }
            const headerRect = {
                x: this.props.layerOffset,
                y: groupHeaderY,
                width: GROUP_LAYER_WIDTH,
                height: groupHeaderHeight,
            };
            const groupRect = {
                x: this.props.layerOffset,
                y: headerRect.y,
                width: GROUP_LAYER_WIDTH,
                height: headerRect.height + (endCoordinates - startCoordinates),
            };
            return {
                headerRect,
                groupRect,
                isEndHidden: this.env.model.getters.isRowHidden(sheetId, endRow),
            };
        }
    }
    class ColGroup extends AbstractHeaderGroup {
        dimension = "COL";
        get groupBorderStyle() {
            const groupBox = this.groupBox;
            if (groupBox.groupRect.width === 0) {
                return "";
            }
            return cssPropertiesToCss({
                top: `calc(50% - 1px)`,
                left: `${groupBox.headerRect.width / 2}px`,
                width: `calc(100% - ${groupBox.headerRect.width / 2}px)`,
                height: `30%`,
                "border-top": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
                "border-right": groupBox.isEndHidden ? "" : `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
            });
        }
        get groupHeaderStyle() {
            return cssPropertiesToCss({
                width: `${this.groupBox.headerRect.width}px`,
                height: `100%`,
            });
        }
        get groupBox() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const { start: startCol, end: endCol } = this.props.group;
            const startCoordinates = this.env.model.getters.getColDimensions(sheetId, startCol).start;
            const endCoordinates = this.env.model.getters.getColDimensions(sheetId, endCol).end;
            let groupHeaderX = 0;
            let groupHeaderWidth = HEADER_WIDTH;
            if (startCol !== 0) {
                const headerRowDims = this.env.model.getters.getColDimensions(sheetId, startCol - 1);
                groupHeaderX = HEADER_WIDTH + headerRowDims.start;
                groupHeaderWidth = headerRowDims.end - headerRowDims.start;
            }
            const headerRect = {
                x: groupHeaderX,
                y: this.props.layerOffset,
                width: groupHeaderWidth,
                height: GROUP_LAYER_WIDTH,
            };
            const groupRect = {
                x: headerRect.x,
                y: this.props.layerOffset,
                width: headerRect.width + (endCoordinates - startCoordinates),
                height: GROUP_LAYER_WIDTH,
            };
            return {
                headerRect,
                groupRect,
                isEndHidden: this.env.model.getters.isColHidden(sheetId, endCol),
            };
        }
    }

    css /* scss */ `
  .o-header-group-frozen-pane-border {
    &.o-group-rows {
      margin-top: -1px;
      border-bottom: 3px solid ${FROZEN_PANE_HEADER_BORDER_COLOR};
    }
    &.o-group-columns {
      margin-left: -1px;
      border-right: 3px solid ${FROZEN_PANE_HEADER_BORDER_COLOR};
    }
  }

  .o-header-group-main-pane {
    &.o-group-rows {
      margin-top: -2px; // Counteract o-header-group-frozen-pane-border offset
    }
    &.o-group-columns {
      margin-left: -2px;
    }
  }
`;
    class HeaderGroupContainer extends owl.Component {
        static template = "o-spreadsheet-HeaderGroupContainer";
        static components = { RowGroup, ColGroup, Menu };
        menu = owl.useState({ isOpen: false, position: null, menuItems: [] });
        getLayerOffset(layerIndex) {
            return layerIndex * GROUP_LAYER_WIDTH;
        }
        onContextMenu(event) {
            const sheetId = this.env.model.getters.getActiveSheetId();
            const position = { x: event.clientX, y: event.clientY };
            const menuItems = createHeaderGroupContainerContextMenu(sheetId, this.props.dimension);
            this.openContextMenu(position, menuItems);
        }
        openContextMenu(position, menuItems) {
            this.menu.isOpen = true;
            this.menu.position = position;
            this.menu.menuItems = menuItems;
        }
        closeMenu() {
            this.menu.isOpen = false;
            this.menu.position = null;
            this.menu.menuItems = [];
        }
        get groupComponent() {
            return this.props.dimension === "ROW" ? RowGroup : ColGroup;
        }
        get hasFrozenPane() {
            const viewportCoordinates = this.env.model.getters.getMainViewportCoordinates();
            return this.props.dimension === "COL" ? viewportCoordinates.x > 0 : viewportCoordinates.y > 0;
        }
        get scrollContainerStyle() {
            const { scrollX, scrollY } = this.env.model.getters.getActiveSheetScrollInfo();
            const cssProperties = {};
            if (this.props.dimension === "COL") {
                cssProperties.left = `${-scrollX - this.frozenPaneContainerSize}px`;
            }
            else {
                cssProperties.top = `${-scrollY - this.frozenPaneContainerSize}px`;
            }
            return cssPropertiesToCss(cssProperties);
        }
        get frozenPaneContainerStyle() {
            const cssProperties = {};
            if (this.props.dimension === "COL") {
                cssProperties.width = `${this.frozenPaneContainerSize}px`;
            }
            else {
                cssProperties.height = `${this.frozenPaneContainerSize}px`;
            }
            return cssPropertiesToCss(cssProperties);
        }
        get frozenPaneContainerSize() {
            if (!this.hasFrozenPane) {
                return 0;
            }
            const viewportCoordinates = this.env.model.getters.getMainViewportCoordinates();
            if (this.props.dimension === "COL") {
                return HEADER_WIDTH + viewportCoordinates.x;
            }
            else {
                return HEADER_HEIGHT + viewportCoordinates.y;
            }
        }
    }
    HeaderGroupContainer.props = {
        dimension: String,
        layers: Array,
    };

    css /* scss */ `
  .o-sidePanel {
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
    background-color: white;
    border: 1px solid darkgray;
    user-select: none;
    .o-sidePanelHeader {
      padding: 6px;
      height: 30px;
      background-color: ${BACKGROUND_HEADER_COLOR};
      display: flex;
      align-items: center;
      justify-content: space-between;
      border-bottom: 1px solid darkgray;
      border-top: 1px solid darkgray;
      font-weight: bold;
      .o-sidePanelTitle {
        font-weight: bold;
        padding: 5px 10px;
        color: dimgrey;
      }
      .o-sidePanelClose {
        padding: 5px 10px;
        cursor: pointer;
        &:hover {
          background-color: WhiteSmoke;
        }
      }
    }
    .o-sidePanelBody {
      overflow: auto;
      width: 100%;
      height: 100%;

      .o-section {
        padding: 16px;

        .o-section-title {
          font-weight: bold;
          color: dimgrey;
          margin-bottom: 5px;
        }

        .o-section-subtitle {
          color: dimgrey;
          font-weight: 500;
          font-size: 12px;
          line-height: 14px;
          margin: 8px 0 4px 0;
        }

        .o-subsection-left {
          display: inline-block;
          width: 47%;
          margin-right: 3%;
        }

        .o-subsection-right {
          display: inline-block;
          width: 47%;
          margin-left: 3%;
        }
      }
    }

    .o-sidePanelButtons {
      padding: 16px;
      text-align: right;
    }

    .o-sidePanel-btn-link {
      font-size: 14px;
      padding: 20px 24px 11px 24px;
      height: 44px;
      cursor: pointer;
      text-decoration: none;
      &:hover {
        color: #003a39;
        text-decoration: none;
      }
    }

    .o-button.primary:not(.o-disabled) {
      background-color: ${FILTERS_COLOR};
      color: white;
      &:hover:enabled {
        opacity: 0.8;
        background-color: ${FILTERS_COLOR};
      }
    }

    input.o-required,
    select.o-required {
      border-color: #4c4c4c;
    }
    input.o-optional,
    select.o-optional {
      border: 1px solid #a9a9a9;
    }
    input.o-invalid {
      border-color: red;
    }
    select.o-input {
      background-color: white;
      text-align: left;
    }

    .o-checkbox {
      display: flex;
      justify-items: center;
      input {
        margin-right: 5px;
      }
    }

    .o-inflection {
      table {
        table-layout: fixed;
        margin-top: 2%;
        display: table;
        text-align: left;
        font-size: 12px;
        line-height: 18px;
        width: 100%;
      }
      input,
      select {
        width: 100%;
        height: 100%;
        box-sizing: border-box;
      }
    }

    .o-sidePanel-tools {
      color: #333;
      font-size: 13px;
      cursor: default;
      display: flex;

      .o-tool {
        display: flex;
        align-items: center;
        margin: 2px;
        padding: 0 3px;
        border-radius: 2px;

        &:hover {
          background-color: rgba(0, 0, 0, 0.08);
        }
      }
    }
  }
`;
    class SidePanel extends owl.Component {
        static template = "o-spreadsheet-SidePanel";
        state;
        setup() {
            this.state = owl.useState({
                panel: sidePanelRegistry.get(this.props.component),
            });
            owl.onWillUpdateProps((nextProps) => (this.state.panel = sidePanelRegistry.get(nextProps.component)));
        }
        getTitle() {
            return typeof this.state.panel.title === "function"
                ? this.state.panel.title(this.env)
                : this.state.panel.title;
        }
    }
    SidePanel.props = {
        component: String,
        panelProps: { type: Object, optional: true },
        onCloseSidePanel: Function,
    };

    css /* scss */ `
  .o-menu-item-button {
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 2px;
    border-radius: 2px;
    min-width: 20px;
  }
  .o-disabled {
    opacity: 0.6;
    cursor: default;
  }
`;
    class ActionButton extends owl.Component {
        static template = "o-spreadsheet-ActionButton";
        actionButton = createAction(this.props.action);
        setup() {
            owl.onWillUpdateProps((nextProps) => {
                if (nextProps.action !== this.props.action) {
                    this.actionButton = createAction(this.props.action);
                }
            });
        }
        get isVisible() {
            return this.actionButton.isVisible(this.env);
        }
        get isEnabled() {
            return this.actionButton.isEnabled(this.env);
        }
        get isActive() {
            return this.actionButton.isActive?.(this.env);
        }
        get title() {
            const name = this.actionButton.name(this.env);
            const description = this.actionButton.description(this.env);
            return name + (description ? ` (${description})` : "");
        }
        get iconTitle() {
            return this.actionButton.icon(this.env);
        }
        onClick(ev) {
            if (this.isEnabled) {
                this.props.onClick?.(ev);
                this.actionButton.execute?.(this.env);
            }
        }
        get buttonStyle() {
            if (this.props.selectedColor) {
                return cssPropertiesToCss({
                    "border-bottom": `4px solid ${this.props.selectedColor}`,
                    height: "16px",
                    "margin-top": "2px",
                });
            }
            return "";
        }
    }
    ActionButton.props = {
        action: Object,
        hasTriangleDownIcon: { type: Boolean, optional: true },
        selectedColor: { type: String, optional: true },
        class: { type: String, optional: true },
        onClick: { type: Function, optional: true },
    };

    /**
     * List the available borders positions and the corresponding icons.
     * The structure of this array is defined to match the order/lines we want
     * to display in the topbar's border tool.
     */
    const BORDER_POSITIONS = [
        [
            ["all", "o-spreadsheet-Icon.BORDERS"],
            ["hv", "o-spreadsheet-Icon.BORDER_HV"],
            ["h", "o-spreadsheet-Icon.BORDER_H"],
            ["v", "o-spreadsheet-Icon.BORDER_V"],
            ["external", "o-spreadsheet-Icon.BORDER_EXTERNAL"],
        ],
        [
            ["left", "o-spreadsheet-Icon.BORDER_LEFT"],
            ["top", "o-spreadsheet-Icon.BORDER_TOP"],
            ["right", "o-spreadsheet-Icon.BORDER_RIGHT"],
            ["bottom", "o-spreadsheet-Icon.BORDER_BOTTOM"],
            ["clear", "o-spreadsheet-Icon.BORDER_CLEAR"],
        ],
    ];
    // -----------------------------------------------------------------------------
    // Border Editor
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-border-selector {
    padding: 4px;
    background-color: white;
  }
  .o-divider {
    border-right: 1px solid #e0e2e4;
    margin: 0 6px;
  }
  .o-border-selector-section {
    .o-dropdown-line {
      height: 30px;
      margin: 1px;
      .o-line-item {
        padding: 4px;
        width: 18px;
        height: 18px;
      }
    }
    .o-border-style-tool {
      padding: 0px 3px;
      margin: 2px;
      height: 25px;
    }
  }
  .o-style-preview {
    margin: 7px 5px 7px 5px;
    width: 60px;
    height: 5px;
  }
  .o-style-thin {
    border-bottom: 1px solid #000000;
  }
  .o-style-medium {
    border-bottom: 2px solid #000000;
  }
  .o-style-thick {
    border-bottom: 3px solid #000000;
  }
  .o-style-dashed {
    border-bottom: 1px dashed #000000;
  }
  .o-style-dotted {
    border-bottom: 1px dotted #000000;
  }
  .o-dropdown-border-type {
    &:not(.o-disabled):not(.active):hover {
      background-color: ${BG_HOVER_COLOR};
    }
  }
  .o-dropdown-border-check {
    width: 20px;
    font-size: 12px;
  }
  .o-border-style-dropdown {
    background: #ffffff;
    padding: 4px;
    .o-dropdown-line {
      .o-line-item.active {
        background-color: rgba(0, 0, 0, 0.2);
      }
    }
  }
  .o-border-picker-button {
    padding: 0px !important;
    margin: 5px 0px 0px 0px !important;
    height: 25px !important;
  }
`;
    class BorderEditor extends owl.Component {
        static template = "o-spreadsheet-BorderEditor";
        static components = { ColorPickerWidget, Popover };
        BORDER_POSITIONS = BORDER_POSITIONS;
        lineStyleButtonRef = owl.useRef("lineStyleButton");
        borderStyles = borderStyles;
        state = owl.useState({
            activeTool: undefined,
        });
        toggleDropdownTool(tool) {
            const isOpen = this.state.activeTool === tool;
            this.state.activeTool = isOpen ? undefined : tool;
        }
        closeDropdown() {
            this.state.activeTool = undefined;
        }
        setBorderPosition(position) {
            this.props.onBorderPositionPicked(position);
            this.closeDropdown();
        }
        setBorderColor(color) {
            this.props.onBorderColorPicked(color);
            this.closeDropdown();
        }
        setBorderStyle(style) {
            this.props.onBorderStylePicked(style);
            this.closeDropdown();
        }
        get lineStylePickerPopoverProps() {
            return {
                anchorRect: this.lineStylePickerAnchorRect,
                positioning: "BottomLeft",
                verticalOffset: 0,
            };
        }
        get popoverProps() {
            return {
                anchorRect: this.props.anchorRect,
                maxHeight: this.props.maxHeight,
                positioning: "BottomLeft",
                verticalOffset: 0,
            };
        }
        get lineStylePickerAnchorRect() {
            const button = this.lineStyleButtonRef.el;
            if (button === null) {
                return { x: 0, y: 0, width: 0, height: 0 };
            }
            const buttonRect = button.getBoundingClientRect();
            return {
                x: buttonRect.x,
                y: buttonRect.y,
                width: buttonRect.width,
                height: buttonRect.height,
            };
        }
    }
    BorderEditor.props = {
        class: { type: String, optional: true },
        currentBorderColor: { type: String, optional: false },
        currentBorderStyle: { type: String, optional: false },
        currentBorderPosition: { type: String, optional: true },
        onBorderColorPicked: Function,
        onBorderStylePicked: Function,
        onBorderPositionPicked: Function,
        maxHeight: { type: Number, optional: true },
        anchorRect: Object,
    };

    class BorderEditorWidget extends owl.Component {
        static template = "o-spreadsheet-BorderEditorWidget";
        static components = { BorderEditor };
        borderEditorButtonRef = owl.useRef("borderEditorButton");
        state = owl.useState({
            currentColor: DEFAULT_BORDER_DESC.color,
            currentStyle: DEFAULT_BORDER_DESC.style,
            currentPosition: undefined,
        });
        get borderEditorAnchorRect() {
            const button = this.borderEditorButtonRef.el;
            const buttonRect = button.getBoundingClientRect();
            return {
                x: buttonRect.x,
                y: buttonRect.y,
                width: buttonRect.width,
                height: buttonRect.height,
            };
        }
        onBorderPositionPicked(position) {
            this.state.currentPosition = position;
            this.updateBorder();
        }
        onBorderColorPicked(color) {
            this.state.currentColor = color;
            this.updateBorder();
        }
        onBorderStylePicked(style) {
            this.state.currentStyle = style;
            this.updateBorder();
        }
        updateBorder() {
            if (this.state.currentPosition === undefined) {
                return;
            }
            this.env.model.dispatch("SET_ZONE_BORDERS", {
                sheetId: this.env.model.getters.getActiveSheetId(),
                target: this.env.model.getters.getSelectedZones(),
                border: {
                    position: this.state.currentPosition,
                    color: this.state.currentColor,
                    style: this.state.currentStyle,
                },
            });
        }
    }
    BorderEditorWidget.props = {
        toggleBorderEditor: Function,
        showBorderEditor: Boolean,
        disabled: { type: Boolean, optional: true },
        dropdownMaxHeight: { type: Number, optional: true },
        class: { type: String, optional: true },
    };

    const COMPOSER_MAX_HEIGHT = 100;
    /* svg free of use from https://uxwing.com/formula-fx-icon/ */
    const FX_SVG = /*xml*/ `
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 121.8 122.9' width='16' height='16' focusable='false'>
  <path d='m28 34-4 5v2h10l-6 40c-4 22-6 28-7 30-2 2-3 3-5 3-3 0-7-2-9-4H4c-2 2-4 4-4 7s4 6 8 6 9-2 15-8c8-7 13-17 18-39l7-35 13-1 3-6H49c4-23 7-27 11-27 2 0 5 2 8 6h4c1-1 4-4 4-7 0-2-3-6-9-6-5 0-13 4-20 10-6 7-9 14-11 24h-8zm41 16c4-5 7-7 8-7s2 1 5 9l3 12c-7 11-12 17-16 17l-3-1-2-1c-3 0-6 3-6 7s3 7 7 7c6 0 12-6 22-23l3 10c3 9 6 13 10 13 5 0 11-4 18-15l-3-4c-4 6-7 8-8 8-2 0-4-3-6-10l-5-15 8-10 6-4 3 1 3 2c2 0 6-3 6-7s-2-7-6-7c-6 0-11 5-21 20l-2-6c-3-9-5-14-9-14-5 0-12 6-18 15l3 3z' fill='#BDBDBD'/>
</svg>
`;
    css /* scss */ `
  .o-topbar-composer {
    height: fit-content;
    margin-top: -1px;
    border: 1px solid;
    z-index: ${ComponentsImportance.TopBarComposer};

    .o-composer:empty:not(:focus):not(.active)::before {
      content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}");
      position: relative;
      top: 20%;
    }
  }

  .user-select-text {
    user-select: text;
  }
`;
    class TopBarComposer extends owl.Component {
        static template = "o-spreadsheet-TopBarComposer";
        static components = { Composer };
        get composerStyle() {
            const style = {
                padding: "5px 0px 5px 8px",
                "max-height": `${COMPOSER_MAX_HEIGHT}px`,
                "line-height": "24px",
            };
            style.height = this.props.focus === "inactive" ? `${TOPBAR_TOOLBAR_HEIGHT}px` : "fit-content";
            return cssPropertiesToCss(style);
        }
        get containerStyle() {
            if (this.props.focus === "inactive") {
                return cssPropertiesToCss({
                    "border-color": SEPARATOR_COLOR,
                    "border-right": "none",
                });
            }
            return cssPropertiesToCss({
                "border-color": SELECTION_BORDER_COLOR,
            });
        }
    }
    TopBarComposer.props = {
        focus: { validate: (value) => ["inactive", "cellFocus", "contentFocus"].includes(value) },
        onComposerContentFocused: Function,
    };

    css /* scss */ `
  .o-font-size-editor {
    height: calc(100% - 4px);
    input.o-font-size {
      outline-color: ${SELECTION_BORDER_COLOR};
      height: 20px;
      width: 23px;
    }
    input[type="number"] {
      -moz-appearance: textfield;
    }
    input::-webkit-outer-spin-button,
    input::-webkit-inner-spin-button {
      -webkit-appearance: none;
    }
  }
  .o-text-options > div {
    cursor: pointer;
    line-height: 26px;
    padding: 3px 12px;
    &:hover {
      background-color: rgba(0, 0, 0, 0.08);
    }
  }
`;
    class FontSizeEditor extends owl.Component {
        static template = "o-spreadsheet-FontSizeEditor";
        static components = {};
        fontSizes = FONT_SIZES;
        dropdown = owl.useState({ isOpen: false });
        inputRef = owl.useRef("inputFontSize");
        rootEditorRef = owl.useRef("FontSizeEditor");
        setup() {
            owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
        }
        onExternalClick(ev) {
            if (!isChildEvent(this.rootEditorRef.el, ev)) {
                this.closeFontList();
            }
        }
        get currentFontSize() {
            return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
        }
        toggleFontList() {
            const isOpen = this.dropdown.isOpen;
            if (!isOpen) {
                this.props.onToggle();
                this.inputRef.el.focus();
            }
            else {
                this.closeFontList();
            }
        }
        closeFontList() {
            this.dropdown.isOpen = false;
        }
        setSize(fontSizeStr) {
            const fontSize = clip(Math.floor(parseFloat(fontSizeStr)), 1, 400);
            setStyle(this.env, { fontSize });
            this.closeFontList();
        }
        setSizeFromInput(ev) {
            this.setSize(ev.target.value);
        }
        setSizeFromList(fontSizeStr) {
            this.setSize(fontSizeStr);
        }
        onInputFocused(ev) {
            this.dropdown.isOpen = true;
            ev.target.select();
        }
        onInputKeydown(ev) {
            if (ev.key === "Enter" || ev.key === "Escape") {
                this.closeFontList();
                const target = ev.target;
                // In the case of a ESCAPE key, we get the previous font size back
                if (ev.key === "Escape") {
                    target.value = `${this.currentFontSize}`;
                }
                this.props.onToggle();
            }
        }
    }
    FontSizeEditor.props = {
        onToggle: Function,
        dropdownStyle: String,
        class: String,
    };

    class PaintFormatButton extends owl.Component {
        static template = "o-spreadsheet-PaintFormatButton";
        get isActive() {
            return this.env.model.getters.isPaintingFormat();
        }
        onDblClick() {
            this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: true });
        }
        togglePaintFormat() {
            if (this.isActive) {
                this.env.model.dispatch("CANCEL_PAINT_FORMAT");
            }
            else {
                this.env.model.dispatch("ACTIVATE_PAINT_FORMAT", { persistent: false });
            }
        }
    }
    PaintFormatButton.props = {
        class: { type: String, optional: true },
    };

    // -----------------------------------------------------------------------------
    // TopBar
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-spreadsheet-topbar {
    line-height: 1.2;
    font-size: 13px;
    font-weight: 500;
    background-color: #fff;

    .o-topbar-top {
      border-bottom: 1px solid ${SEPARATOR_COLOR};
      padding: 2px 10px;

      /* Menus */
      .o-topbar-topleft {
        .o-topbar-menu {
          padding: 4px 6px;
          margin: 0 2px;

          &.active {
            background-color: ${BG_HOVER_COLOR};
            color: #000;
          }
        }
      }
    }

    .o-topbar-composer {
      flex-grow: 1;
    }

    /* Toolbar */
    .o-topbar-toolbar {
      height: ${TOPBAR_TOOLBAR_HEIGHT}px;

      .o-readonly-toolbar {
        background-color: ${BACKGROUND_HEADER_COLOR};
        padding-left: 18px;
        padding-right: 18px;
      }

      /* Toolbar */
      .o-toolbar-tools {
        display: flex;
        flex-shrink: 0;
        margin: 0px 6px 0px 16px;
        cursor: default;

        .o-divider {
          display: inline-block;
          border-right: 1px solid ${SEPARATOR_COLOR};
          width: 0;
          margin: 0 6px;
        }

        .o-dropdown {
          position: relative;
          display: flex;
          align-items: center;

          > span {
            height: 30px;
          }

          .o-dropdown-content {
            position: absolute;
            top: 100%;
            left: 0;
            overflow-y: auto;
            overflow-x: hidden;
            padding: 2px;
            z-index: ${ComponentsImportance.Dropdown};
            box-shadow: 1px 2px 5px 2px rgba(51, 51, 51, 0.15);
            background-color: white;

            .o-dropdown-line {
              display: flex;

              > span {
                padding: 4px;
              }
            }
          }
        }
      }
    }
  }
`;
    class TopBar extends owl.Component {
        static template = "o-spreadsheet-TopBar";
        get dropdownStyle() {
            return `max-height:${this.props.dropdownMaxHeight}px`;
        }
        static components = {
            ColorPickerWidget,
            ColorPicker,
            Menu,
            TopBarComposer,
            FontSizeEditor,
            ActionButton,
            PaintFormatButton,
            BorderEditorWidget,
        };
        state = owl.useState({
            menuState: { isOpen: false, position: null, menuItems: [] },
            activeTool: "",
            fillColor: "#ffffff",
            textColor: "#000000",
        });
        isSelectingMenu = false;
        openedEl = null;
        menus = [];
        EDIT = ACTION_EDIT;
        FORMAT = ACTION_FORMAT;
        VIEW = ACTION_VIEW;
        formatNumberMenuItemSpec = formatNumberMenuItemSpec;
        isntToolbarMenu = false;
        setup() {
            owl.useExternalListener(window, "click", this.onExternalClick);
            owl.onWillStart(() => this.updateCellState());
            owl.onWillUpdateProps(() => this.updateCellState());
        }
        get topbarComponents() {
            return topbarComponentRegistry
                .getAll()
                .filter((item) => !item.isVisible || item.isVisible(this.env));
        }
        onExternalClick(ev) {
            // TODO : manage click events better. We need this piece of code
            // otherwise the event opening the menu would close it on the same frame.
            // And we cannot stop the event propagation because it's used in an
            // external listener of the Menu component to close the context menu when
            // clicking on the top bar
            if (this.openedEl === ev.target) {
                return;
            }
            this.closeMenus();
        }
        onClick() {
            this.props.onClick();
            this.closeMenus();
        }
        onMenuMouseOver(menu, ev) {
            if (this.isSelectingMenu && this.isntToolbarMenu) {
                this.openMenu(menu, ev);
            }
        }
        toggleDropdownTool(tool, ev) {
            const isOpen = this.state.activeTool === tool;
            this.closeMenus();
            this.state.activeTool = isOpen ? "" : tool;
            this.openedEl = isOpen ? null : ev.target;
        }
        toggleContextMenu(menu, ev) {
            if (this.state.menuState.isOpen && this.isntToolbarMenu) {
                this.closeMenus();
            }
            else {
                this.openMenu(menu, ev);
                this.isntToolbarMenu = true;
            }
        }
        toggleToolbarContextMenu(menuSpec, ev) {
            if (this.state.menuState.isOpen && !this.isntToolbarMenu) {
                this.closeMenus();
            }
            else {
                const menu = createAction(menuSpec);
                this.openMenu(menu, ev);
                this.isntToolbarMenu = false;
            }
        }
        openMenu(menu, ev) {
            const { left, top, height } = ev.currentTarget.getBoundingClientRect();
            this.state.activeTool = "";
            this.state.menuState.isOpen = true;
            this.state.menuState.position = { x: left, y: top + height };
            this.state.menuState.menuItems = menu
                .children(this.env)
                .sort((a, b) => a.sequence - b.sequence);
            this.state.menuState.parentMenu = menu;
            this.isSelectingMenu = true;
            this.openedEl = ev.target;
            interactiveStopEdition(this.env);
        }
        closeMenus() {
            this.state.activeTool = "";
            this.state.menuState.isOpen = false;
            this.state.menuState.parentMenu = undefined;
            this.isSelectingMenu = false;
            this.openedEl = null;
        }
        updateCellState() {
            const style = this.env.model.getters.getCurrentStyle();
            this.state.fillColor = style.fillColor || "#ffffff";
            this.state.textColor = style.textColor || "#000000";
            this.menus = topbarMenuRegistry.getMenuItems();
        }
        getMenuName(menu) {
            return menu.name(this.env);
        }
        setColor(target, color) {
            setStyle(this.env, { [target]: color });
            this.onClick();
        }
    }
    TopBar.props = {
        onClick: Function,
        focusComposer: String,
        onComposerContentFocused: Function,
        dropdownMaxHeight: Number,
    };

    function instantiateClipboard() {
        return new WebClipboardWrapper(navigator.clipboard);
    }
    class WebClipboardWrapper {
        clipboard;
        // Can be undefined because navigator.clipboard doesn't exist in old browsers
        constructor(clipboard) {
            this.clipboard = clipboard;
        }
        async write(clipboardContent) {
            try {
                this.clipboard?.write(this.getClipboardItems(clipboardContent));
            }
            catch (e) { }
        }
        async writeText(text) {
            try {
                this.clipboard?.writeText(text);
            }
            catch (e) { }
        }
        async readText() {
            let permissionResult = undefined;
            try {
                //@ts-ignore - clipboard-read is not implemented in all browsers
                permissionResult = await navigator.permissions.query({ name: "clipboard-read" });
            }
            catch (e) { }
            try {
                const clipboardContent = await this.clipboard.readText();
                return { status: "ok", content: clipboardContent };
            }
            catch (e) {
                const status = permissionResult?.state === "denied" ? "permissionDenied" : "notImplemented";
                return { status };
            }
        }
        getClipboardItems(content) {
            return [
                new ClipboardItem({
                    [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText),
                    [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html),
                }),
            ];
        }
        getBlob(clipboardContent, type) {
            return new Blob([clipboardContent[type] || ""], {
                type,
            });
        }
    }

    // If we ever change these colors, make sure the filter tool stays green to match the icon in the grid
    const ACTIVE_BG_COLOR = BACKGROUND_HEADER_FILTER_COLOR;
    const ACTIVE_FONT_COLOR = FILTERS_COLOR;
    const HOVERED_BG_COLOR = BG_HOVER_COLOR;
    const HOVERED_FONT_COLOR = "#000";
    css /* scss */ `
  .o-spreadsheet {
    position: relative;
    display: grid;
    grid-template-columns: auto 350px;
    color: #333;
    input {
      background-color: white;
    }
    .text-muted {
      color: grey !important;
    }
    button {
      color: #333;
    }
    .o-disabled {
      opacity: 0.4;
      pointer: default;
      pointer-events: none;
    }

    &,
    *,
    *:before,
    *:after {
      box-sizing: content-box;
      /** rtl not supported ATM */
      direction: ltr;
    }
    .o-separator {
      border-bottom: ${MENU_SEPARATOR_BORDER_WIDTH}px solid ${SEPARATOR_COLOR};
      margin-top: ${MENU_SEPARATOR_PADDING}px;
      margin-bottom: ${MENU_SEPARATOR_PADDING}px;
    }
    .o-hoverable-button {
      border-radius: 2px;
      cursor: pointer;
      .o-icon {
        color: ${ICONS_COLOR};
      }
      &:not(.o-disabled):not(.active):hover {
        background-color: ${HOVERED_BG_COLOR};
        color: ${HOVERED_FONT_COLOR};
        .o-icon {
          color: ${HOVERED_FONT_COLOR};
        }
      }
      &.active {
        background-color: ${ACTIVE_BG_COLOR};
        color: ${ACTIVE_FONT_COLOR};
        .o-icon {
          color: ${ACTIVE_FONT_COLOR};
        }
      }
    }

    .o-grid-container {
      display: grid;
      background-color: ${HEADER_GROUPING_BACKGROUND_COLOR};

      .o-top-left {
        border: 1px solid ${GRID_BORDER_COLOR};
        margin-bottom: -1px;
        margin-right: -1px;
      }

      .o-column-groups {
        grid-column-start: 2;
        border-top: 1px solid ${GRID_BORDER_COLOR};
      }

      .o-row-groups {
        grid-row-start: 2;
      }

      .o-group-grid {
        border-top: 1px solid ${GRID_BORDER_COLOR};
        border-left: 1px solid ${GRID_BORDER_COLOR};
      }
    }
  }

  .o-two-columns {
    grid-column: 1 / 3;
  }

  .o-icon {
    width: ${ICON_EDGE_LENGTH}px;
    height: ${ICON_EDGE_LENGTH}px;
    vertical-align: middle;
  }

  .o-cf-icon {
    width: ${CF_ICON_EDGE_LENGTH}px;
    height: ${CF_ICON_EDGE_LENGTH}px;
    vertical-align: sub;
  }

  .o-text-icon {
    vertical-align: middle;
  }
`;
    // -----------------------------------------------------------------------------
    // GRID STYLE
    // -----------------------------------------------------------------------------
    css /* scss */ `
  .o-grid {
    position: relative;
    overflow: hidden;
    background-color: ${BACKGROUND_GRAY_COLOR};
    &:focus {
      outline: none;
    }

    > canvas {
      border-bottom: 1px solid #e2e3e3;
    }
    .o-scrollbar {
      &.corner {
        right: 0px;
        bottom: 0px;
        height: ${SCROLLBAR_WIDTH}px;
        width: ${SCROLLBAR_WIDTH}px;
        border-top: 1px solid #e2e3e3;
        border-left: 1px solid #e2e3e3;
      }
    }

    .o-grid-overlay {
      position: absolute;
      outline: none;
    }
  }

  .o-button {
    border: 1px solid lightgrey;
    padding: 0px 20px 0px 20px;
    border-radius: 4px;
    font-weight: 500;
    font-size: 14px;
    height: 30px;
    line-height: 16px;
    background: white;
    margin-right: 8px;
    &:hover:enabled {
      background-color: rgba(0, 0, 0, 0.08);
    }

    &:enabled {
      cursor: pointer;
    }

    &:disabled {
      color: ${DISABLED_TEXT_COLOR};
    }

    &:last-child {
      margin-right: 0px;
    }
  }

  .o-input {
    color: #666666;
    border-radius: 4px;
    min-width: 0px;
    padding: 4px 6px;
    box-sizing: border-box;
    line-height: 1;
    width: 100%;
    height: 28px;
  }
`;
    class Spreadsheet extends owl.Component {
        static template = "o-spreadsheet-Spreadsheet";
        static components = {
            TopBar,
            Grid,
            BottomBar,
            SidePanel,
            SpreadsheetDashboard,
            HeaderGroupContainer,
        };
        sidePanel;
        composer;
        _focusGrid;
        keyDownMapping;
        isViewportTooSmall = false;
        get model() {
            return this.props.model;
        }
        getStyle() {
            if (this.env.isDashboard()) {
                return `grid-template-rows: auto;`;
            }
            return `grid-template-rows: ${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;
        }
        setup() {
            this.sidePanel = owl.useState({ isOpen: false, panelProps: {} });
            this.composer = owl.useState({
                topBarFocus: "inactive",
                gridFocusMode: "inactive",
            });
            this.keyDownMapping = {
                "CTRL+H": () => this.toggleSidePanel("FindAndReplace", {}),
                "CTRL+F": () => this.toggleSidePanel("FindAndReplace", {}),
            };
            const fileStore = this.model.config.external.fileStore;
            owl.useSubEnv({
                model: this.model,
                imageProvider: fileStore ? new ImageProvider(fileStore) : undefined,
                loadCurrencies: this.model.config.external.loadCurrencies,
                loadLocales: this.model.config.external.loadLocales,
                isDashboard: () => this.model.getters.isDashboard(),
                openSidePanel: this.openSidePanel.bind(this),
                toggleSidePanel: this.toggleSidePanel.bind(this),
                clipboard: this.env.clipboard || instantiateClipboard(),
                startCellEdition: (content) => this.onGridComposerCellFocused(content),
                focusableElement: new FocusableElement(),
            });
            owl.useExternalListener(window, "resize", () => this.render(true));
            owl.useExternalListener(window, "beforeunload", this.unbindModelEvents.bind(this));
            this.bindModelEvents();
            owl.onWillUpdateProps((nextProps) => {
                if (nextProps.model !== this.props.model) {
                    throw new Error("Changing the props model is not supported at the moment.");
                }
            });
            owl.onMounted(() => {
                this.checkViewportSize();
            });
            owl.onWillUnmount(() => this.unbindModelEvents());
            owl.onPatched(() => {
                this.checkViewportSize();
            });
        }
        get focusTopBarComposer() {
            return this.model.getters.getEditionMode() === "inactive"
                ? "inactive"
                : this.composer.topBarFocus;
        }
        get focusGridComposer() {
            return this.model.getters.getEditionMode() === "inactive"
                ? "inactive"
                : this.composer.gridFocusMode;
        }
        bindModelEvents() {
            this.model.on("update", this, () => this.render(true));
            this.model.on("notify-ui", this, (notification) => this.env.notifyUser(notification));
            this.model.on("raise-error-ui", this, ({ text }) => this.env.raiseError(text));
        }
        unbindModelEvents() {
            this.model.off("update", this);
            this.model.off("notify-ui", this);
            this.model.off("raise-error-ui", this);
        }
        checkViewportSize() {
            const { xRatio, yRatio } = this.env.model.getters.getFrozenSheetViewRatio(this.env.model.getters.getActiveSheetId());
            if (!isFinite(xRatio) || !isFinite(yRatio)) {
                // before mounting, the ratios can be NaN or Infinity if the viewport size is 0
                return;
            }
            if (yRatio > MAXIMAL_FREEZABLE_RATIO || xRatio > MAXIMAL_FREEZABLE_RATIO) {
                if (this.isViewportTooSmall) {
                    return;
                }
                this.env.notifyUser({
                    text: _t("The current window is too small to display this sheet properly. Consider resizing your browser window or adjusting frozen rows and columns."),
                    type: "warning",
                    sticky: false,
                });
                this.isViewportTooSmall = true;
            }
            else {
                this.isViewportTooSmall = false;
            }
        }
        openSidePanel(panel, panelProps) {
            if (this.sidePanel.isOpen && panel !== this.sidePanel.component) {
                this.sidePanel.panelProps?.onCloseSidePanel?.();
            }
            this.sidePanel.component = panel;
            this.sidePanel.panelProps = panelProps;
            this.sidePanel.isOpen = true;
        }
        closeSidePanel() {
            this.sidePanel.isOpen = false;
            this.focusGrid();
            this.sidePanel.panelProps?.onCloseSidePanel?.();
        }
        toggleSidePanel(panel, panelProps) {
            if (this.sidePanel.isOpen && panel === this.sidePanel.component) {
                this.sidePanel.isOpen = false;
                this.focusGrid();
            }
            else {
                this.openSidePanel(panel, panelProps);
            }
        }
        focusGrid() {
            if (!this._focusGrid) {
                throw new Error("_focusGrid should be exposed by the grid component");
            }
            this._focusGrid();
        }
        onKeydown(ev) {
            let keyDownString = "";
            if (isCtrlKey(ev)) {
                keyDownString += "CTRL+";
            }
            keyDownString += ev.key.toUpperCase();
            let handler = this.keyDownMapping[keyDownString];
            if (handler) {
                ev.preventDefault();
                ev.stopPropagation();
                handler();
                return;
            }
        }
        onTopBarComposerFocused(selection) {
            if (this.model.getters.isReadonly()) {
                return;
            }
            this.composer.topBarFocus = "contentFocus";
            this.composer.gridFocusMode = "inactive";
            this.setComposerContent({ selection });
        }
        onGridComposerContentFocused(selection) {
            if (this.model.getters.isReadonly()) {
                return;
            }
            this.composer.topBarFocus = "inactive";
            this.composer.gridFocusMode = "contentFocus";
            this.setComposerContent({ selection });
        }
        // TODO: either both are defined or none of them. change those args to an object
        onGridComposerCellFocused(content, selection) {
            if (this.model.getters.isReadonly()) {
                return;
            }
            this.composer.topBarFocus = "inactive";
            this.composer.gridFocusMode = "cellFocus";
            this.setComposerContent({ content, selection } || {});
        }
        /**
         * Start the edition or update the content if it's already started.
         */
        setComposerContent({ content, selection, }) {
            if (this.model.getters.getEditionMode() === "inactive") {
                this.model.dispatch("START_EDITION", { text: content, selection });
            }
            else if (content) {
                this.model.dispatch("SET_CURRENT_CONTENT", { content, selection });
            }
        }
        get gridHeight() {
            const { height } = this.env.model.getters.getSheetViewDimension();
            return height;
        }
        get gridContainerStyle() {
            const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
            const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;
            return cssPropertiesToCss({
                "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`,
                "grid-template-rows": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,
            });
        }
        get rowLayers() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            return this.env.model.getters.getVisibleGroupLayers(sheetId, "ROW");
        }
        get colLayers() {
            const sheetId = this.env.model.getters.getActiveSheetId();
            return this.env.model.getters.getVisibleGroupLayers(sheetId, "COL");
        }
    }
    Spreadsheet.props = {
        model: Object,
    };

    class LocalTransportService {
        listeners = [];
        sendMessage(message) {
            for (const { callback } of this.listeners) {
                callback(message);
            }
        }
        onNewMessage(id, callback) {
            this.listeners.push({ id, callback });
        }
        leave(id) {
            this.listeners = this.listeners.filter((listener) => listener.id !== id);
        }
    }

    function inverseCommand(cmd) {
        return inverseCommandRegistry.get(cmd.type)(cmd);
    }

    /**
     * Create an empty structure according to the type of the node key:
     * string: object
     * number: array
     */
    function createEmptyStructure(node) {
        if (typeof node === "string") {
            return {};
        }
        else if (typeof node === "number") {
            return [];
        }
        throw new Error(`Cannot create new node`);
    }

    /**
     * A branch holds a sequence of operations.
     * It can be represented as "A - B - C - D" if A, B, C and D are executed one
     * after the other.
     *
     * @param buildTransformation Factory to build transformations
     * @param operations initial operations
     */
    class Branch {
        buildTransformation;
        operations;
        constructor(buildTransformation, operations = []) {
            this.buildTransformation = buildTransformation;
            this.operations = operations;
        }
        getOperations() {
            return this.operations;
        }
        getOperation(operationId) {
            const operation = this.operations.find((op) => op.id === operationId);
            if (!operation) {
                throw new Error(`Operation ${operationId} not found`);
            }
            return operation;
        }
        getLastOperationId() {
            return this.operations[this.operations.length - 1]?.id;
        }
        /**
         * Get the id of the operation appears first in the list of operations
         */
        getFirstOperationAmong(op1, op2) {
            for (const operation of this.operations) {
                if (operation.id === op1)
                    return op1;
                if (operation.id === op2)
                    return op2;
            }
            throw new Error(`Operation ${op1} and ${op2} not found`);
        }
        contains(operationId) {
            return !!this.operations.find((operation) => operation.id === operationId);
        }
        /**
         * Add the given operation as the first operation
         */
        prepend(operation) {
            const transformation = this.buildTransformation.with(operation.data);
            this.operations = [
                operation,
                ...this.operations.map((operation) => operation.transformed(transformation)),
            ];
        }
        /**
         * add the given operation after the given predecessorOpId
         */
        insert(newOperation, predecessorOpId) {
            const transformation = this.buildTransformation.with(newOperation.data);
            const { before, operation, after } = this.locateOperation(predecessorOpId);
            this.operations = [
                ...before,
                operation,
                newOperation,
                ...after.map((operation) => operation.transformed(transformation)),
            ];
        }
        /**
         * Add the given operation as the last operation
         */
        append(operation) {
            this.operations.push(operation);
        }
        /**
         * Append operations in the given branch to this branch.
         */
        appendBranch(branch) {
            this.operations = this.operations.concat(branch.operations);
        }
        /**
         * Create and return a copy of this branch, starting after the given operationId
         */
        fork(operationId) {
            const { after } = this.locateOperation(operationId);
            return new Branch(this.buildTransformation, after);
        }
        /**
         * Transform all the operations in this branch with the given transformation
         */
        transform(transformation) {
            this.operations = this.operations.map((operation) => operation.transformed(transformation));
        }
        /**
         * Cut the branch before the operation, meaning the operation
         * and all following operations are dropped.
         */
        cutBefore(operationId) {
            this.operations = this.locateOperation(operationId).before;
        }
        /**
         * Cut the branch after the operation, meaning all following operations are dropped.
         */
        cutAfter(operationId) {
            const { before, operation } = this.locateOperation(operationId);
            this.operations = before.concat([operation]);
        }
        /**
         * Find an operation in this branch based on its id.
         * This returns the operation itself, operations which comes before it
         * and operation which comes after it.
         */
        locateOperation(operationId) {
            const operationIndex = this.operations.findIndex((step) => step.id === operationId);
            if (operationIndex === -1) {
                throw new Error(`Operation ${operationId} not found`);
            }
            return {
                before: this.operations.slice(0, operationIndex),
                operation: this.operations[operationIndex],
                after: this.operations.slice(operationIndex + 1),
            };
        }
    }

    /**
     * An Operation can be executed to change a data structure from state A
     * to state B.
     * It should hold the necessary data used to perform this transition.
     * It should be possible to revert the changes made by this operation.
     *
     * In the context of o-spreadsheet, the data from an operation would
     * be a revision (the commands are used to execute it, the `changes` are used
     * to revert it).
     */
    class Operation {
        id;
        data;
        constructor(id, data) {
            this.id = id;
            this.data = data;
        }
        transformed(transformation) {
            return new LazyOperation(this.id, lazy(() => transformation(this.data)));
        }
    }
    class LazyOperation {
        id;
        lazyData;
        constructor(id, lazyData) {
            this.id = id;
            this.lazyData = lazyData;
        }
        get data() {
            return this.lazyData();
        }
        transformed(transformation) {
            return new LazyOperation(this.id, this.lazyData.map(transformation));
        }
    }

    /**
     * An execution object is a sequence of executionSteps (each execution step is an operation in a branch).
     *
     * You can iterate over the steps of an execution
     * ```js
     * for (const operation of execution) {
     *   // ... do something
     * }
     * ```
     */
    class OperationSequence {
        operations;
        constructor(operations) {
            this.operations = operations;
        }
        [Symbol.iterator]() {
            return this.operations[Symbol.iterator]();
        }
        /**
         * Stop the operation sequence at a given operation
         * @param operationId included
         */
        stopWith(operationId) {
            function* filter(execution, operationId) {
                for (const step of execution) {
                    yield step;
                    if (step.operation.id === operationId) {
                        return;
                    }
                }
            }
            return new OperationSequence(filter(this.operations, operationId));
        }
        /**
         * Stop the operation sequence before a given operation
         * @param operationId excluded
         */
        stopBefore(operationId) {
            function* filter(execution, operationId) {
                for (const step of execution) {
                    if (step.operation.id === operationId) {
                        return;
                    }
                    yield step;
                }
            }
            return new OperationSequence(filter(this.operations, operationId));
        }
        /**
         * Start the operation sequence at a given operation
         * @param operationId excluded
         */
        startAfter(operationId) {
            function* filter(execution, operationId) {
                let skip = true;
                for (const step of execution) {
                    if (!skip) {
                        yield step;
                    }
                    if (step.operation.id === operationId) {
                        skip = false;
                    }
                }
            }
            return new OperationSequence(filter(this.operations, operationId));
        }
    }

    /**
     * The tree is a data structure used to maintain the different branches of the
     * SelectiveHistory.
     *
     * Branches can be "stacked" on each other and an execution path can be derived
     * from any stack of branches. The rules to derive this path is explained below.
     *
     * An operation can be cancelled/undone by inserting a new branch below
     * this operation.
     * e.g
     *    Given the branch A    B   C
     *    To undo B, a new branching branch is inserted at operation B.
     *    ```txt
     *    A   B   C   D
     *        >   C'  D'
     *    ```
     *    A new execution path can now be derived. At each operation:
     *    - if there is a lower branch, don't execute it and go to the operation below
     *    - if not, execute it and go to the operation on the right.
     *    The execution path is   A   C'    D'
     *    Operation C and D have been adapted (transformed) in the lower branch
     *    since operation B is not executed in this branch.
     *
     */
    class Tree {
        buildTransformation;
        branches;
        branchingOperationIds = new Map();
        constructor(buildTransformation, initialBranch) {
            this.buildTransformation = buildTransformation;
            this.branches = [initialBranch];
        }
        /**
         * Return the last branch of the entire stack of branches.
         */
        getLastBranch() {
            return this.branches[this.branches.length - 1];
        }
        /**
         * Return the sequence of operations from this branch
         * until the very last branch.
         */
        execution(branch) {
            return new OperationSequence(linkNext(this._execution(branch), this._execution(branch)));
        }
        /**
         * Return the sequence of operations from this branch
         * to the very first branch.
         */
        revertedExecution(branch) {
            return new OperationSequence(linkNext(this._revertedExecution(branch), this._revertedExecution(branch)));
        }
        /**
         * Append an operation to the end of the tree.
         * Also insert the (transformed) operation in all previous branches.
         *
         * Adding operation `D` to the last branch
         * ```txt
         *  A1   B1   C1
         *  >    B2   C2
         * ```
         * will give
         * ```txt
         *  A1   B1   C1   D'   with D' = D transformed with A1
         *  >    B2   C2   D
         * ```
         */
        insertOperationLast(branch, operation) {
            const insertAfter = branch.getLastOperationId() || this.previousBranch(branch)?.getLastOperationId();
            branch.append(operation);
            if (insertAfter) {
                this.insertPrevious(branch, operation, insertAfter);
            }
        }
        /**
         * Insert a new operation after an other operation.
         * The operation will be inserted in this branch, in next branches (transformed)
         * and in previous branches (also transformed).
         *
         * Given
         * ```txt
         *  1: A1   B1   C1
         *  2: >    B2   C2
         *  3:      >    C3
         * ```
         * Inserting D to branch 2 gives
         * ```txt
         *  1: A1   B1   C1   D1          D1 = D transformed with A1
         *  2: >    B2   C2   D     with  D  = D
         *  3:      >    C3   D2          D2 = D transformed without B2 (B2⁻¹)
         * ```
         */
        insertOperationAfter(branch, operation, predecessorOpId) {
            branch.insert(operation, predecessorOpId);
            this.updateNextWith(branch, operation, predecessorOpId);
            this.insertPrevious(branch, operation, predecessorOpId);
        }
        /**
         * Create a new branching branch at the given operation.
         * This cancels the operation from the execution path.
         */
        undo(branch, operation) {
            const transformation = this.buildTransformation.without(operation.data);
            const branchingId = this.branchingOperationIds.get(branch);
            this.branchingOperationIds.set(branch, operation.id);
            const nextBranch = branch.fork(operation.id);
            if (branchingId) {
                this.branchingOperationIds.set(nextBranch, branchingId);
            }
            this.insertBranchAfter(branch, nextBranch);
            this.transform(nextBranch, transformation);
        }
        /**
         * Remove the branch just after this one. This un-cancels (redo) the branching
         * operation. Lower branches will be transformed accordingly.
         *
         * Given
         * ```txt
         *  1: A1   B1   C1
         *  2: >    B2   C2
         *  3:      >    C3
         * ```
         * removing the next branch of 1 gives
         *
         * ```txt
         *  1: A1   B1   C1
         *  2:      >    C3'   with  C3' = C1 transformed without B1 (B1⁻¹)
         * ```
         */
        redo(branch) {
            const removedBranch = this.nextBranch(branch);
            if (!removedBranch)
                return;
            const nextBranch = this.nextBranch(removedBranch);
            this.removeBranchFromTree(removedBranch);
            const undoBranchingId = this.branchingOperationIds.get(removedBranch);
            if (undoBranchingId) {
                this.branchingOperationIds.set(branch, undoBranchingId);
            }
            else {
                this.branchingOperationIds.delete(branch);
            }
            if (nextBranch) {
                this.rebaseUp(nextBranch);
            }
        }
        /**
         * Drop the operation and all following operations in every
         * branch
         */
        drop(operationId) {
            for (const branch of this.branches) {
                if (branch.contains(operationId)) {
                    branch.cutBefore(operationId);
                }
            }
        }
        /**
         * Find the operation in the execution path.
         */
        findOperation(branch, operationId) {
            for (const operation of this.revertedExecution(branch)) {
                if (operation.operation.id === operationId) {
                    return operation;
                }
            }
            throw new Error(`Operation ${operationId} not found`);
        }
        /**
         * Rebuild transformed operations of this branch based on the upper branch.
         *
         * Given the following structure:
         * ```txt
         *  1: A1   B1    C1
         *  2: >    B2    C2
         *  3:      >     C3
         * ```
         * Rebasing branch "2" gives
         * ```txt
         *  1: A1   B1    C1
         *  2: >    B2'   C2'  With  B2' = B1 transformed without A1 and C2' = C1 transformed without A1
         *  3:      >     C3'        C3' = C2' transformed without B2'
         * ```
         */
        rebaseUp(branch) {
            const { previousBranch, branchingOperation } = this.findPreviousBranchingOperation(branch);
            if (!previousBranch || !branchingOperation)
                return;
            const rebaseTransformation = this.buildTransformation.without(branchingOperation.data);
            const newBranch = previousBranch.fork(branchingOperation.id);
            this.branchingOperationIds.set(newBranch, this.branchingOperationIds.get(branch));
            this.removeBranchFromTree(branch);
            this.insertBranchAfter(previousBranch, newBranch);
            newBranch.transform(rebaseTransformation);
            const nextBranch = this.nextBranch(newBranch);
            if (nextBranch) {
                this.rebaseUp(nextBranch);
            }
        }
        removeBranchFromTree(branch) {
            const index = this.branches.findIndex((l) => l === branch);
            this.branches.splice(index, 1);
        }
        insertBranchAfter(branch, toInsert) {
            const index = this.branches.findIndex((l) => l === branch);
            this.branches.splice(index + 1, 0, toInsert);
        }
        /**
         * Update the branching branch of this branch, either by (1) inserting the new
         * operation in it or (2) by transforming it.
         * (1) If the operation is positioned before the branching branch, the branching
         *     branch should be transformed with this operation.
         * (2) If it's positioned after, the operation should be inserted in the
         *     branching branch.
         */
        updateNextWith(branch, operation, predecessorOpId) {
            const branchingId = this.branchingOperationIds.get(branch);
            const nextBranch = this.nextBranch(branch);
            if (!branchingId || !nextBranch) {
                return;
            }
            if (branch.getFirstOperationAmong(predecessorOpId, branchingId) === branchingId) {
                const transformedOperation = this.addToNextBranch(branch, nextBranch, branchingId, operation, predecessorOpId);
                this.updateNextWith(nextBranch, transformedOperation, predecessorOpId);
            }
            else {
                const transformation = this.buildTransformation.with(operation.data);
                this.transform(nextBranch, transformation);
            }
        }
        addToNextBranch(branch, nextBranch, branchingId, operation, predecessorOpId) {
            // If the operation is inserted after the branching operation, it should
            // be positioned first.
            let transformedOperation = operation;
            if (predecessorOpId === branchingId) {
                transformedOperation = this.getTransformedOperation(branch, branchingId, operation);
                nextBranch.prepend(transformedOperation);
            }
            else if (nextBranch.contains(predecessorOpId)) {
                transformedOperation = this.getTransformedOperation(branch, branchingId, operation);
                nextBranch.insert(transformedOperation, predecessorOpId);
            }
            else {
                nextBranch.append(operation);
            }
            return transformedOperation;
        }
        getTransformedOperation(branch, branchingId, operation) {
            const branchingOperation = branch.getOperation(branchingId);
            const branchingTransformation = this.buildTransformation.without(branchingOperation.data);
            return operation.transformed(branchingTransformation);
        }
        /**
         * Check if this branch should execute the given operation.
         * i.e. If the operation is not cancelled by a branching branch.
         */
        shouldExecute(branch, operation) {
            return operation.id !== this.branchingOperationIds.get(branch);
        }
        transform(branch, transformation) {
            branch.transform(transformation);
            const nextBranch = this.nextBranch(branch);
            if (nextBranch) {
                this.transform(nextBranch, transformation);
            }
        }
        /**
         * Insert a new operation in previous branches. The operations which are
         * positioned after the inserted operations are transformed with the newly
         * inserted operations. This one is also transformed, with the branching
         * operation.
         */
        insertPrevious(branch, newOperation, insertAfter) {
            const { previousBranch, branchingOperation } = this.findPreviousBranchingOperation(branch);
            if (!previousBranch || !branchingOperation)
                return;
            const transformation = this.buildTransformation.with(branchingOperation.data);
            const branchTail = branch.fork(insertAfter);
            branchTail.transform(transformation);
            previousBranch.cutAfter(insertAfter);
            previousBranch.appendBranch(branchTail);
            const operationToInsert = newOperation.transformed(transformation);
            this.insertPrevious(previousBranch, operationToInsert, insertAfter);
        }
        findPreviousBranchingOperation(branch) {
            const previousBranch = this.previousBranch(branch);
            if (!previousBranch)
                return { previousBranch: undefined, branchingOperation: undefined };
            const previousBranchingId = this.branchingOperationIds.get(previousBranch);
            if (!previousBranchingId)
                return { previousBranch: undefined, branchingOperation: undefined };
            return {
                previousBranch,
                branchingOperation: previousBranch.getOperation(previousBranchingId),
            };
        }
        /**
         * Retrieve the next branch of the given branch
         */
        nextBranch(branch) {
            const index = this.branches.findIndex((l) => l === branch);
            if (index === -1) {
                return undefined;
            }
            return this.branches[index + 1];
        }
        /**
         * Retrieve the previous branch of the given branch
         */
        previousBranch(branch) {
            const index = this.branches.findIndex((l) => l === branch);
            if (index === -1) {
                return undefined;
            }
            return this.branches[index - 1];
        }
        /**
         * Yields the sequence of operations to execute, in reverse order.
         */
        *_revertedExecution(branch) {
            const branchingOperationId = this.branchingOperationIds.get(branch);
            let afterBranchingPoint = !!branchingOperationId;
            const operations = branch.getOperations();
            for (let i = operations.length - 1; i >= 0; i--) {
                const operation = operations[i];
                if (operation.id === branchingOperationId) {
                    afterBranchingPoint = false;
                }
                if (!afterBranchingPoint) {
                    yield {
                        operation: operation,
                        branch: branch,
                        isCancelled: !this.shouldExecute(branch, operation),
                    };
                }
            }
            const previous = this.previousBranch(branch);
            yield* previous ? this._revertedExecution(previous) : [];
        }
        /**
         * Yields the sequence of operations to execute
         */
        *_execution(branch) {
            for (const operation of branch.getOperations()) {
                yield {
                    operation: operation,
                    branch: branch,
                    isCancelled: !this.shouldExecute(branch, operation),
                };
                if (operation.id === this.branchingOperationIds.get(branch)) {
                    const next = this.nextBranch(branch);
                    yield* next ? this._execution(next) : [];
                    return;
                }
            }
            if (!this.branchingOperationIds.get(branch)) {
                const next = this.nextBranch(branch);
                yield* next ? this._execution(next) : [];
            }
        }
    }

    class SelectiveHistory {
        HEAD_BRANCH;
        HEAD_OPERATION;
        tree;
        applyOperation;
        revertOperation;
        buildEmpty;
        buildTransformation;
        /**
         * The selective history is a data structure used to register changes/updates of a state.
         * Each change/update is called an "operation".
         * The data structure allows to easily cancel (and redo) any operation individually.
         * An operation can be represented by any data structure. It can be a "command", a "diff", etc.
         * However it must have the following properties:
         * - it can be applied to modify the state
         * - it can be reverted on the state such that it was never executed.
         * - it can be transformed given other operation (Operational Transformation)
         *
         * Since this data structure doesn't know anything about the state nor the structure of
         * operations, the actual work must be performed by external functions given as parameters.
         * @param initialOperationId
         * @param applyOperation a function which can apply an operation to the state
         * @param revertOperation  a function which can revert an operation from the state
         * @param buildEmpty  a function returning an "empty" operation.
         *                    i.e an operation that leaves the state unmodified once applied or reverted
         *                    (used for internal implementation)
         * @param buildTransformation Factory used to build transformations
         */
        constructor(args) {
            this.applyOperation = args.applyOperation;
            this.revertOperation = args.revertOperation;
            this.buildEmpty = args.buildEmpty;
            this.buildTransformation = args.buildTransformation;
            this.HEAD_BRANCH = new Branch(this.buildTransformation);
            this.tree = new Tree(this.buildTransformation, this.HEAD_BRANCH);
            const initialOperationId = args.initialOperationId;
            const initial = new Operation(initialOperationId, this.buildEmpty(initialOperationId));
            this.tree.insertOperationLast(this.HEAD_BRANCH, initial);
            this.HEAD_OPERATION = initial;
        }
        /**
         * Return the operation identified by its id.
         */
        get(operationId) {
            return this.tree.findOperation(this.HEAD_BRANCH, operationId).operation.data;
        }
        /**
         * Append a new operation as the last one
         */
        append(operationId, data) {
            const operation = new Operation(operationId, data);
            const branch = this.tree.getLastBranch();
            this.tree.insertOperationLast(branch, operation);
            this.HEAD_BRANCH = branch;
            this.HEAD_OPERATION = operation;
        }
        /**
         * Insert a new operation after a specific operation (may not be the last operation).
         * Following operations will be transformed according
         * to the new operation.
         */
        insert(operationId, data, insertAfter) {
            const operation = new Operation(operationId, data);
            this.revertTo(insertAfter);
            this.tree.insertOperationAfter(this.HEAD_BRANCH, operation, insertAfter);
            this.fastForward();
        }
        /**
         * @param operationId operation to undo
         * @param undoId the id of the "undo operation"
         * @param insertAfter the id of the operation after which to insert the undo
         */
        undo(operationId, undoId, insertAfter) {
            const { branch, operation } = this.tree.findOperation(this.HEAD_BRANCH, operationId);
            this.revertBefore(operationId);
            this.tree.undo(branch, operation);
            this.fastForward();
            this.insert(undoId, this.buildEmpty(undoId), insertAfter);
        }
        /**
         * @param operationId operation to redo
         * @param redoId the if of the "redo operation"
         * @param insertAfter the id of the operation after which to insert the redo
         */
        redo(operationId, redoId, insertAfter) {
            const { branch } = this.tree.findOperation(this.HEAD_BRANCH, operationId);
            this.revertBefore(operationId);
            this.tree.redo(branch);
            this.fastForward();
            this.insert(redoId, this.buildEmpty(redoId), insertAfter);
        }
        drop(operationId) {
            this.revertBefore(operationId);
            this.tree.drop(operationId);
        }
        getRevertedExecution() {
            const data = [];
            const operations = this.tree.revertedExecution(this.HEAD_BRANCH);
            for (const { operation } of operations) {
                data.push(operation.data);
            }
            return data;
        }
        /**
         * Revert the state as it was *before* the given operation was executed.
         */
        revertBefore(operationId) {
            const execution = this.tree.revertedExecution(this.HEAD_BRANCH).stopWith(operationId);
            this.revert(execution);
        }
        /**
         * Revert the state as it was *after* the given operation was executed.
         */
        revertTo(operationId) {
            const execution = operationId
                ? this.tree.revertedExecution(this.HEAD_BRANCH).stopBefore(operationId)
                : this.tree.revertedExecution(this.HEAD_BRANCH);
            this.revert(execution);
        }
        /**
         * Revert an execution
         */
        revert(execution) {
            for (const { next, operation, isCancelled } of execution) {
                if (!isCancelled) {
                    this.revertOperation(operation.data);
                }
                if (next) {
                    this.HEAD_BRANCH = next.branch;
                    this.HEAD_OPERATION = next.operation;
                }
            }
        }
        /**
         * Replay the operations between the current HEAD_BRANCH and the end of the tree
         */
        fastForward() {
            const operations = this.HEAD_OPERATION
                ? this.tree.execution(this.HEAD_BRANCH).startAfter(this.HEAD_OPERATION.id)
                : this.tree.execution(this.HEAD_BRANCH);
            for (const { operation: operation, branch, isCancelled } of operations) {
                if (!isCancelled) {
                    this.applyOperation(operation.data);
                }
                this.HEAD_OPERATION = operation;
                this.HEAD_BRANCH = branch;
            }
        }
    }

    function buildRevisionLog(args) {
        return new SelectiveHistory({
            initialOperationId: args.initialRevisionId,
            applyOperation: (revision) => {
                const commands = revision.commands.slice();
                const { changes } = args.recordChanges(() => {
                    for (const command of commands) {
                        args.dispatch(command);
                    }
                });
                revision.setChanges(changes);
            },
            revertOperation: (revision) => revertChanges([revision]),
            buildEmpty: (id) => new Revision(id, "empty", []),
            buildTransformation: {
                with: (revision) => (toTransform) => {
                    return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands), toTransform.rootCommand, undefined, toTransform.timestamp);
                },
                without: (revision) => (toTransform) => {
                    return new Revision(toTransform.id, toTransform.clientId, transformAll(toTransform.commands, revision.commands.map(inverseCommand).flat()), toTransform.rootCommand, undefined, toTransform.timestamp);
                },
            },
        });
    }
    /**
     * Revert changes from the given revisions
     */
    function revertChanges(revisions) {
        for (const revision of revisions.slice().reverse()) {
            for (let i = revision.changes.length - 1; i >= 0; i--) {
                const change = revision.changes[i];
                applyChange(change, "before");
            }
        }
    }
    /**
     * Apply the changes of the given HistoryChange to the state
     */
    function applyChange(change, target) {
        let val = change.path[0];
        const key = change.path.at(-1);
        for (let pathIndex = 1; pathIndex < change.path.slice(0, -1).length; pathIndex++) {
            const p = change.path[pathIndex];
            if (val[p] === undefined) {
                const nextPath = change.path[pathIndex + 1];
                val[p] = createEmptyStructure(nextPath);
            }
            val = val[p];
        }
        if (change[target] === undefined) {
            delete val[key];
        }
        else {
            val[key] = change[target];
        }
    }

    /**
     * Stateless sequence of events that can be processed by consumers.
     *
     * There are three kind of consumers:
     * - the main consumer
     * - the default consumer
     * - observer consumers
     *
     * Main consumer
     * -------------
     * Anyone can capture the event stream and become the main consumer.
     * If there is already a main consumer, it is kicked off and it will no longer
     * receive events.
     * The main consumer can release the stream at any moment to stop listening
     * events.
     *
     * Default consumer
     * ----------------
     * When the main consumer releases the stream and until the stream is captured
     * again, all events are transmitted to the default consumer.
     *
     * Observer consumers
     * ------------------
     * Observers permanently receive events.
     *
     */
    class EventStream {
        observers = new Map();
        /**
         * the one we default to when someone releases the stream by themeselves
         */
        defaultSubscription;
        mainSubscription;
        registerAsDefault(owner, callbacks) {
            this.defaultSubscription = { owner, callbacks };
            if (!this.mainSubscription) {
                this.mainSubscription = this.defaultSubscription;
            }
        }
        /**
         * Register callbacks to observe the stream
         */
        observe(owner, callbacks) {
            this.observers.set(owner, { owner, callbacks });
        }
        /**
         * Capture the stream for yourself
         */
        capture(owner, callbacks) {
            if (this.observers.get(owner)) {
                throw new Error("You are already subscribed forever");
            }
            if (this.mainSubscription?.owner && this.mainSubscription.owner !== owner) {
                this.mainSubscription.callbacks.release?.();
            }
            this.mainSubscription = { owner, callbacks };
        }
        release(owner) {
            if (this.mainSubscription?.owner !== owner || this.observers.get(owner)) {
                return;
            }
            this.mainSubscription = this.defaultSubscription;
        }
        /**
         * Release whichever subscription in charge and get back to the default subscription
         */
        getBackToDefault() {
            if (this.mainSubscription === this.defaultSubscription) {
                return;
            }
            this.mainSubscription?.callbacks.release?.();
            this.mainSubscription = this.defaultSubscription;
        }
        /**
         * Check if you are currently the main stream consumer
         */
        isListening(owner) {
            return this.mainSubscription?.owner === owner;
        }
        /**
         * Push an event to the stream and broadcast it to consumers
         */
        send(event) {
            this.mainSubscription?.callbacks.handleEvent(event);
            this.observers.forEach((sub) => sub.callbacks.handleEvent(event));
        }
    }

    /**
     * Processes all selection updates (usually from user inputs) and emits an event
     * with the new selected anchor
     */
    class SelectionStreamProcessorImpl {
        getters;
        stream;
        /**
         * "Active" anchor used as a reference to compute new anchors
         * An new initial value is given each time the stream is
         * captured. The value is updated with each new anchor.
         */
        anchor;
        defaultAnchor;
        constructor(getters) {
            this.getters = getters;
            this.stream = new EventStream();
            this.anchor = { cell: { col: 0, row: 0 }, zone: positionToZone({ col: 0, row: 0 }) };
            this.defaultAnchor = this.anchor;
        }
        capture(owner, anchor, callbacks) {
            this.stream.capture(owner, callbacks);
            this.anchor = anchor;
        }
        /**
         * Register as default subscriber and capture the event stream.
         */
        registerAsDefault(owner, anchor, callbacks) {
            this.checkAnchorZoneOrThrow(anchor);
            this.stream.registerAsDefault(owner, callbacks);
            this.defaultAnchor = anchor;
            this.capture(owner, anchor, callbacks);
        }
        resetDefaultAnchor(owner, anchor) {
            this.checkAnchorZoneOrThrow(anchor);
            if (this.stream.isListening(owner)) {
                this.anchor = anchor;
            }
            this.defaultAnchor = anchor;
        }
        resetAnchor(owner, anchor) {
            this.checkAnchorZoneOrThrow(anchor);
            if (this.stream.isListening(owner)) {
                this.anchor = anchor;
            }
        }
        observe(owner, callbacks) {
            this.stream.observe(owner, callbacks);
        }
        release(owner) {
            if (this.stream.isListening(owner)) {
                this.stream.release(owner);
                this.anchor = this.defaultAnchor;
            }
        }
        getBackToDefault() {
            this.stream.getBackToDefault();
        }
        modifyAnchor(anchor, mode, options) {
            const sheetId = this.getters.getActiveSheetId();
            anchor = {
                ...anchor,
                zone: this.getters.expandZone(sheetId, anchor.zone),
            };
            return this.processEvent({
                options,
                anchor,
                mode,
            });
        }
        /**
         * Select a new anchor
         */
        selectZone(anchor, options = { scrollIntoView: true }) {
            return this.modifyAnchor(anchor, "overrideSelection", options);
        }
        /**
         * Select a single cell as the new anchor.
         */
        selectCell(col, row) {
            const zone = positionToZone({ col, row });
            return this.selectZone({ zone, cell: { col, row } }, { scrollIntoView: true });
        }
        /**
         * Set the selection to one of the cells adjacent to the current anchor cell.
         */
        moveAnchorCell(direction, step = 1) {
            if (step !== "end" && step <= 0) {
                return new DispatchResult("InvalidSelectionStep" /* CommandResult.InvalidSelectionStep */);
            }
            const { col, row } = this.getNextAvailablePosition(direction, step);
            return this.selectCell(col, row);
        }
        /**
         * Update the current anchor such that it includes the given
         * cell position.
         */
        setAnchorCorner(col, row) {
            const sheetId = this.getters.getActiveSheetId();
            const { col: anchorCol, row: anchorRow } = this.anchor.cell;
            const zone = {
                left: Math.min(anchorCol, col),
                top: Math.min(anchorRow, row),
                right: Math.max(anchorCol, col),
                bottom: Math.max(anchorRow, row),
            };
            const expandedZone = this.getters.expandZone(sheetId, zone);
            const anchor = { zone: expandedZone, cell: { col: anchorCol, row: anchorRow } };
            return this.processEvent({
                mode: "updateAnchor",
                anchor: anchor,
                options: { scrollIntoView: false },
            });
        }
        /**
         * Add a new cell to the current selection
         */
        addCellToSelection(col, row) {
            const sheetId = this.getters.getActiveSheetId();
            ({ col, row } = this.getters.getMainCellPosition({ sheetId, col, row }));
            const zone = this.getters.expandZone(sheetId, positionToZone({ col, row }));
            return this.processEvent({
                options: { scrollIntoView: true },
                anchor: { zone, cell: { col, row } },
                mode: "newAnchor",
            });
        }
        /**
         * Increase or decrease the size of the current anchor zone.
         * The anchor cell remains where it is. It's the opposite side
         * of the anchor zone which moves.
         */
        resizeAnchorZone(direction, step = 1) {
            if (step !== "end" && step <= 0) {
                return new DispatchResult("InvalidSelectionStep" /* CommandResult.InvalidSelectionStep */);
            }
            const sheetId = this.getters.getActiveSheetId();
            const anchor = this.anchor;
            const { col: anchorCol, row: anchorRow } = anchor.cell;
            const { left, right, top, bottom } = anchor.zone;
            const starting = this.getStartingPosition(direction);
            let [deltaCol, deltaRow] = this.deltaToTarget(starting, direction, step);
            if (deltaCol === 0 && deltaRow === 0) {
                return DispatchResult.Success;
            }
            let result = anchor.zone;
            const expand = (z) => {
                z = organizeZone(z);
                const { left, right, top, bottom } = this.getters.expandZone(sheetId, z);
                return {
                    left: Math.max(0, left),
                    right: Math.min(this.getters.getNumberCols(sheetId) - 1, right),
                    top: Math.max(0, top),
                    bottom: Math.min(this.getters.getNumberRows(sheetId) - 1, bottom),
                };
            };
            const { col: refCol, row: refRow } = this.getReferencePosition();
            // check if we can shrink selection
            let n = 0;
            while (result !== null) {
                n++;
                if (deltaCol < 0) {
                    const newRight = this.getNextAvailableCol(deltaCol, right - (n - 1), refRow);
                    result = refCol <= right - n ? expand({ top, left, bottom, right: newRight }) : null;
                }
                if (deltaCol > 0) {
                    const newLeft = this.getNextAvailableCol(deltaCol, left + (n - 1), refRow);
                    result = left + n <= refCol ? expand({ top, left: newLeft, bottom, right }) : null;
                }
                if (deltaRow < 0) {
                    const newBottom = this.getNextAvailableRow(deltaRow, refCol, bottom - (n - 1));
                    result = refRow <= bottom - n ? expand({ top, left, bottom: newBottom, right }) : null;
                }
                if (deltaRow > 0) {
                    const newTop = this.getNextAvailableRow(deltaRow, refCol, top + (n - 1));
                    result = top + n <= refRow ? expand({ top: newTop, left, bottom, right }) : null;
                }
                result = result ? organizeZone(result) : result;
                if (result && !isEqual(result, anchor.zone)) {
                    return this.processEvent({
                        options: { scrollIntoView: true },
                        mode: "updateAnchor",
                        anchor: { zone: result, cell: { col: anchorCol, row: anchorRow } },
                    });
                }
            }
            const currentZone = {
                top: anchorRow,
                bottom: anchorRow,
                left: anchorCol,
                right: anchorCol,
            };
            const zoneWithDelta = organizeZone({
                top: this.getNextAvailableRow(deltaRow, refCol, top),
                left: this.getNextAvailableCol(deltaCol, left, refRow),
                bottom: this.getNextAvailableRow(deltaRow, refCol, bottom),
                right: this.getNextAvailableCol(deltaCol, right, refRow),
            });
            result = expand(union(currentZone, zoneWithDelta));
            const newAnchor = { zone: result, cell: { col: anchorCol, row: anchorRow } };
            return this.processEvent({
                anchor: newAnchor,
                mode: "updateAnchor",
                options: { scrollIntoView: true },
            });
        }
        selectColumn(index, mode) {
            const sheetId = this.getters.getActiveSheetId();
            const bottom = this.getters.getNumberRows(sheetId) - 1;
            let zone = { left: index, right: index, top: 0, bottom };
            const top = this.getters.findFirstVisibleColRowIndex(sheetId, "ROW");
            let col, row;
            switch (mode) {
                case "overrideSelection":
                case "newAnchor":
                    col = index;
                    row = top;
                    break;
                case "updateAnchor":
                    ({ col, row } = this.anchor.cell);
                    zone = union(zone, { left: col, right: col, top, bottom });
                    break;
            }
            return this.processEvent({
                options: {
                    scrollIntoView: false,
                    unbounded: true,
                },
                anchor: { zone, cell: { col, row } },
                mode,
            });
        }
        selectRow(index, mode) {
            const sheetId = this.getters.getActiveSheetId();
            const right = this.getters.getNumberCols(sheetId) - 1;
            let zone = { top: index, bottom: index, left: 0, right };
            const left = this.getters.findFirstVisibleColRowIndex(sheetId, "COL");
            let col, row;
            switch (mode) {
                case "overrideSelection":
                case "newAnchor":
                    col = left;
                    row = index;
                    break;
                case "updateAnchor":
                    ({ col, row } = this.anchor.cell);
                    zone = union(zone, { left, right, top: row, bottom: row });
                    break;
            }
            return this.processEvent({
                options: {
                    scrollIntoView: false,
                    unbounded: true,
                },
                anchor: { zone, cell: { col, row } },
                mode,
            });
        }
        /**
         * Loop the current selection while keeping the same anchor. The selection will loop through:
         *  1) the smallest zone that contain the anchor and that have only empty cells bordering it
         *  2) the whole sheet
         *  3) the anchor cell
         */
        loopSelection() {
            const sheetId = this.getters.getActiveSheetId();
            const anchor = this.anchor;
            // The whole sheet is selected, select the anchor cell
            if (isEqual(this.anchor.zone, this.getters.getSheetZone(sheetId))) {
                return this.modifyAnchor({ ...anchor, zone: positionToZone(anchor.cell) }, "updateAnchor", {
                    scrollIntoView: false,
                });
            }
            const tableZone = this.expandZoneToTable(anchor.zone);
            return !deepEquals(tableZone, anchor.zone)
                ? this.modifyAnchor({ ...anchor, zone: tableZone }, "updateAnchor", {
                    scrollIntoView: false,
                })
                : this.selectAll();
        }
        /**
         * Select a "table" around the current selection.
         * We define a table by the smallest zone that contain the anchor and that have only empty
         * cells bordering it
         */
        selectTableAroundSelection() {
            const tableZone = this.expandZoneToTable(this.anchor.zone);
            return this.modifyAnchor({ ...this.anchor, zone: tableZone }, "updateAnchor", {
                scrollIntoView: false,
            });
        }
        /**
         * Select the entire sheet
         */
        selectAll() {
            const sheetId = this.getters.getActiveSheetId();
            const bottom = this.getters.getNumberRows(sheetId) - 1;
            const right = this.getters.getNumberCols(sheetId) - 1;
            const zone = { left: 0, top: 0, bottom, right };
            return this.processEvent({
                mode: "overrideSelection",
                anchor: { zone, cell: this.anchor.cell },
                options: {
                    scrollIntoView: false,
                },
            });
        }
        isListening(owner) {
            return this.stream.isListening(owner);
        }
        /**
         * Process a new anchor selection event. If the new anchor is inside
         * the sheet boundaries, the event is pushed to the event stream to
         * be processed.
         */
        processEvent(newAnchorEvent) {
            const event = { ...newAnchorEvent, previousAnchor: deepCopy(this.anchor) };
            const commandResult = this.checkEventAnchorZone(event);
            if (commandResult !== "Success" /* CommandResult.Success */) {
                return new DispatchResult(commandResult);
            }
            this.anchor = event.anchor;
            this.stream.send(event);
            return DispatchResult.Success;
        }
        checkEventAnchorZone(event) {
            return this.checkAnchorZone(event.anchor);
        }
        checkAnchorZone(anchor) {
            const { cell, zone } = anchor;
            if (!isInside(cell.col, cell.row, zone)) {
                return "InvalidAnchorZone" /* CommandResult.InvalidAnchorZone */;
            }
            const { left, right, top, bottom } = zone;
            const sheetId = this.getters.getActiveSheetId();
            const refCol = this.getters.findVisibleHeader(sheetId, "COL", left, right);
            const refRow = this.getters.findVisibleHeader(sheetId, "ROW", top, bottom);
            if (refRow === undefined || refCol === undefined) {
                return "SelectionOutOfBound" /* CommandResult.SelectionOutOfBound */;
            }
            return "Success" /* CommandResult.Success */;
        }
        checkAnchorZoneOrThrow(anchor) {
            const result = this.checkAnchorZone(anchor);
            if (result === "InvalidAnchorZone" /* CommandResult.InvalidAnchorZone */) {
                throw new Error(_t("The provided anchor is invalid. The cell must be part of the zone."));
            }
        }
        /**
         *  ---- PRIVATE ----
         */
        /** Computes the next cell position in the direction of deltaX and deltaY
         * by crossing through merges and skipping hidden cells.
         * Note that the resulting position might be out of the sheet, it needs to be validated.
         */
        getNextAvailablePosition(direction, step = 1) {
            const { col, row } = this.anchor.cell;
            const delta = this.deltaToTarget({ col, row }, direction, step);
            return {
                col: this.getNextAvailableCol(delta[0], col, row),
                row: this.getNextAvailableRow(delta[1], col, row),
            };
        }
        getNextAvailableCol(delta, colIndex, rowIndex) {
            const sheetId = this.getters.getActiveSheetId();
            const position = { col: colIndex, row: rowIndex };
            const isInPositionMerge = (nextCol) => this.getters.isInSameMerge(sheetId, colIndex, rowIndex, nextCol, rowIndex);
            return this.getNextAvailableHeader(delta, "COL", colIndex, position, isInPositionMerge);
        }
        getNextAvailableRow(delta, colIndex, rowIndex) {
            const sheetId = this.getters.getActiveSheetId();
            const position = { col: colIndex, row: rowIndex };
            const isInPositionMerge = (nextRow) => this.getters.isInSameMerge(sheetId, colIndex, rowIndex, colIndex, nextRow);
            return this.getNextAvailableHeader(delta, "ROW", rowIndex, position, isInPositionMerge);
        }
        getNextAvailableHeader(delta, dimension, startingHeaderIndex, position, isInPositionMerge) {
            const sheetId = this.getters.getActiveSheetId();
            if (delta === 0) {
                return startingHeaderIndex;
            }
            const step = Math.sign(delta);
            let header = startingHeaderIndex + delta;
            while (isInPositionMerge(header)) {
                header += step;
            }
            while (this.getters.isHeaderHidden(sheetId, dimension, header)) {
                header += step;
            }
            const outOfBound = header < 0 || header > this.getters.getNumberHeaders(sheetId, dimension) - 1;
            if (outOfBound) {
                if (this.getters.isHeaderHidden(sheetId, dimension, startingHeaderIndex)) {
                    return this.getNextAvailableHeader(-step, dimension, startingHeaderIndex, position, isInPositionMerge);
                }
                else {
                    return startingHeaderIndex;
                }
            }
            return header;
        }
        /**
         * Finds a visible cell in the currently selected zone starting with the anchor.
         * If the anchor is hidden, browses from left to right and top to bottom to
         * find a visible cell.
         */
        getReferencePosition() {
            const sheetId = this.getters.getActiveSheetId();
            const anchor = this.anchor;
            const { left, right, top, bottom } = anchor.zone;
            const { col: anchorCol, row: anchorRow } = anchor.cell;
            return {
                col: this.getters.isColHidden(sheetId, anchorCol)
                    ? this.getters.findVisibleHeader(sheetId, "COL", left, right) || anchorCol
                    : anchorCol,
                row: this.getters.isRowHidden(sheetId, anchorRow)
                    ? this.getters.findVisibleHeader(sheetId, "ROW", top, bottom) || anchorRow
                    : anchorRow,
            };
        }
        deltaToTarget(position, direction, step) {
            switch (direction) {
                case "up":
                    return step !== "end"
                        ? [0, -step]
                        : [0, this.getEndOfCluster(position, "rows", -1) - position.row];
                case "down":
                    return step !== "end"
                        ? [0, step]
                        : [0, this.getEndOfCluster(position, "rows", 1) - position.row];
                case "left":
                    return step !== "end"
                        ? [-step, 0]
                        : [this.getEndOfCluster(position, "cols", -1) - position.col, 0];
                case "right":
                    return step !== "end"
                        ? [step, 0]
                        : [this.getEndOfCluster(position, "cols", 1) - position.col, 0];
            }
        }
        // TODO rename this
        getStartingPosition(direction) {
            let { col, row } = this.getPosition();
            const zone = this.anchor.zone;
            switch (direction) {
                case "down":
                case "up":
                    row = row === zone.top ? zone.bottom : zone.top;
                    break;
                case "left":
                case "right":
                    col = col === zone.right ? zone.left : zone.right;
                    break;
            }
            return { col, row };
        }
        /**
         * Given a starting position, compute the end of the cluster containing the position in the given
         * direction or the start of the next cluster. We define cluster here as side-by-side cells that
         * all have a content.
         *
         * We will return the end of the cluster if the given cell is inside a cluster, and the start of the
         * next cluster if the given cell is outside a cluster or at the border of a cluster in the given direction.
         */
        getEndOfCluster(startPosition, dim, dir) {
            let currentPosition = startPosition;
            // If both the current cell and the next cell are not empty, we want to go to the end of the cluster
            const nextCellPosition = this.getNextCellPosition(startPosition, dim, dir);
            let mode = !this.isEvaluatedCellEmpty(currentPosition) && !this.isEvaluatedCellEmpty(nextCellPosition)
                ? "endOfCluster"
                : "nextCluster";
            while (true) {
                const nextCellPosition = this.getNextCellPosition(currentPosition, dim, dir);
                // Break if nextPosition === currentPosition, which happens if there's no next valid position
                if (currentPosition.col === nextCellPosition.col &&
                    currentPosition.row === nextCellPosition.row) {
                    break;
                }
                const isNextCellEmpty = this.isEvaluatedCellEmpty(nextCellPosition);
                if (mode === "endOfCluster" && isNextCellEmpty) {
                    break;
                }
                else if (mode === "nextCluster" && !isNextCellEmpty) {
                    // We want to return the start of the next cluster, not the end of the empty zone
                    currentPosition = nextCellPosition;
                    break;
                }
                currentPosition = nextCellPosition;
            }
            return dim === "cols" ? currentPosition.col : currentPosition.row;
        }
        /**
         * Checks if a cell is empty (i.e. does not have a content). If the cell is part of a merge,
         * the check applies to the main cell of the merge.
         */
        isCellEmpty({ col, row }) {
            const sheetId = this.getters.getActiveSheetId();
            const position = this.getters.getMainCellPosition({ sheetId, col, row });
            return !(this.getters.getCorrespondingFormulaCell(position) || this.getters.getCell(position)?.content);
        }
        /**
         * Checks if a cell evaluated value is empty. If the cell is part of a merge,
         * the check applies to the main cell of the merge.
         */
        isEvaluatedCellEmpty({ col, row }) {
            const sheetId = this.getters.getActiveSheetId();
            const position = this.getters.getMainCellPosition({ sheetId, col, row });
            const cell = this.getters.getEvaluatedCell(position);
            return cell.type === CellValueType.empty;
        }
        /** Computes the next cell position in the given direction by crossing through merges and skipping hidden cells.
         *
         * This has the same behaviour as getNextAvailablePosition() for certain arguments, but use this method instead
         * inside directionToDelta(), which is called in getNextAvailablePosition(), to avoid possible infinite
         * recursion.
         */
        getNextCellPosition(currentPosition, dimension, direction) {
            const dimOfInterest = dimension === "cols" ? "col" : "row";
            const startingPosition = { ...currentPosition };
            const nextCoord = dimension === "cols"
                ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)
                : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);
            startingPosition[dimOfInterest] = nextCoord;
            return { col: startingPosition.col, row: startingPosition.row };
        }
        getPosition() {
            return { ...this.anchor.cell };
        }
        /**
         * Expand the given zone to a table.
         * We define a table by the smallest zone that contain the anchor and that have only empty
         * cells bordering it
         */
        expandZoneToTable(zoneToExpand) {
            /** Try to expand the zone by one col/row in any direction to include a new non-empty cell */
            const expandZone = (zone) => {
                for (const col of range(zone.left, zone.right + 1)) {
                    if (!this.isCellEmpty({ col, row: zone.top - 1 })) {
                        return { ...zone, top: zone.top - 1 };
                    }
                    if (!this.isCellEmpty({ col, row: zone.bottom + 1 })) {
                        return { ...zone, bottom: zone.bottom + 1 };
                    }
                }
                for (const row of range(zone.top, zone.bottom + 1)) {
                    if (!this.isCellEmpty({ col: zone.left - 1, row })) {
                        return { ...zone, left: zone.left - 1 };
                    }
                    if (!this.isCellEmpty({ col: zone.right + 1, row })) {
                        return { ...zone, right: zone.right + 1 };
                    }
                }
                return zone;
            };
            let hasExpanded = false;
            let zone = zoneToExpand;
            do {
                hasExpanded = false;
                const newZone = expandZone(zone);
                if (!isEqual(zone, newZone)) {
                    hasExpanded = true;
                    zone = newZone;
                    continue;
                }
            } while (hasExpanded);
            return zone;
        }
    }

    class StateObserver {
        changes = [];
        commands = [];
        /**
         * Record the changes which could happen in the given callback, save them in a
         * new revision with the given id and userId.
         */
        recordChanges(callback) {
            this.changes = [];
            this.commands = [];
            callback();
            return { changes: this.changes, commands: this.commands };
        }
        addCommand(command) {
            this.commands.push(command);
        }
        addChange(...args) {
            const val = args.pop();
            const root = args[0];
            let value = root;
            let key = args.at(-1);
            const pathLength = args.length - 2;
            for (let pathIndex = 1; pathIndex <= pathLength; pathIndex++) {
                const p = args[pathIndex];
                if (value[p] === undefined) {
                    const nextPath = args[pathIndex + 1];
                    value[p] = createEmptyStructure(nextPath);
                }
                value = value[p];
            }
            if (value[key] === val) {
                return;
            }
            this.changes.push({
                path: args,
                before: value[key],
                after: val,
            });
            if (val === undefined) {
                delete value[key];
            }
            else {
                value[key] = val;
            }
        }
    }

    /**
     * Each axis present inside a graph needs to be identified by an unsigned integer
     * The value does not matter, it can be hardcoded.
     */
    const catAxId = 17781237;
    const valAxId = 88853993;
    function createChart(chart, chartSheetIndex, data) {
        const namespaces = [
            ["xmlns:r", RELATIONSHIP_NSR],
            ["xmlns:a", DRAWING_NS_A],
            ["xmlns:c", DRAWING_NS_C],
        ];
        const chartShapeProperty = shapeProperty({
            backgroundColor: chart.data.backgroundColor,
            line: { color: "000000" },
        });
        // <manualLayout/> to manually position the chart in the figure container
        let title = escapeXml ``;
        if (chart.data.title) {
            title = escapeXml /*xml*/ `
      <c:title>
        ${insertText(chart.data.title, chart.data.fontColor)}
        <c:overlay val="0" />
      </c:title>
    `;
        }
        // switch on chart type
        let plot = escapeXml ``;
        switch (chart.data.type) {
            case "bar":
                plot = addBarChart(chart.data);
                break;
            case "line":
                plot = addLineChart(chart.data);
                break;
            case "pie":
                plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });
                break;
        }
        let position = "t";
        switch (chart.data.legendPosition) {
            case "bottom":
                position = "b";
                break;
            case "left":
                position = "l";
                break;
            case "right":
                position = "r";
                break;
            case "top":
                position = "t";
                break;
        }
        const fontColor = chart.data.fontColor;
        const xml = escapeXml /*xml*/ `
    <c:chartSpace ${formatAttributes(namespaces)}>
      <c:roundedCorners val="0" />
      <!-- <manualLayout/> to manually position the chart in the figure container -->
      ${chartShapeProperty}
      <c:chart>
        ${title}
        <c:autoTitleDeleted val="0" />
        <c:plotArea>
          <!-- how the chart element is placed on the chart -->
          <c:layout />
          ${plot}
          ${shapeProperty({ backgroundColor: chart.data.backgroundColor })}
        </c:plotArea>
        ${addLegend(position, fontColor)}
      </c:chart>
    </c:chartSpace>
  `;
        return parseXML(xml);
    }
    function shapeProperty(params) {
        return escapeXml /*xml*/ `
    <c:spPr>
      ${params.backgroundColor ? solidFill(params.backgroundColor) : ""}
      ${params.line ? lineAttributes(params.line) : ""}
    </c:spPr>
  `;
    }
    function solidFill(color) {
        return escapeXml /*xml*/ `
    <a:solidFill>
      <a:srgbClr val="${color}"/>
    </a:solidFill>
  `;
    }
    function lineAttributes(params) {
        const attrs = [["cmpd", "sng"]];
        if (params.width) {
            attrs.push(["w", convertDotValueToEMU(params.width)]);
        }
        const lineStyle = params.style ? escapeXml /*xml*/ `<a:prstDash val="${params.style}"/>` : "";
        return escapeXml /*xml*/ `
    <a:ln ${formatAttributes(attrs)}>
      ${solidFill(params.color)}
      ${lineStyle}
    </a:ln>
  `;
    }
    function insertText(text, fontColor = "000000", fontsize = 22) {
        return escapeXml /*xml*/ `
    <c:tx>
      <c:rich>
        <a:bodyPr />
        <a:lstStyle />
        <a:p>
          <a:pPr lvl="0">
            <a:defRPr b="0">
              ${solidFill(fontColor)}
              <a:latin typeface="+mn-lt"/>
            </a:defRPr>
          </a:pPr>
          <a:r> <!-- Runs -->
            <a:rPr sz="${fontsize * 100}"/>
            <a:t>${text}</a:t>
          </a:r>
        </a:p>
      </c:rich>
    </c:tx>
  `;
    }
    function insertTextProperties(fontsize = 12, fontColor = "000000", bold = false, italic = false) {
        const defPropertiesAttributes = [
            ["b", bold ? "1" : "0"],
            ["i", italic ? "1" : "0"],
            ["sz", fontsize * 100],
        ];
        return escapeXml /*xml*/ `
    <c:txPr>
      <a:bodyPr/>
      <a:lstStyle/>
      <a:p>
        <a:pPr lvl="0">
          <a:defRPr ${formatAttributes(defPropertiesAttributes)}>
            ${solidFill(fontColor)}
            <a:latin typeface="+mn-lt"/>
          </a:defRPr>
        </a:pPr>
      </a:p>
    </c:txPr>
  `;
    }
    function addBarChart(chart) {
        // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)
        // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB
        // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB
        //
        // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.
        // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage
        const colors = new ChartColors();
        const dataSetsNodes = [];
        for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
            const color = toXlsxHexColor(colors.next());
            const dataShapeProperty = shapeProperty({
                backgroundColor: color,
                line: { color },
            });
            dataSetsNodes.push(escapeXml /*xml*/ `
      <c:ser>
        <c:idx val="${dsIndex}"/>
        <c:order val="${dsIndex}"/>
        ${dataset.label ? escapeXml /*xml*/ `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
        ${dataShapeProperty}
        ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
        <c:val> <!-- x-coordinate values -->
          ${numberRef(dataset.range)}
        </c:val>
      </c:ser>
    `);
        }
        // Excel does not support this feature
        const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
        const grouping = chart.stacked ? "stacked" : "clustered";
        const overlap = chart.stacked ? 100 : -20;
        return escapeXml /*xml*/ `
    <c:barChart>
      <c:barDir val="col"/>
      <c:grouping val="${grouping}"/>
      <c:overlap val="${overlap}"/>
      <c:gapWidth val="70"/>
      <!-- each data marker in the series does not have a different color -->
      <c:varyColors val="0"/>
      ${joinXmlNodes(dataSetsNodes)}
      <c:axId val="${catAxId}" />
      <c:axId val="${valAxId}" />
    </c:barChart>
    ${addAx("b", "c:catAx", catAxId, valAxId, { fontColor: chart.fontColor })}
    ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
  `;
    }
    function addLineChart(chart) {
        const colors = new ChartColors();
        const dataSetsNodes = [];
        for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
            const dataShapeProperty = shapeProperty({
                line: {
                    width: 2.5,
                    style: "solid",
                    color: toXlsxHexColor(colors.next()),
                },
            });
            dataSetsNodes.push(escapeXml /*xml*/ `
      <c:ser>
        <c:idx val="${dsIndex}"/>
        <c:order val="${dsIndex}"/>
        <c:smooth val="0"/>
        <c:marker>
          <c:symbol val="circle" />
          <c:size val="5"/>
        </c:marker>
        ${dataset.label ? escapeXml `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
        ${dataShapeProperty}
        ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
        <c:val> <!-- x-coordinate values -->
          ${numberRef(dataset.range)}
        </c:val>
      </c:ser>
    `);
        }
        // Excel does not support this feature
        const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
        const grouping = chart.stacked ? "stacked" : "standard";
        return escapeXml /*xml*/ `
    <c:lineChart>
      <c:grouping val="${grouping}"/>
      <!-- each data marker in the series does not have a different color -->
      <c:varyColors val="0"/>
      ${joinXmlNodes(dataSetsNodes)}
      <c:axId val="${catAxId}" />
      <c:axId val="${valAxId}" />
    </c:lineChart>
    ${addAx("b", "c:catAx", catAxId, valAxId, { fontColor: chart.fontColor })}
    ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
  `;
    }
    function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
        const colors = new ChartColors();
        const maxLength = Math.max(...chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
        const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
        const dataSetsNodes = [];
        for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
            //dataset slice labels
            const dsSize = getRangeSize(dataset.range, chartSheetIndex, data);
            const dataPoints = [];
            for (const index of range(0, dsSize)) {
                const pointShapeProperty = shapeProperty({
                    backgroundColor: doughnutColors[index],
                    line: { color: "FFFFFF", width: 1.5 },
                });
                dataPoints.push(escapeXml /*xml*/ `
        <c:dPt>
          <c:idx val="${index}"/>
          ${pointShapeProperty}
        </c:dPt>
      `);
            }
            dataSetsNodes.push(escapeXml /*xml*/ `
      <c:ser>
        <c:idx val="${dsIndex}"/>
        <c:order val="${dsIndex}"/>
        ${dataset.label ? escapeXml `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
        ${joinXmlNodes(dataPoints)}
        ${insertDataLabels({ showLeaderLines: true })}
        ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""}
        <c:val>
          ${numberRef(dataset.range)}
        </c:val>
      </c:ser>
    `);
        }
        return escapeXml /*xml*/ `
    <c:doughnutChart>
      <c:varyColors val="1" />
      <c:holeSize val="${holeSize}" />
      ${insertDataLabels()}
      ${joinXmlNodes(dataSetsNodes)}
    </c:doughnutChart>
  `;
    }
    function insertDataLabels({ showLeaderLines } = { showLeaderLines: false }) {
        return escapeXml /*xml*/ `
    <dLbls>
      <c:showLegendKey val="0"/>
      <c:showVal val="0"/>
      <c:showCatName val="0"/>
      <c:showSerName val="0"/>
      <c:showPercent val="0"/>
      <c:showBubbleSize val="0"/>
      <c:showLeaderLines val="${showLeaderLines ? "1" : "0"}"/>
    </dLbls>
  `;
    }
    function addAx(position, axisName, axId, crossAxId, { fontColor }) {
        // Each Axis present inside a graph needs to be identified by an unsigned integer in order to be referenced by its crossAxis.
        // I.e. x-axis, will reference y-axis and vice-versa.
        return escapeXml /*xml*/ `
    <${axisName}>
      <c:axId val="${axId}"/>
      <c:crossAx val="${crossAxId}"/> <!-- reference to the other axe of the chart -->
      <c:delete val="0"/> <!-- by default, axis are not displayed -->
      <c:scaling>
        <c:orientation  val="minMax" />
      </c:scaling>
      <c:axPos val="${position}" />
      ${insertMajorGridLines()}
      <c:majorTickMark val="out" />
      <c:minorTickMark val="none" />
      <c:numFmt formatCode="General" sourceLinked="1" />
      <c:title>
        ${insertText("")}
      </c:title>
      ${insertTextProperties(10, fontColor)}
    </${axisName}>
    <!-- <tickLblPos/> omitted -->
  `;
    }
    function addLegend(position, fontColor) {
        return escapeXml /*xml*/ `
    <c:legend>
      <c:legendPos val="${position}"/>
      <c:overlay val="0"/>
      ${insertTextProperties(10, fontColor)}
    </c:legend>
  `;
    }
    function insertMajorGridLines(color = "B7B7B7") {
        return escapeXml /*xml*/ `
    <c:majorGridlines>
      ${shapeProperty({ line: { color } })}
    </c:majorGridlines>
  `;
    }
    function stringRef(reference) {
        return escapeXml /*xml*/ `
    <c:strRef>
      <c:f>${reference}</c:f>
    </c:strRef>
  `;
    }
    function numberRef(reference) {
        return escapeXml /*xml*/ `
    <c:numRef>
      <c:f>${reference}</c:f>
      <c:numCache />
    </c:numRef>
  `;
    }

    function addFormula(cell) {
        const formula = cell.content;
        if (!formula) {
            return { attrs: [], node: escapeXml `` };
        }
        const attrs = [];
        let node = escapeXml ``;
        let cycle = escapeXml ``;
        const XlsxFormula = adaptFormulaToExcel(formula);
        // hack for cycles : if we don't set a value (be it 0 or #VALUE!), it will appear as invisible on excel,
        // Making it very hard for the client to find where the recursion is.
        if (cell.value === CellErrorType.CircularDependency) {
            attrs.push(["t", "str"]);
            cycle = escapeXml /*xml*/ `<v>${cell.value}</v>`;
        }
        node = escapeXml /*xml*/ `<f>${XlsxFormula}</f>${cycle}`;
        return { attrs, node };
    }
    function addContent(content, sharedStrings, forceString = false) {
        let value = content;
        const attrs = [];
        const clearValue = value.trim().toUpperCase();
        if (!forceString && ["TRUE", "FALSE"].includes(clearValue)) {
            value = clearValue === "TRUE" ? "1" : "0";
            attrs.push(["t", "b"]);
        }
        else if (forceString || !isNumber(value, DEFAULT_LOCALE)) {
            const { id } = pushElement(content, sharedStrings);
            value = id.toString();
            attrs.push(["t", "s"]);
        }
        return { attrs, node: escapeXml /*xml*/ `<v>${value}</v>` };
    }
    function adaptFormulaToExcel(formulaText) {
        if (formulaText[0] === "=") {
            formulaText = formulaText.slice(1);
        }
        let ast;
        try {
            ast = parse(formulaText);
        }
        catch (error) {
            return formulaText;
        }
        ast = convertAstNodes(ast, "STRING", convertDateFormat);
        ast = convertAstNodes(ast, "FUNCALL", (ast) => {
            ast = { ...ast, value: ast.value.toUpperCase() };
            ast = prependNonRetrocompatibleFunction(ast);
            ast = addMissingRequiredArgs(ast);
            return ast;
        });
        return ast ? astToFormula(ast) : formulaText;
    }
    /**
     * Some Excel function need required args that might not be mandatory in o-spreadsheet.
     * This adds those missing args.
     */
    function addMissingRequiredArgs(ast) {
        const formulaName = ast.value.toUpperCase();
        const args = ast.args;
        const exportDefaultArgs = FORCE_DEFAULT_ARGS_FUNCTIONS[formulaName];
        if (exportDefaultArgs) {
            const requiredArgs = functionRegistry.content[formulaName].args.filter((el) => !el.optional);
            const diffArgs = requiredArgs.length - ast.args.length;
            if (diffArgs) {
                // We know that we have at least 1 default Value missing
                for (let i = ast.args.length; i < requiredArgs.length; i++) {
                    const currentDefaultArg = exportDefaultArgs[i - diffArgs];
                    args.push({ type: currentDefaultArg.type, value: currentDefaultArg.value });
                }
            }
        }
        return { ...ast, args };
    }
    /**
     * Prepend function names that are not compatible with Old Excel versions
     */
    function prependNonRetrocompatibleFunction(ast) {
        const formulaName = ast.value.toUpperCase();
        return {
            ...ast,
            value: NON_RETROCOMPATIBLE_FUNCTIONS.includes(formulaName)
                ? `_xlfn.${formulaName}`
                : formulaName,
        };
    }
    /**
     * Convert strings that correspond to a date to the format YYYY-DD-MM
     */
    function convertDateFormat(ast) {
        const value = ast.value.replace(new RegExp('"', "g"), "");
        const internalDate = parseDateTime(value, DEFAULT_LOCALE);
        if (internalDate) {
            let format = [];
            if (mdyDateRegexp.test(value) || ymdDateRegexp.test(value)) {
                format.push("yyyy-mm-dd");
            }
            if (timeRegexp.test(value)) {
                format.push("hh:mm:ss");
            }
            return {
                ...ast,
                value: formatValue(internalDate.value, { format: format.join(" "), locale: DEFAULT_LOCALE }),
            };
        }
        else {
            return { ...ast, value: ast.value.replace(/\\"/g, `""`) };
        }
    }

    function addConditionalFormatting(dxfs, conditionalFormats) {
        // Conditional Formats
        const cfNodes = [];
        for (const cf of conditionalFormats) {
            // Special case for each type of rule: might be better to extract that logic in dedicated functions
            switch (cf.rule.type) {
                case "CellIsRule":
                    cfNodes.push(addCellIsRule(cf, cf.rule, dxfs));
                    break;
                case "ColorScaleRule":
                    cfNodes.push(addColorScaleRule(cf, cf.rule));
                    break;
                case "IconSetRule":
                    cfNodes.push(addIconSetRule(cf, cf.rule));
                    break;
                default:
                    // @ts-ignore Typescript knows it will never happen at compile time
                    console.warn(`Conditional formatting ${cf.rule.type} not implemented`);
                    break;
            }
        }
        return cfNodes;
    }
    // ----------------------
    //         RULES
    // ----------------------
    function addCellIsRule(cf, rule, dxfs) {
        const ruleAttributes = commonCfAttributes(cf);
        const operator = convertOperator(rule.operator);
        ruleAttributes.push(...cellRuleTypeAttributes(rule), ["operator", operator]);
        const formulas = cellRuleFormula(cf.ranges, rule).map((formula) => escapeXml /*xml*/ `<formula>${formula}</formula>`);
        const dxf = {
            font: {
                color: { rgb: rule.style.textColor },
                bold: rule.style.bold,
                italic: rule.style.italic,
                strike: rule.style.strikethrough,
                underline: rule.style.underline,
            },
        };
        if (rule.style.fillColor) {
            dxf.fill = { fgColor: { rgb: rule.style.fillColor } };
        }
        const { id } = pushElement(dxf, dxfs);
        ruleAttributes.push(["dxfId", id]);
        return escapeXml /*xml*/ `
    <conditionalFormatting sqref="${cf.ranges.join(" ")}">
      <cfRule ${formatAttributes(ruleAttributes)}>
        ${joinXmlNodes(formulas)}
      </cfRule>
    </conditionalFormatting>
  `;
    }
    function cellRuleFormula(ranges, rule) {
        const firstCell = ranges[0].split(":")[0];
        const values = rule.values;
        switch (rule.operator) {
            case "ContainsText":
                return [`NOT(ISERROR(SEARCH("${values[0]}",${firstCell})))`];
            case "NotContains":
                return [`ISERROR(SEARCH("${values[0]}",${firstCell}))`];
            case "BeginsWith":
                return [`LEFT(${firstCell},LEN("${values[0]}"))="${values[0]}"`];
            case "EndsWith":
                return [`RIGHT(${firstCell},LEN("${values[0]}"))="${values[0]}"`];
            case "IsEmpty":
                return [`LEN(TRIM(${firstCell}))=0`];
            case "IsNotEmpty":
                return [`LEN(TRIM(${firstCell}))>0`];
            case "Equal":
            case "NotEqual":
            case "GreaterThan":
            case "GreaterThanOrEqual":
            case "LessThan":
            case "LessThanOrEqual":
                return [values[0]];
            case "Between":
            case "NotBetween":
                return [values[0], values[1]];
        }
    }
    function cellRuleTypeAttributes(rule) {
        const operator = convertOperator(rule.operator);
        switch (rule.operator) {
            case "ContainsText":
            case "NotContains":
            case "BeginsWith":
            case "EndsWith":
                return [
                    ["type", operator],
                    ["text", rule.values[0]],
                ];
            case "IsEmpty":
            case "IsNotEmpty":
                return [["type", operator]];
            case "Equal":
            case "NotEqual":
            case "GreaterThan":
            case "GreaterThanOrEqual":
            case "LessThan":
            case "LessThanOrEqual":
            case "Between":
            case "NotBetween":
                return [["type", "cellIs"]];
        }
    }
    function addColorScaleRule(cf, rule) {
        const ruleAttributes = commonCfAttributes(cf);
        ruleAttributes.push(["type", "colorScale"]);
        /** mimic our flow:
         * for a given ColorScale CF, each range of the "ranges set" has its own behaviour.
         */
        const conditionalFormats = [];
        for (const range of cf.ranges) {
            const cfValueObject = [];
            const colors = [];
            let canExport = true;
            for (let position of ["minimum", "midpoint", "maximum"]) {
                const threshold = rule[position];
                if (!threshold) {
                    // pass midpoint if not defined
                    continue;
                }
                if (threshold.type === "formula") {
                    canExport = false;
                    continue;
                }
                cfValueObject.push(thresholdAttributes(threshold, position));
                colors.push([["rgb", toXlsxHexColor(colorNumberString(threshold.color))]]);
            }
            if (!canExport) {
                console.warn("Conditional formats with formula rules are not supported at the moment. The rule is therefore skipped.");
                continue;
            }
            const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ `<cfvo ${formatAttributes(attrs)}/>`);
            const cfColorNodes = colors.map((attrs) => escapeXml /*xml*/ `<color ${formatAttributes(attrs)}/>`);
            conditionalFormats.push(escapeXml /*xml*/ `
      <conditionalFormatting sqref="${range}">
        <cfRule ${formatAttributes(ruleAttributes)}>
          <colorScale>
            ${joinXmlNodes(cfValueObjectNodes)}
            ${joinXmlNodes(cfColorNodes)}
          </colorScale>
        </cfRule>
      </conditionalFormatting>
    `);
        }
        return joinXmlNodes(conditionalFormats);
    }
    function addIconSetRule(cf, rule) {
        const ruleAttributes = commonCfAttributes(cf);
        ruleAttributes.push(["type", "iconSet"]);
        /** mimic our flow:
         * for a given IconSet CF, each range of the "ranges set" has its own behaviour.
         */
        const conditionalFormats = [];
        for (const range of cf.ranges) {
            const cfValueObject = [
                // It looks like they always want 3 cfvo and they add a dummy entry
                [
                    ["type", "percent"],
                    ["val", 0],
                ],
            ];
            let canExport = true;
            for (let position of ["lowerInflectionPoint", "upperInflectionPoint"]) {
                if (rule[position].type === "formula") {
                    canExport = false;
                    continue;
                }
                const threshold = rule[position];
                cfValueObject.push([
                    ...thresholdAttributes(threshold, position),
                    ["gte", threshold.operator === "ge" ? "1" : "0"],
                ]);
            }
            if (!canExport) {
                console.warn("Conditional formats with formula rules are not supported at the moment. The rule is therefore skipped.");
                continue;
            }
            const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ `<cfvo ${formatAttributes(attrs)} />`);
            conditionalFormats.push(escapeXml /*xml*/ `
      <conditionalFormatting sqref="${range}">
        <cfRule ${formatAttributes(ruleAttributes)}>
          <iconSet iconSet="${getIconSet(rule.icons)}">
            ${joinXmlNodes(cfValueObjectNodes)}
          </iconSet>
        </cfRule>
      </conditionalFormatting>
    `);
        }
        return joinXmlNodes(conditionalFormats);
    }
    // ----------------------
    //         MISC
    // ----------------------
    function commonCfAttributes(cf) {
        return [
            ["priority", 1],
            ["stopIfTrue", cf.stopIfTrue ? 1 : 0],
        ];
    }
    function getIconSet(iconSet) {
        return XLSX_ICONSET_MAP[Object.keys(XLSX_ICONSET_MAP).find((key) => iconSet.upper.toLowerCase().startsWith(key)) ||
            "dots"];
    }
    function thresholdAttributes(threshold, position) {
        const type = getExcelThresholdType(threshold.type, position);
        const attrs = [["type", type]];
        if (type !== "min" && type !== "max") {
            // what if the formula is not correct
            // references cannot be relative :/
            let val = threshold.value;
            if (type === "formula") {
                try {
                    // Relative references are not supported in formula
                    val = adaptFormulaToExcel(threshold.value);
                }
                catch (error) {
                    val = threshold.value;
                }
            }
            attrs.push(["val", val]); // value is undefined only for type="value")
        }
        return attrs;
    }
    /**
     * This function adapts our Threshold types to their Excel equivalents.
     *
     * if type === "value" ,then we must replace it by min or max according to the position
     * if type === "number", then it becomes num
     * if type === "percentage", it becomes "percent"
     * rest of the time, the type is unchanged
     */
    function getExcelThresholdType(type, position) {
        switch (type) {
            case "value":
                return position === "minimum" ? "min" : "max";
            case "number":
                return "num";
            case "percentage":
                return "percent";
            default:
                return type;
        }
    }

    function createDrawing(drawingRelIds, sheet, figures) {
        const namespaces = [
            ["xmlns:xdr", NAMESPACE.drawing],
            ["xmlns:r", RELATIONSHIP_NSR],
            ["xmlns:a", DRAWING_NS_A],
            ["xmlns:c", DRAWING_NS_C],
        ];
        const figuresNodes = [];
        for (const [figureIndex, figure] of Object.entries(figures)) {
            switch (figure?.tag) {
                case "chart":
                    figuresNodes.push(createChartDrawing(figure, sheet, drawingRelIds[figureIndex]));
                    break;
                case "image":
                    figuresNodes.push(createImageDrawing(figure, sheet, drawingRelIds[figureIndex]));
                    break;
            }
        }
        const xml = escapeXml /*xml*/ `
    <xdr:wsDr ${formatAttributes(namespaces)}>
      ${joinXmlNodes(figuresNodes)}
    </xdr:wsDr>
  `;
        return parseXML(xml);
    }
    /**
     *  Returns the coordinates of topLeft (from) and BottomRight (to) of the chart in English Metric Units (EMU)
     */
    function convertFigureData(figure, sheet) {
        const { x, y, height, width } = figure;
        const cols = Object.values(sheet.cols);
        const rows = Object.values(sheet.rows);
        const { index: colFrom, offset: offsetColFrom } = figureCoordinates(cols, x);
        const { index: colTo, offset: offsetColTo } = figureCoordinates(cols, x + width);
        const { index: rowFrom, offset: offsetRowFrom } = figureCoordinates(rows, y);
        const { index: rowTo, offset: offsetRowTo } = figureCoordinates(rows, y + height);
        return {
            from: {
                col: colFrom,
                colOff: offsetColFrom,
                row: rowFrom,
                rowOff: offsetRowFrom,
            },
            to: {
                col: colTo,
                colOff: offsetColTo,
                row: rowTo,
                rowOff: offsetRowTo,
            },
        };
    }
    /** Returns figure coordinates in EMU for a specific header dimension
     *  See https://docs.microsoft.com/en-us/windows/win32/vml/msdn-online-vml-units#other-units-of-measurement
     */
    function figureCoordinates(headers, position) {
        let currentPosition = 0;
        for (const [headerIndex, header] of headers.entries()) {
            if (currentPosition <= position && position < currentPosition + header.size) {
                return {
                    index: headerIndex,
                    offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH),
                };
            }
            else if (headerIndex < headers.length - 1) {
                currentPosition += header.size;
            }
        }
        return {
            index: headers.length - 1,
            offset: convertDotValueToEMU(position - currentPosition + FIGURE_BORDER_WIDTH),
        };
    }
    function createChartDrawing(figure, sheet, chartRelId) {
        // position
        const { from, to } = convertFigureData(figure, sheet);
        const chartId = convertChartId(figure.id);
        const cNvPrAttrs = [
            ["id", chartId],
            ["name", `Chart ${chartId}`],
            ["title", "Chart"],
        ];
        return escapeXml /*xml*/ `
    <xdr:twoCellAnchor>
      <xdr:from>
        <xdr:col>${from.col}</xdr:col>
        <xdr:colOff>${from.colOff}</xdr:colOff>
        <xdr:row>${from.row}</xdr:row>
        <xdr:rowOff>${from.rowOff}</xdr:rowOff>
      </xdr:from>
      <xdr:to>
        <xdr:col>${to.col}</xdr:col>
        <xdr:colOff>${to.colOff}</xdr:colOff>
        <xdr:row>${to.row}</xdr:row>
        <xdr:rowOff>${to.rowOff}</xdr:rowOff>
      </xdr:to>
      <xdr:graphicFrame>
        <xdr:nvGraphicFramePr>
          <xdr:cNvPr ${formatAttributes(cNvPrAttrs)} />
          <xdr:cNvGraphicFramePr />
        </xdr:nvGraphicFramePr>
        <xdr:xfrm>
          <a:off x="0" y="0"/>
          <a:ext cx="0" cy="0"/>
        </xdr:xfrm>
        <a:graphic>
          <a:graphicData uri="${DRAWING_NS_C}">
            <c:chart r:id="${chartRelId}" />
          </a:graphicData>
        </a:graphic>
      </xdr:graphicFrame>
      <xdr:clientData fLocksWithSheet="0"/>
    </xdr:twoCellAnchor>
  `;
    }
    function createImageDrawing(figure, sheet, imageRelId) {
        // position
        const { from, to } = convertFigureData(figure, sheet);
        const imageId = convertImageId(figure.id);
        const cNvPrAttrs = [
            ["id", imageId],
            ["name", `Image ${imageId}`],
            ["title", "Image"],
        ];
        const cx = convertDotValueToEMU(figure.width);
        const cy = convertDotValueToEMU(figure.height);
        return escapeXml /*xml*/ `
    <xdr:twoCellAnchor editAs="oneCell">
      <xdr:from>
        <xdr:col>${from.col}</xdr:col>
        <xdr:colOff>${from.colOff}</xdr:colOff>
        <xdr:row>${from.row}</xdr:row>
        <xdr:rowOff>${from.rowOff}</xdr:rowOff>
      </xdr:from>
      <xdr:to>
        <xdr:col>${to.col}</xdr:col>
        <xdr:colOff>${to.colOff}</xdr:colOff>
        <xdr:row>${to.row}</xdr:row>
        <xdr:rowOff>${to.rowOff}</xdr:rowOff>
      </xdr:to>
      <xdr:pic>
        <xdr:nvPicPr>
          <xdr:cNvPr ${formatAttributes(cNvPrAttrs)}/>
          <xdr:cNvPicPr preferRelativeResize="0"/>
        </xdr:nvPicPr>
        <xdr:blipFill>
          <a:blip cstate="print" r:embed="${imageRelId}"/>
          <a:stretch>
            <a:fillRect/>
          </a:stretch>
        </xdr:blipFill>
        <xdr:spPr>
          <a:xfrm>
            <a:ext cx="${cx}" cy="${cy}" />
          </a:xfrm>
          <a:prstGeom prst="rect">
            <a:avLst/>
          </a:prstGeom>
          <a:noFill/>
        </xdr:spPr>
      </xdr:pic>
      <xdr:clientData fLocksWithSheet="0"/>
    </xdr:twoCellAnchor>
  `;
    }

    function addNumberFormats(numFmts) {
        const numFmtNodes = [];
        for (let [index, numFmt] of Object.entries(numFmts)) {
            const numFmtAttrs = [
                ["numFmtId", parseInt(index) + FIRST_NUMFMT_ID],
                ["formatCode", numFmt.format],
            ];
            numFmtNodes.push(escapeXml /*xml*/ `
      <numFmt ${formatAttributes(numFmtAttrs)}/>
    `);
        }
        return escapeXml /*xml*/ `
    <numFmts count="${numFmts.length}">
      ${joinXmlNodes(numFmtNodes)}
    </numFmts>
  `;
    }
    function addFont(font) {
        if (isObjectEmptyRecursive(font)) {
            return escapeXml /*xml*/ ``;
        }
        return escapeXml /*xml*/ `
    <font>
      ${font.bold ? escapeXml /*xml*/ `<b />` : ""}
      ${font.italic ? escapeXml /*xml*/ `<i />` : ""}
      ${font.underline ? escapeXml /*xml*/ `<u />` : ""}
      ${font.strike ? escapeXml /*xml*/ `<strike />` : ""}
      ${font.size ? escapeXml /*xml*/ `<sz val="${font.size}" />` : ""}
      ${font.color && font.color.rgb
        ? escapeXml /*xml*/ `<color rgb="${toXlsxHexColor(font.color.rgb)}" />`
        : ""}
      ${font.name ? escapeXml /*xml*/ `<name val="${font.name}" />` : ""}
    </font>
  `;
    }
    function addFonts(fonts) {
        return escapeXml /*xml*/ `
    <fonts count="${fonts.length}">
      ${joinXmlNodes(Object.values(fonts).map(addFont))}
    </fonts>
  `;
    }
    function addFills(fills) {
        const fillNodes = [];
        for (let fill of Object.values(fills)) {
            if (fill.reservedAttribute !== undefined) {
                fillNodes.push(escapeXml /*xml*/ `
        <fill>
          <patternFill patternType="${fill.reservedAttribute}" />
        </fill>
      `);
            }
            else {
                fillNodes.push(escapeXml /*xml*/ `
        <fill>
          <patternFill patternType="solid">
            <fgColor rgb="${toXlsxHexColor(fill.fgColor.rgb)}" />
            <bgColor indexed="64" />
          </patternFill>
        </fill>
      `);
            }
        }
        return escapeXml /*xml*/ `
    <fills count="${fills.length}">
    ${joinXmlNodes(fillNodes)}
    </fills>
  `;
    }
    function addBorders(borders) {
        const borderNodes = [];
        for (let border of Object.values(borders)) {
            borderNodes.push(escapeXml /*xml*/ `
      <border>
        <left ${formatBorderAttribute(border["left"])}>
          ${addBorderColor(border["left"])}
        </left>
        <right ${formatBorderAttribute(border["right"])}>
          ${addBorderColor(border["right"])}
        </right>
        <top ${formatBorderAttribute(border["top"])}>
          ${addBorderColor(border["top"])}
        </top>
        <bottom ${formatBorderAttribute(border["bottom"])}>
          ${addBorderColor(border["bottom"])}
        </bottom>
        <diagonal ${formatBorderAttribute(border["diagonal"])}>
          ${addBorderColor(border["diagonal"])}
        </diagonal>
      </border>
    `);
        }
        return escapeXml /*xml*/ `
    <borders count="${borders.length}">
      ${joinXmlNodes(borderNodes)}
    </borders>
  `;
    }
    function formatBorderAttribute(description) {
        if (!description) {
            return escapeXml ``;
        }
        return formatAttributes([["style", description.style]]);
    }
    function addBorderColor(description) {
        if (!description) {
            return escapeXml ``;
        }
        return escapeXml /*xml*/ `
    <color ${formatAttributes([["rgb", toXlsxHexColor(description.color.rgb)]])}/>
  `;
    }
    function addStyles(styles) {
        const styleNodes = [];
        for (let style of styles) {
            const attributes = [
                ["numFmtId", style.numFmtId],
                ["fillId", style.fillId],
                ["fontId", style.fontId],
                ["borderId", style.borderId],
            ];
            // Note: the apply${substyleName} does not seem to be required
            const alignAttrs = [];
            if (style.alignment && style.alignment.vertical) {
                alignAttrs.push(["vertical", style.alignment.vertical]);
            }
            if (style.alignment && style.alignment.horizontal) {
                alignAttrs.push(["horizontal", style.alignment.horizontal]);
            }
            if (style.alignment && style.alignment.wrapText) {
                alignAttrs.push(["wrapText", "1"]);
            }
            if (alignAttrs.length > 0) {
                attributes.push(["applyAlignment", "1"]); // for Libre Office
                styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}>${escapeXml /*xml*/ `<alignment ${formatAttributes(alignAttrs)} />`}</xf> `);
            }
            else {
                styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
            }
        }
        return escapeXml /*xml*/ `
    <cellXfs count="${styles.length}">
      ${joinXmlNodes(styleNodes)}
    </cellXfs>
  `;
    }
    /**
     * DXFS : Differential Formatting Records - Conditional formats
     */
    function addCellWiseConditionalFormatting(dxfs // cell-wise CF
    ) {
        const dxfNodes = [];
        for (const dxf of dxfs) {
            let fontNode = escapeXml ``;
            if (dxf.font) {
                fontNode = addFont(dxf.font);
            }
            let fillNode = escapeXml ``;
            if (dxf.fill) {
                fillNode = escapeXml /*xml*/ `
        <fill>
          <patternFill>
            <bgColor rgb="${toXlsxHexColor(dxf.fill.fgColor.rgb)}" />
          </patternFill>
        </fill>
      `;
            }
            dxfNodes.push(escapeXml /*xml*/ `
      <dxf>
        ${fontNode}
        ${fillNode}
      </dxf>
    `);
        }
        return escapeXml /*xml*/ `
    <dxfs count="${dxfs.length}">
      ${joinXmlNodes(dxfNodes)}
    </dxfs>
  `;
    }

    const TABLE_DEFAULT_ATTRS = [
        ["name", "TableStyleLight8"],
        ["showFirstColumn", "0"],
        ["showLastColumn", "0"],
        ["showRowStripes", "0"],
        ["showColumnStripes", "0"],
    ];
    const TABLE_DEFAULT_STYLE = escapeXml /*xml*/ `<tableStyleInfo ${formatAttributes(TABLE_DEFAULT_ATTRS)}/>`;
    function createTable(table, tableId, sheetData) {
        const tableAttributes = [
            ["id", tableId],
            ["name", `Table${tableId}`],
            ["displayName", `Table${tableId}`],
            ["ref", table.range],
            ["xmlns", NAMESPACE.table],
            ["xmlns:xr", NAMESPACE.revision],
            ["xmlns:xr3", NAMESPACE.revision3],
            ["xmlns:mc", NAMESPACE.markupCompatibility],
        ];
        const xml = escapeXml /*xml*/ `
    <table ${formatAttributes(tableAttributes)}>
      ${addAutoFilter(table)}
      ${addTableColumns(table, sheetData)}
      ${TABLE_DEFAULT_STYLE}
    </table>
    `;
        return parseXML(xml);
    }
    function addAutoFilter(table) {
        const autoFilterAttributes = [["ref", table.range]];
        return escapeXml /*xml*/ `
  <autoFilter ${formatAttributes(autoFilterAttributes)}>
    ${joinXmlNodes(addFilterColumns(table))}
  </autoFilter>
  `;
    }
    function addFilterColumns(table) {
        const columns = [];
        for (const filter of table.filters) {
            const colXml = escapeXml /*xml*/ `
      <filterColumn ${formatAttributes([["colId", filter.colId]])}>
        ${addFilter(filter)}
      </filterColumn>
      `;
            columns.push(colXml);
        }
        return columns;
    }
    function addFilter(filter) {
        const filterValues = filter.displayedValues.map((val) => escapeXml /*xml*/ `<filter ${formatAttributes([["val", val]])}/>`);
        const filterAttributes = filter.displayBlanks ? [["blank", 1]] : [];
        return escapeXml /*xml*/ `
  <filters ${formatAttributes(filterAttributes)}>
      ${joinXmlNodes(filterValues)}
  </filters>
`;
    }
    function addTableColumns(table, sheetData) {
        const tableZone = toZone(table.range);
        const columns = [];
        for (const i of range(0, zoneToDimension(tableZone).numberOfCols)) {
            const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
            const colName = sheetData.cells[colHeaderXc]?.content || `col${i}`;
            const colAttributes = [
                ["id", i + 1],
                ["name", colName],
            ];
            columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
        }
        return escapeXml /*xml*/ `
        <tableColumns ${formatAttributes([["count", columns.length]])}>
            ${joinXmlNodes(columns)}
        </tableColumns>
    `;
    }

    function addColumns(cols) {
        if (!Object.values(cols).length) {
            return escapeXml ``;
        }
        const colNodes = [];
        for (let [id, col] of Object.entries(cols)) {
            // Always force our own col width
            const attributes = [
                ["min", parseInt(id) + 1],
                ["max", parseInt(id) + 1],
                ["width", convertWidthToExcel(col.size || DEFAULT_CELL_WIDTH)],
                ["customWidth", 1],
                ["hidden", col.isHidden ? 1 : 0],
            ];
            colNodes.push(escapeXml /*xml*/ `
      <col ${formatAttributes(attributes)}/>
    `);
        }
        return escapeXml /*xml*/ `
    <cols>
      ${joinXmlNodes(colNodes)}
    </cols>
  `;
    }
    function addRows(construct, data, sheet) {
        const rowNodes = [];
        for (let r = 0; r < sheet.rowNumber; r++) {
            const rowAttrs = [["r", r + 1]];
            const row = sheet.rows[r] || {};
            // Always force our own row height
            rowAttrs.push(["ht", convertHeightToExcel(row.size || DEFAULT_CELL_HEIGHT)], ["customHeight", 1], ["hidden", row.isHidden ? 1 : 0]);
            const cellNodes = [];
            for (let c = 0; c < sheet.colNumber; c++) {
                const xc = toXC(c, r);
                const cell = sheet.cells[xc];
                if (cell) {
                    const attributes = [["r", xc]];
                    // style
                    const id = normalizeStyle(construct, extractStyle(cell, data));
                    attributes.push(["s", id]);
                    let additionalAttrs = [];
                    let cellNode = escapeXml ``;
                    // Either formula or static value inside the cell
                    if (cell.isFormula) {
                        ({ attrs: additionalAttrs, node: cellNode } = addFormula(cell));
                    }
                    else if (cell.content && isMarkdownLink(cell.content)) {
                        const { label } = parseMarkdownLink(cell.content);
                        ({ attrs: additionalAttrs, node: cellNode } = addContent(label, construct.sharedStrings));
                    }
                    else if (cell.content && cell.content !== "") {
                        const isTableHeader = isCellTableHeader(c, r, sheet);
                        ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader));
                    }
                    attributes.push(...additionalAttrs);
                    // prettier-ignore
                    cellNodes.push(escapeXml /*xml*/ `<c ${formatAttributes(attributes)}>
  ${cellNode}
</c>`);
                }
            }
            if (cellNodes.length || row.size !== DEFAULT_CELL_HEIGHT || row.isHidden) {
                rowNodes.push(escapeXml /*xml*/ `
        <row ${formatAttributes(rowAttrs)}>
          ${joinXmlNodes(cellNodes)}
        </row>
      `);
            }
        }
        return escapeXml /*xml*/ `
    <sheetData>
      ${joinXmlNodes(rowNodes)}
    </sheetData>
  `;
    }
    function isCellTableHeader(col, row, sheet) {
        return sheet.filterTables.some((table) => {
            const zone = toZone(table.range);
            const headerZone = { ...zone, bottom: zone.top };
            return isInside(col, row, headerZone);
        });
    }
    function addHyperlinks(construct, data, sheetIndex) {
        const sheet = data.sheets[sheetIndex];
        const cells = sheet.cells;
        const linkNodes = [];
        for (const xc in cells) {
            const content = cells[xc]?.content;
            if (content && isMarkdownLink(content)) {
                const { label, url } = parseMarkdownLink(content);
                if (isSheetUrl(url)) {
                    const sheetId = parseSheetUrl(url);
                    const sheet = data.sheets.find((sheet) => sheet.id === sheetId);
                    const location = sheet ? `${sheet.name}!A1` : INCORRECT_RANGE_STRING;
                    const hyperlinkAttributes = [
                        ["display", label],
                        ["location", location],
                        ["ref", xc],
                    ];
                    linkNodes.push(escapeXml /*xml*/ `
          <hyperlink ${formatAttributes(hyperlinkAttributes)}/>
        `);
                }
                else {
                    const linkRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {
                        target: withHttps(url),
                        type: XLSX_RELATION_TYPE.hyperlink,
                        targetMode: "External",
                    });
                    const hyperlinkAttributes = [
                        ["r:id", linkRelId],
                        ["ref", xc],
                    ];
                    linkNodes.push(escapeXml /*xml*/ `
          <hyperlink ${formatAttributes(hyperlinkAttributes)}/>
        `);
                }
            }
        }
        if (!linkNodes.length) {
            return escapeXml ``;
        }
        return escapeXml /*xml*/ `
    <hyperlinks>
      ${joinXmlNodes(linkNodes)}
    </hyperlinks>
  `;
    }
    function addMerges(merges) {
        if (merges.length) {
            const mergeNodes = merges.map((merge) => escapeXml /*xml*/ `<mergeCell ref="${merge}" />`);
            return escapeXml /*xml*/ `
      <mergeCells count="${merges.length}">
        ${joinXmlNodes(mergeNodes)}
      </mergeCells>
    `;
        }
        else
            return escapeXml ``;
    }
    function addSheetViews(sheet) {
        const panes = sheet.panes;
        let splitPanes = escapeXml /*xml*/ ``;
        if (panes && (panes.xSplit || panes.ySplit)) {
            const xc = toXC(panes.xSplit, panes.ySplit);
            //workbookViewId should be defined in the workbook file but it seems like Excel has a default behaviour.
            const xSplit = panes.xSplit ? escapeXml `xSplit="${panes.xSplit}"` : "";
            const ySplit = panes.ySplit ? escapeXml `ySplit="${panes.ySplit}"` : "";
            const topRight = panes.xSplit ? escapeXml `<selection pane="topRight"/>` : "";
            const bottomLeft = panes.ySplit ? escapeXml `<selection pane="bottomLeft"/>` : "";
            const bottomRight = panes.xSplit && panes.ySplit ? escapeXml `<selection pane="bottomRight"/>` : "";
            splitPanes = escapeXml /*xml*/ `
    <pane
      ${xSplit}
      ${ySplit}
      topLeftCell="${xc}"
      activePane="${panes.xSplit ? (panes.ySplit ? "bottomRight" : "topRight") : "bottomLeft"}"
      state="frozen"/>
      ${topRight}
      ${bottomLeft}
      ${bottomRight}
    `;
        }
        const sheetViewAttrs = [
            ["showGridLines", sheet.areGridLinesVisible ? 1 : 0],
            ["workbookViewId", 0],
        ];
        let sheetView = escapeXml /*xml*/ `
      <sheetViews>
        <sheetView ${formatAttributes(sheetViewAttrs)}>
          ${splitPanes}
        </sheetView>
      </sheetViews>
    `;
        return sheetView;
    }

    /**
     * Return the spreadsheet data in the Office Open XML file format.
     * See ECMA-376 standard.
     * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
     */
    function getXLSX(data) {
        const files = [];
        const construct = getDefaultXLSXStructure();
        files.push(createWorkbook(data, construct));
        files.push(...createWorksheets(data, construct));
        files.push(createStylesSheet(construct));
        files.push(createSharedStrings(construct.sharedStrings));
        files.push(...createRelsFiles(construct.relsFiles));
        files.push(createContentTypes(files));
        files.push(createRelRoot());
        return {
            name: `my_spreadsheet.xlsx`,
            files,
        };
    }
    function createWorkbook(data, construct) {
        const namespaces = [
            ["xmlns", NAMESPACE["workbook"]],
            ["xmlns:r", RELATIONSHIP_NSR],
        ];
        const sheetNodes = [];
        for (const [index, sheet] of Object.entries(data.sheets)) {
            const attributes = [
                ["state", sheet.isVisible ? "visible" : "hidden"],
                ["name", sheet.name],
                ["sheetId", parseInt(index) + 1],
                ["r:id", `rId${parseInt(index) + 1}`],
            ];
            sheetNodes.push(escapeXml /*xml*/ `
      <sheet ${formatAttributes(attributes)} />
    `);
            addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
                type: XLSX_RELATION_TYPE.sheet,
                target: `worksheets/sheet${index}.xml`,
            });
        }
        const xml = escapeXml /*xml*/ `
    <workbook ${formatAttributes(namespaces)}>
      <sheets>
        ${joinXmlNodes(sheetNodes)}
      </sheets>
    </workbook>
  `;
        return createXMLFile(parseXML(xml), "xl/workbook.xml", "workbook");
    }
    function createWorksheets(data, construct) {
        const files = [];
        let currentTableIndex = 1;
        for (const [sheetIndex, sheet] of Object.entries(data.sheets)) {
            const namespaces = [
                ["xmlns", NAMESPACE["worksheet"]],
                ["xmlns:r", RELATIONSHIP_NSR],
            ];
            const sheetFormatAttributes = [
                ["defaultRowHeight", convertHeightToExcel(DEFAULT_CELL_HEIGHT)],
                ["defaultColWidth", convertWidthToExcel(DEFAULT_CELL_WIDTH)],
            ];
            const tablesNode = createTablesForSheet(sheet, sheetIndex, currentTableIndex, construct, files);
            currentTableIndex += sheet.filterTables.length;
            // Figures and Charts
            let drawingNode = escapeXml ``;
            const drawingRelIds = [];
            for (const chart of sheet.charts) {
                const xlsxChartId = convertChartId(chart.id);
                const chartRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {
                    target: `../charts/chart${xlsxChartId}.xml`,
                    type: XLSX_RELATION_TYPE.chart,
                });
                drawingRelIds.push(chartRelId);
                files.push(createXMLFile(createChart(chart, sheetIndex, data), `xl/charts/chart${xlsxChartId}.xml`, "chart"));
            }
            for (const image of sheet.images) {
                const mimeType = image.data.mimetype;
                if (mimeType === undefined)
                    continue;
                const extension = IMAGE_MIMETYPE_TO_EXTENSION_MAPPING[mimeType];
                // only support exporting images with mimetypes specified in the mapping
                if (extension === undefined)
                    continue;
                const xlsxImageId = convertImageId(image.id);
                let imageFileName = `image${xlsxImageId}.${extension}`;
                const imageRelId = addRelsToFile(construct.relsFiles, `xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, {
                    target: `../media/${imageFileName}`,
                    type: XLSX_RELATION_TYPE.image,
                });
                drawingRelIds.push(imageRelId);
                files.push({
                    path: `xl/media/${imageFileName}`,
                    imageSrc: image.data.path,
                });
            }
            const drawings = [...sheet.charts, ...sheet.images];
            if (drawings.length) {
                const drawingRelId = addRelsToFile(construct.relsFiles, `xl/worksheets/_rels/sheet${sheetIndex}.xml.rels`, {
                    target: `../drawings/drawing${sheetIndex}.xml`,
                    type: XLSX_RELATION_TYPE.drawing,
                });
                files.push(createXMLFile(createDrawing(drawingRelIds, sheet, drawings), `xl/drawings/drawing${sheetIndex}.xml`, "drawing"));
                drawingNode = escapeXml /*xml*/ `<drawing r:id="${drawingRelId}" />`;
            }
            const sheetXml = escapeXml /*xml*/ `
      <worksheet ${formatAttributes(namespaces)}>
        ${addSheetViews(sheet)}
        <sheetFormatPr ${formatAttributes(sheetFormatAttributes)} />
        ${addColumns(sheet.cols)}
        ${addRows(construct, data, sheet)}
        ${addMerges(sheet.merges)}
        ${joinXmlNodes(addConditionalFormatting(construct.dxfs, sheet.conditionalFormats))}
        ${addHyperlinks(construct, data, sheetIndex)}
        ${drawingNode}
        ${tablesNode}
      </worksheet>
    `;
            files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
        }
        addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
            type: XLSX_RELATION_TYPE.sharedStrings,
            target: "sharedStrings.xml",
        });
        addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
            type: XLSX_RELATION_TYPE.styles,
            target: "styles.xml",
        });
        return files;
    }
    /**
     * Create xlsx files for each tables contained in the given sheet, and add them to the XLSXStructure ans XLSXExportFiles.
     *
     * Return an XML string that should be added in the sheet to link these table to the sheet.
     */
    function createTablesForSheet(sheetData, sheetId, startingTableId, construct, files) {
        let currentTableId = startingTableId;
        if (!sheetData.filterTables.length)
            return new XMLString("");
        const sheetRelFile = `xl/worksheets/_rels/sheet${sheetId}.xml.rels`;
        const tableParts = [];
        for (const table of sheetData.filterTables) {
            const tableRelId = addRelsToFile(construct.relsFiles, sheetRelFile, {
                target: `../tables/table${currentTableId}.xml`,
                type: XLSX_RELATION_TYPE.table,
            });
            files.push(createXMLFile(createTable(table, currentTableId, sheetData), `xl/tables/table${currentTableId}.xml`, "table"));
            tableParts.push(escapeXml /*xml*/ `<tablePart r:id="${tableRelId}" />`);
            currentTableId++;
        }
        return escapeXml /*xml*/ `
    <tableParts count="${sheetData.filterTables.length}">
      ${joinXmlNodes(tableParts)}
    </tableParts>
`;
    }
    function createStylesSheet(construct) {
        const namespaces = [
            ["xmlns", NAMESPACE["styleSheet"]],
            ["xmlns:r", RELATIONSHIP_NSR],
        ];
        const styleXml = escapeXml /*xml*/ `
    <styleSheet ${formatAttributes(namespaces)}>
      ${addNumberFormats(construct.numFmts)}
      ${addFonts(construct.fonts)}
      ${addFills(construct.fills)}
      ${addBorders(construct.borders)}
      ${addStyles(construct.styles)}
      ${addCellWiseConditionalFormatting(construct.dxfs)}
    </styleSheet>
  `;
        return createXMLFile(parseXML(styleXml), "xl/styles.xml", "styles");
    }
    function createSharedStrings(strings) {
        const namespaces = [
            ["xmlns", NAMESPACE["sst"]],
            ["count", strings.length],
            ["uniqueCount", strings.length],
        ];
        const stringNodes = strings.map((string) => escapeXml /*xml*/ `<si><t>${string}</t></si>`);
        const xml = escapeXml /*xml*/ `
    <sst ${formatAttributes(namespaces)}>
      ${joinXmlNodes(stringNodes)}
    </sst>
  `;
        return createXMLFile(parseXML(xml), "xl/sharedStrings.xml", "sharedStrings");
    }
    function createRelsFiles(relsFiles) {
        const XMLRelsFiles = [];
        for (const relFile of relsFiles) {
            const relationNodes = [];
            for (const rel of relFile.rels) {
                const attributes = [
                    ["Id", rel.id],
                    ["Target", rel.target],
                    ["Type", rel.type],
                ];
                if (rel.targetMode) {
                    attributes.push(["TargetMode", rel.targetMode]);
                }
                relationNodes.push(escapeXml /*xml*/ `
        <Relationship ${formatAttributes(attributes)} />
      `);
            }
            const xml = escapeXml /*xml*/ `
      <Relationships xmlns="${NAMESPACE["Relationships"]}">
        ${joinXmlNodes(relationNodes)}
      </Relationships>
    `;
            XMLRelsFiles.push(createXMLFile(parseXML(xml), relFile.path));
        }
        return XMLRelsFiles;
    }
    function createContentTypes(files) {
        const overrideNodes = [];
        // hard-code supported image mimetypes
        const imageDefaultNodes = Object.entries(IMAGE_MIMETYPE_TO_EXTENSION_MAPPING).map(([mimetype, extension]) => createDefaultXMLElement(extension, mimetype));
        for (const file of files) {
            if ("contentType" in file && file.contentType) {
                overrideNodes.push(createOverride("/" + file.path, CONTENT_TYPES[file.contentType]));
            }
        }
        const relsAttributes = [
            ["Extension", "rels"],
            ["ContentType", "application/vnd.openxmlformats-package.relationships+xml"],
        ];
        const xmlAttributes = [
            ["Extension", "xml"],
            ["ContentType", "application/xml"],
        ];
        const xml = escapeXml /*xml*/ `
    <Types xmlns="${NAMESPACE["Types"]}">
      ${joinXmlNodes(Object.values(imageDefaultNodes))}
      <Default ${formatAttributes(relsAttributes)} />
      <Default ${formatAttributes(xmlAttributes)} />
      ${joinXmlNodes(overrideNodes)}
    </Types>
  `;
        return createXMLFile(parseXML(xml), "[Content_Types].xml");
    }
    function createRelRoot() {
        const attributes = [
            ["Id", "rId1"],
            ["Type", XLSX_RELATION_TYPE.document],
            ["Target", "xl/workbook.xml"],
        ];
        const xml = escapeXml /*xml*/ `
    <Relationships xmlns="${NAMESPACE["Relationships"]}">
      <Relationship ${formatAttributes(attributes)} />
    </Relationships>
  `;
        return createXMLFile(parseXML(xml), "_rels/.rels");
    }

    var Status;
    (function (Status) {
        Status[Status["Ready"] = 0] = "Ready";
        Status[Status["Running"] = 1] = "Running";
        Status[Status["RunningCore"] = 2] = "RunningCore";
        Status[Status["Finalizing"] = 3] = "Finalizing";
    })(Status || (Status = {}));
    class Model extends EventBus {
        corePlugins = [];
        featurePlugins = [];
        statefulUIPlugins = [];
        coreViewsPlugins = [];
        range;
        session;
        /**
         * In a collaborative context, some commands can be replayed, we have to ensure
         * that these commands are not replayed on the UI plugins.
         */
        isReplayingCommand = false;
        /**
         * A plugin can draw some contents on the canvas. But even better: it can do
         * so multiple times.  The order of the render calls will determine a list of
         * "layers" (i.e., earlier calls will be obviously drawn below later calls).
         * This list simply keeps the renderers+layer information so the drawing code
         * can just iterate on it
         */
        renderers = [];
        /**
         * Internal status of the model. Important for command handling coordination
         */
        status = 0 /* Status.Ready */;
        /**
         * The config object contains some configuration flag and callbacks
         */
        config;
        corePluginConfig;
        uiPluginConfig;
        state;
        selection;
        /**
         * Getters are the main way the rest of the UI read data from the model. Also,
         * it is shared between all plugins, so they can also communicate with each
         * other.
         */
        getters;
        /**
         * Getters that are accessible from the core plugins. It's a subset of `getters`,
         * without the UI getters
         */
        coreGetters;
        uuidGenerator;
        handlers = [];
        uiHandlers = [];
        coreHandlers = [];
        constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = true) {
            super();
            stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);
            const workbookData = load(data, verboseImport);
            this.state = new StateObserver();
            this.uuidGenerator = uuidGenerator;
            this.config = this.setupConfig(config);
            this.session = this.setupSession(workbookData.revisionId);
            this.coreGetters = {};
            this.range = new RangeAdapter(this.coreGetters);
            this.coreGetters.getRangeString = this.range.getRangeString.bind(this.range);
            this.coreGetters.getRangeFromSheetXC = this.range.getRangeFromSheetXC.bind(this.range);
            this.coreGetters.createAdaptedRanges = this.range.createAdaptedRanges.bind(this.range);
            this.coreGetters.getRangeDataFromXc = this.range.getRangeDataFromXc.bind(this.range);
            this.coreGetters.getRangeDataFromZone = this.range.getRangeDataFromZone.bind(this.range);
            this.coreGetters.getRangeFromRangeData = this.range.getRangeFromRangeData.bind(this.range);
            this.coreGetters.getRangeFromZone = this.range.getRangeFromZone.bind(this.range);
            this.getters = {
                isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
                isDashboard: () => this.config.mode === "dashboard",
            };
            this.uuidGenerator.setIsFastStrategy(true);
            // Initiate stream processor
            this.selection = new SelectionStreamProcessorImpl(this.getters);
            this.coreHandlers.push(this.range);
            this.handlers.push(this.range);
            this.corePluginConfig = this.setupCorePluginConfig();
            this.uiPluginConfig = this.setupUiPluginConfig();
            // registering plugins
            for (let Plugin of corePluginRegistry.getAll()) {
                this.setupCorePlugin(Plugin, workbookData);
            }
            Object.assign(this.getters, this.coreGetters);
            this.session.loadInitialMessages(stateUpdateMessages);
            for (let Plugin of coreViewsPluginRegistry.getAll()) {
                const plugin = this.setupUiPlugin(Plugin);
                this.coreViewsPlugins.push(plugin);
                this.handlers.push(plugin);
                this.uiHandlers.push(plugin);
                this.coreHandlers.push(plugin);
            }
            for (let Plugin of statefulUIPluginRegistry.getAll()) {
                const plugin = this.setupUiPlugin(Plugin);
                this.statefulUIPlugins.push(plugin);
                this.handlers.push(plugin);
                this.uiHandlers.push(plugin);
            }
            for (let Plugin of featurePluginRegistry.getAll()) {
                const plugin = this.setupUiPlugin(Plugin);
                this.featurePlugins.push(plugin);
                this.handlers.push(plugin);
                this.uiHandlers.push(plugin);
            }
            this.uuidGenerator.setIsFastStrategy(false);
            // starting plugins
            this.dispatch("START");
            // Model should be the last permanent subscriber in the list since he should render
            // after all changes have been applied to the other subscribers (plugins)
            this.selection.observe(this, {
                handleEvent: () => this.trigger("update"),
            });
            // This should be done after construction of LocalHistory due to order of
            // events
            this.setupSessionEvents();
            this.joinSession();
            if (config.snapshotRequested) {
                this.session.snapshot(this.exportData());
                this.garbageCollectExternalResources();
            }
            // mark all models as "raw", so they will not be turned into reactive objects
            // by owl, since we do not rely on reactivity
            owl.markRaw(this);
        }
        joinSession() {
            this.session.join(this.config.client);
        }
        leaveSession() {
            this.session.leave();
        }
        setupUiPlugin(Plugin) {
            const plugin = new Plugin(this.uiPluginConfig);
            for (let name of Plugin.getters) {
                if (!(name in plugin)) {
                    throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);
                }
                if (name in this.getters) {
                    throw new Error(`Getter "${name}" is already defined.`);
                }
                this.getters[name] = plugin[name].bind(plugin);
            }
            const layers = Plugin.layers.map((l) => [plugin, l]);
            this.renderers.push(...layers);
            this.renderers.sort((p1, p2) => p1[1] - p2[1]);
            return plugin;
        }
        /**
         * Initialize and properly configure a plugin.
         *
         * This method is private for now, but if the need arise, there is no deep
         * reason why the model could not add dynamically a plugin while it is running.
         */
        setupCorePlugin(Plugin, data) {
            const plugin = new Plugin(this.corePluginConfig);
            for (let name of Plugin.getters) {
                if (!(name in plugin)) {
                    throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);
                }
                if (name in this.coreGetters) {
                    throw new Error(`Getter "${name}" is already defined.`);
                }
                this.coreGetters[name] = plugin[name].bind(plugin);
            }
            plugin.import(data);
            this.corePlugins.push(plugin);
            this.coreHandlers.push(plugin);
            this.handlers.push(plugin);
        }
        onRemoteRevisionReceived({ commands }) {
            for (let command of commands) {
                const previousStatus = this.status;
                this.status = 2 /* Status.RunningCore */;
                this.dispatchToHandlers(this.statefulUIPlugins, command);
                this.status = previousStatus;
            }
            this.finalize();
        }
        setupSession(revisionId) {
            const session = new Session(buildRevisionLog({
                initialRevisionId: revisionId,
                recordChanges: this.state.recordChanges.bind(this.state),
                dispatch: (command) => {
                    const result = this.checkDispatchAllowed(command);
                    if (!result.isSuccessful) {
                        return;
                    }
                    this.isReplayingCommand = true;
                    this.dispatchToHandlers(this.coreHandlers, command);
                    this.isReplayingCommand = false;
                },
            }), this.config.transportService, revisionId);
            return session;
        }
        setupSessionEvents() {
            this.session.on("remote-revision-received", this, this.onRemoteRevisionReceived);
            this.session.on("revision-undone", this, ({ commands }) => {
                this.dispatchFromCorePlugin("UNDO", { commands });
                this.finalize();
            });
            this.session.on("revision-redone", this, ({ commands }) => {
                this.dispatchFromCorePlugin("REDO", { commands });
                this.finalize();
            });
            // How could we improve communication between the session and UI?
            // It feels weird to have the model piping specific session events to its own bus.
            this.session.on("unexpected-revision-id", this, () => this.trigger("unexpected-revision-id"));
            this.session.on("collaborative-event-received", this, () => {
                this.trigger("update");
            });
        }
        setupConfig(config) {
            const client = config.client || {
                id: this.uuidGenerator.uuidv4(),
                name: _t("Anonymous").toString(),
            };
            const transportService = config.transportService || new LocalTransportService();
            return {
                ...config,
                mode: config.mode || "normal",
                custom: config.custom || {},
                external: this.setupExternalConfig(config.external || {}),
                transportService,
                client,
                moveClient: () => { },
                snapshotRequested: false,
                notifyUI: (payload) => this.trigger("notify-ui", payload),
                raiseBlockingErrorUI: (text) => this.trigger("raise-error-ui", { text }),
            };
        }
        setupExternalConfig(external) {
            const loadLocales = external.loadLocales || (() => Promise.resolve(DEFAULT_LOCALES));
            return {
                ...external,
                loadLocales,
            };
        }
        setupCorePluginConfig() {
            return {
                getters: this.coreGetters,
                stateObserver: this.state,
                range: this.range,
                dispatch: this.dispatchFromCorePlugin,
                uuidGenerator: this.uuidGenerator,
                custom: this.config.custom,
                external: this.config.external,
            };
        }
        setupUiPluginConfig() {
            return {
                getters: this.getters,
                stateObserver: this.state,
                dispatch: this.dispatch,
                selection: this.selection,
                moveClient: this.session.move.bind(this.session),
                custom: this.config.custom,
                uiActions: this.config,
                session: this.session,
                defaultCurrencyFormat: this.config.defaultCurrencyFormat,
            };
        }
        // ---------------------------------------------------------------------------
        // Command Handling
        // ---------------------------------------------------------------------------
        /**
         * Check if the given command is allowed by all the plugins and the history.
         */
        checkDispatchAllowed(command) {
            const results = isCoreCommand(command)
                ? this.checkDispatchAllowedCoreCommand(command)
                : this.checkDispatchAllowedLocalCommand(command);
            if (results.some((r) => r !== "Success" /* CommandResult.Success */)) {
                return new DispatchResult(results.flat());
            }
            return DispatchResult.Success;
        }
        checkDispatchAllowedCoreCommand(command) {
            const results = this.corePlugins.map((handler) => handler.allowDispatch(command));
            results.push(this.range.allowDispatch(command));
            return results;
        }
        checkDispatchAllowedLocalCommand(command) {
            const results = this.uiHandlers.map((handler) => handler.allowDispatch(command));
            return results;
        }
        finalize() {
            this.status = 3 /* Status.Finalizing */;
            for (const h of this.handlers) {
                h.finalize();
            }
            this.status = 0 /* Status.Ready */;
        }
        /**
         * Check if a command can be dispatched, and returns a DispatchResult object with the possible
         * reasons the dispatch failed.
         */
        canDispatch = (type, payload) => {
            return this.checkDispatchAllowed(createCommand(type, payload));
        };
        /**
         * The dispatch method is the only entry point to manipulate data in the model.
         * This is through this method that commands are dispatched most of the time
         * recursively until no plugin want to react anymore.
         *
         * CoreCommands dispatched from this function are saved in the history.
         *
         * Small technical detail: it is defined as an arrow function.  There are two
         * reasons for this:
         * 1. this means that the dispatch method can be "detached" from the model,
         *    which is done when it is put in the environment (see the Spreadsheet
         *    component)
         * 2. This allows us to define its type by using the interface CommandDispatcher
         */
        dispatch = (type, payload) => {
            const command = createCommand(type, payload);
            let status = this.status;
            if (this.getters.isReadonly() && !canExecuteInReadonly(command)) {
                return new DispatchResult("Readonly" /* CommandResult.Readonly */);
            }
            if (!this.session.canApplyOptimisticUpdate()) {
                return new DispatchResult("WaitingSessionConfirmation" /* CommandResult.WaitingSessionConfirmation */);
            }
            switch (status) {
                case 0 /* Status.Ready */:
                    const result = this.checkDispatchAllowed(command);
                    if (!result.isSuccessful) {
                        return result;
                    }
                    this.status = 1 /* Status.Running */;
                    const { changes, commands } = this.state.recordChanges(() => {
                        if (isCoreCommand(command)) {
                            this.state.addCommand(command);
                        }
                        this.dispatchToHandlers(this.handlers, command);
                        this.finalize();
                    });
                    this.session.save(command, commands, changes);
                    this.status = 0 /* Status.Ready */;
                    this.trigger("update");
                    break;
                case 1 /* Status.Running */:
                    if (isCoreCommand(command)) {
                        const dispatchResult = this.checkDispatchAllowed(command);
                        if (!dispatchResult.isSuccessful) {
                            return dispatchResult;
                        }
                        this.state.addCommand(command);
                    }
                    this.dispatchToHandlers(this.handlers, command);
                    break;
                case 3 /* Status.Finalizing */:
                    throw new Error("Cannot dispatch commands in the finalize state");
                case 2 /* Status.RunningCore */:
                    if (isCoreCommand(command)) {
                        throw new Error(`A UI plugin cannot dispatch ${type} while handling a core command`);
                    }
                    this.dispatchToHandlers(this.handlers, command);
            }
            return DispatchResult.Success;
        };
        /**
         * Dispatch a command from a Core Plugin (or the History).
         * A command dispatched from this function is not added to the history.
         */
        dispatchFromCorePlugin = (type, payload) => {
            const command = createCommand(type, payload);
            const previousStatus = this.status;
            this.status = 2 /* Status.RunningCore */;
            const handlers = this.isReplayingCommand ? this.coreHandlers : this.handlers;
            this.dispatchToHandlers(handlers, command);
            this.status = previousStatus;
            return DispatchResult.Success;
        };
        /**
         * Dispatch the given command to the given handlers.
         * It will call `beforeHandle` and `handle`
         */
        dispatchToHandlers(handlers, command) {
            const isCommandCore = isCoreCommand(command);
            for (const handler of handlers) {
                if (!isCommandCore && handler instanceof CorePlugin) {
                    continue;
                }
                handler.beforeHandle(command);
            }
            for (const handler of handlers) {
                if (!isCommandCore && handler instanceof CorePlugin) {
                    continue;
                }
                handler.handle(command);
            }
        }
        // ---------------------------------------------------------------------------
        // Grid Rendering
        // ---------------------------------------------------------------------------
        /**
         * When the Grid component is ready (= mounted), it has a reference to its
         * canvas and need to draw the grid on it.  This is then done by calling this
         * method, which will dispatch the call to all registered plugins.
         *
         * Note that nothing prevent multiple grid components from calling this method
         * each, or one grid component calling it multiple times with a different
         * context. This is probably the way we should do if we want to be able to
         * freeze a part of the grid (so, we would need to render different zones)
         */
        drawGrid(context) {
            // we make sure here that the viewport is properly positioned: the offsets
            // correspond exactly to a cell
            for (let [renderer, layer] of this.renderers) {
                context.ctx.save();
                renderer.drawGrid(context, layer);
                context.ctx.restore();
            }
        }
        // ---------------------------------------------------------------------------
        // Data Export
        // ---------------------------------------------------------------------------
        /**
         * As the name of this method strongly implies, it is useful when we need to
         * export date out of the model.
         */
        exportData() {
            let data = createEmptyWorkbookData();
            for (let handler of this.handlers) {
                if (handler instanceof CorePlugin) {
                    handler.export(data);
                }
            }
            data.revisionId = this.session.getRevisionId() || DEFAULT_REVISION_ID;
            data = deepCopy(data);
            return data;
        }
        updateMode(mode) {
            if (mode !== "normal") {
                this.dispatch("CANCEL_EDITION");
            }
            // @ts-ignore For testing purposes only
            this.config.mode = mode;
            this.trigger("update");
        }
        /**
         * Exports the current model data into a list of serialized XML files
         * to be zipped together as an *.xlsx file.
         *
         * We need to trigger a cell revaluation  on every sheet and ensure that even
         * async functions are evaluated.
         * This prove to be necessary if the client did not trigger that evaluation in the first place
         * (e.g. open a document with several sheet and click on download before visiting each sheet)
         */
        exportXLSX() {
            this.dispatch("EVALUATE_CELLS");
            let data = createEmptyExcelWorkbookData();
            for (let handler of this.handlers) {
                if (handler instanceof BasePlugin) {
                    handler.exportForExcel(data);
                }
            }
            data = deepCopy(data);
            return getXLSX(data);
        }
        garbageCollectExternalResources() {
            for (const plugin of this.corePlugins) {
                plugin.garbageCollectExternalResources();
            }
        }
    }
    function createCommand(type, payload = {}) {
        const command = deepCopy(payload);
        command.type = type;
        return command;
    }

    /**
     * We export here all entities that needs to be accessed publicly by Odoo.
     *
     * Note that the __info__ key is actually completed by the build process (see
     * the rollup.config.js file)
     */
    const __info__ = {};
    const SPREADSHEET_DIMENSIONS = {
        MIN_ROW_HEIGHT,
        MIN_COL_WIDTH,
        HEADER_HEIGHT,
        HEADER_WIDTH,
        TOPBAR_HEIGHT,
        BOTTOMBAR_HEIGHT,
        DEFAULT_CELL_WIDTH,
        DEFAULT_CELL_HEIGHT,
        SCROLLBAR_WIDTH,
    };
    const registries = {
        autofillModifiersRegistry,
        autofillRulesRegistry,
        cellMenuRegistry,
        colMenuRegistry,
        linkMenuRegistry,
        functionRegistry,
        featurePluginRegistry,
        statefulUIPluginRegistry,
        coreViewsPluginRegistry,
        corePluginRegistry,
        rowMenuRegistry,
        sidePanelRegistry,
        figureRegistry,
        chartSidePanelComponentRegistry,
        chartComponentRegistry,
        chartRegistry,
        topbarMenuRegistry,
        topbarComponentRegistry,
        clickableCellRegistry,
        otRegistry,
        inverseCommandRegistry,
        urlRegistry,
        cellPopoverRegistry,
        numberFormatMenuRegistry,
        repeatLocalCommandTransformRegistry,
        repeatCommandTransformRegistry,
    };
    const helpers = {
        arg,
        toBoolean,
        toJsDate,
        toNumber,
        toString,
        toXC,
        toZone,
        toUnboundedZone,
        toCartesian,
        numberToLetters,
        lettersToNumber,
        UuidGenerator,
        formatValue,
        createCurrencyFormat,
        computeTextWidth,
        createEmptyWorkbookData,
        createEmptySheet,
        createEmptyExcelSheet,
        getDefaultChartJsRuntime,
        chartFontColor,
        ChartColors,
        EvaluationError,
        CellErrorLevel,
        getFillingMode,
        rgbaToHex,
        colorToRGBA,
        positionToZone,
        isDefined: isDefined$1,
        isMatrix,
        lazy,
        genericRepeat,
        createAction,
        createActions,
        transformRangeData,
    };
    const links = {
        isMarkdownLink,
        parseMarkdownLink,
        markdownLink,
        openLink,
        urlRepresentation,
    };
    const components = {
        ChartPanel,
        ChartFigure,
        ChartJsComponent,
        Grid,
        GridOverlay,
        ScorecardChart,
        LineConfigPanel,
        LineBarPieDesignPanel,
        BarConfigPanel,
        LineBarPieConfigPanel,
        GaugeChartConfigPanel,
        GaugeChartDesignPanel,
        ScorecardChartConfigPanel,
        ScorecardChartDesignPanel,
        FigureComponent,
        Menu,
        SelectionInput,
    };
    const hooks = {
        useDragAndDropListItems,
    };
    function addFunction(functionName, functionDescription) {
        functionRegistry.add(functionName, functionDescription);
        return {
            addFunction: (fName, fDescription) => addFunction(fName, fDescription),
        };
    }
    const constants = {
        DEFAULT_LOCALE,
    };

    exports.AbstractChart = AbstractChart;
    exports.CorePlugin = CorePlugin;
    exports.DispatchResult = DispatchResult;
    exports.EvaluationError = EvaluationError;
    exports.Model = Model;
    exports.Registry = Registry;
    exports.Revision = Revision;
    exports.SPREADSHEET_DIMENSIONS = SPREADSHEET_DIMENSIONS;
    exports.Spreadsheet = Spreadsheet;
    exports.UIPlugin = UIPlugin;
    exports.__info__ = __info__;
    exports.addFunction = addFunction;
    exports.astToFormula = astToFormula;
    exports.compile = compile;
    exports.compileTokens = compileTokens;
    exports.components = components;
    exports.constants = constants;
    exports.convertAstNodes = convertAstNodes;
    exports.coreTypes = coreTypes;
    exports.findCellInNewZone = findCellInNewZone;
    exports.functionCache = functionCache;
    exports.helpers = helpers;
    exports.hooks = hooks;
    exports.invalidateCFEvaluationCommands = invalidateCFEvaluationCommands;
    exports.invalidateDependenciesCommands = invalidateDependenciesCommands;
    exports.invalidateEvaluationCommands = invalidateEvaluationCommands;
    exports.iterateAstNodes = iterateAstNodes;
    exports.links = links;
    exports.load = load;
    exports.parse = parse;
    exports.parseTokens = parseTokens;
    exports.readonlyAllowedCommands = readonlyAllowedCommands;
    exports.registries = registries;
    exports.setDefaultSheetViewSize = setDefaultSheetViewSize;
    exports.setTranslationMethod = setTranslationMethod;
    exports.tokenize = tokenize;


    __info__.version = "17.0.12";
    __info__.date = "2024-02-16T15:05:31.161Z";
    __info__.hash = "8fa2bab99";


})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);
//# sourceMappingURL=o_spreadsheet.js.map
